Skip to content

Backend Function writing guide

This guide helps developers write dynamic scripts for the lovrabet-runtime platform. Use these scripts to implement data-permission filtering, dynamic masking, data enrichment, and complex standalone business logic.


1. Core conventions

To support high-performance async operations (such as database access), every business script must strictly follow this function signature.

1.1 Entry function signature

Every script must export an async function that takes two fixed arguments: params and context.

JavaScript
/**
 * 标准入口函数
 * @param {Object} params - 业务数据对象(如请求参数、查询结果),可原地修改。
 * @param {Object} context - 执行上下文,包含用户信息、应用信息和数据库操作能力。
 * @returns {Promise<Object>} - 返回修改后的 params 对象(对于 HOOK 脚本)或业务结果(对于 ENDPOINT 脚本)。
 */
export default async function functionName(params, context) {
    // 业务逻辑...
    return params;
}

1.2 Arguments in detail

params (business data)

  • Before stage (pre-operation script): the API request parameters (requestBody) — for example, query conditions or submitted form data.
  • After stage (post-operation script): the API response (responseBody.data) — for example, a queried list.
  • Endpoint script: the JSON from the HTTP request body.

context (execution context)

Provides the supporting information and capabilities the script needs:

AttributeTypeDescription
context.userInfoObjectCurrent logged-in user info (such as id, username, tenantCode).
context.appCodeStringCurrent app code.
context.tenantCodeStringCurrent tenant code.
context.clientObjectDatabase access entry point. Provides the models method for working with datasets.

2. Script types and examples

2.1 HOOK scripts (Before/After)

HOOK scripts attach to standard data APIs (such as getList, create) to intercept and modify data. They operate at the HTTP API layer.

Example 1: Before a list query — permission filter (Before)

Scenario: force users to see only data they created.

JavaScript
export default async function before(params, context) {
  // params 是 API 请求参数(如查询条件)
  
  // 强制注入过滤条件:create_by 必须等于当前用户 ID
  params.create_by = context.userInfo.id;
  
  // 必须返回修改后的 params
  return params;
}

Example 2: Filter by user role — permission filter (before)

Scenario: non-admin users see only the data they created.

JavaScript
export default async function(params, context) {
  // 非管理员只能看自己的数据
  if (context.userInfo.role !== "admin") {
    params.created_by = context.userInfo.id;
  }
  return params;
}

Example 3: Before data creation — business validation (Before)

Scenario: reject orders above the amount limit.

JavaScript
export default async function before(params, context) {
  // params 是提交的表单数据
  
  if (params.amount > 500) {
      throw new Error("无法创建超过 500 元的订单");;
  }
  
  // 补充默认值
  params.source = "WEB_APP";
  
  return params;
}

Example 4: Auto-fill fields (Before)

JavaScript
export default async function(params, context) {
  params.created_by = context.userInfo.id;
  params.created_at = new Date().toISOString();
  params.tenant_code = context.tenantCode;
  return params;
}

Example 5: Data masking (After)

Scenario: mask phone numbers in a returned list.

JavaScript
export default async function after(params, context) {
    // params 对应 response.data (通常包含 tableData 列表)
    const list = params.tableData;
    
    if (list && list.length > 0) {
        list.forEach(record => {
            if (record.phone) {
                // 正则替换
                record.phone = record.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
            }
        });
    }

    return params;
}

Example 6: Append computed fields (After)

JavaScript
export default async function(params, context) {
  // params 为接口返回的数据
  params.tableData?.forEach(record => {
    record.total_amount = record.price * record.quantity;
    record.is_vip = record.order_count > 10;
  });

  // 追加列定义(前端自动展示)
  const extraColumns = [
    { dataIndex: "total_amount", title: "总金额", type: "NUMBER" },
    { dataIndex: "is_vip", title: "VIP客户", type: "BOOLEAN" }
  ];
  extraColumns.forEach(col => params.tableColumns?.push(col));

  return params;
}

Example 7: Data-permission filtering (After)

JavaScript
export default async function(params, context) {
  // params 为接口返回的数据
  if (context.userInfo.role !== "admin") {
    params.tableColumns = params.tableColumns?.filter(col =>
      !["salary", "commission", "bank_account"].includes(col.dataIndex)
    );

    params.tableData?.forEach(record => {
      delete record.salary;
      delete record.commission;
      delete record.bank_account;
    });
  }
  return params;
}

Example 8: Run custom SQL

For a complete custom SQL example, see: <cite doc-id="IvGywDsMriMIwMkjo5ocTfchnvf" file-type="wiki" title="BF支持自定义SQL示例" type="doc"></cite>

2.2 ENDPOINT scripts (standalone endpoints)

INFO

For scenarios that require a standalone deployment: ...

A standalone endpoint script is a special kind of JS script with the type ENDPOINT.

  • API path: /api/endpoint/{appCode}/{scriptName}
  • Method: POST
  • Request body: a JSON object, passed to the script directly as params.

Example 1: Complex business logic (stock check and order placement)

This standalone endpoint script simulates an "order placement" transaction.

JavaScript
export default async function createOrder(params, context) {
  // 1. 校验库存
  const product = await context.client.models.dataset_XXXXXXXXXX.getOne({
    id: params.productId
  });

  if (!product || product.stock < params.quantity) {
    throw new Error(`库存不足,当前库存: ${product ? product.stock : 0}`);
  }

  // 2. 计算价格 (根据用户等级打折)                                        
  const user = await context.client.models.dataset_XXXXXXXXXX.getOne({
    id: context.userInfo.id
  });

  const discount = (user.vip_level >= 3) ? 0.8 : 1.0;
  const finalPrice = product.price * params.quantity * discount;

  // 3. 创建订单
  const orderId = await context.client.models.dataset_XXXXXXXXXX.create({
    user_id: context.userInfo.id,
    product_id: params.productId,
    quantity: params.quantity,
    amount: finalPrice,
    status: "pending",
    create_time: new Date()
  });

  // 4. 扣减库存 (更新操作)
  await context.client.models.dataset_XXXXXXXXXX.update({
      id: product.id,
      stock: product.stock - params.quantity
  });

  return {
    success: true,
    orderId: orderId,
    payAmount: finalPrice,
    discountRate: discount
  };
}

3. Dataset model access

Through context.client.models, scripts can access the app's datasets safely and asynchronously, with full await support.

3.1 Basic API

Every operation is keyed on the dataset code (datasetCode). The API is backed by interfaces implemented inside the Java application.

JavaScript
// 获取数据集访问器
const ds = context.client.models.dataset_XXXXXXXXXX // "XXXXXXXXXX" 是数据集编码
MethodDescriptionExample argumentsReturn value
getOne(params)Fetch one record{ id: 1 }Object
getList(params)List query (paginated){ page: 1, size: 20 }List
filter(params)Advanced filter query{

ytWhere: {

age: 18

}


}
List
create(data)Create data{ name: "John", age: 20 }None
update(data)Update data{ id: 1, name: "New Name" }true on success / false on failure
delete(params)Delete data{ id: 1 }true on success / false on failure

3.2 Join-like queries

After querying an order list, look up user details by user_id and merge them into the order records.

JavaScript
export default async function before(params, context) {
    const list = params.tableData;
    
    // 遍历列表(注意:为了性能,建议使用 Promise.all 并行查询)
    for (let i = 0; i < list.length; i++) {
        const order = list[i];
        if (order.user_id) {
            // 异步查询用户信息
            const user = await context.client.models.dataset_XXXXXXXXXX.getOne({
                id: order.user_id
            });
            // 数据增强
            if (user) {
                order.user_name = user.username;
                order.user_level = user.vip_level;
            }
        }
    }
    
    return params;
}

4. Best practices

  1. Return value: always end the function with return params (for HOOKs) or a business result object (for ENDPOINTs). Forget to return, and downstream steps may receive null.
  2. Await async calls: all database operations (context.client.models) are asynchronous — you must await them, or the script continues executing without the data.
  3. Exception handling: an Error thrown in the script is caught by the system and returned to the frontend, usually with status 500 (Failure). Keep error messages user-friendly.
  4. Rate limiting: to guard against infinite loops, each script execution has a cap on database calls (50 by default). Exceeding it throws an exception.

5. Script caching and propagation

For high performance, the system applies multi-level caching to dynamic scripts. After changing a script in the database, note how the change propagates:

5.1 Cache levels

  1. Metadata cache: a system aspect caches script lookups (including the script code) for 5 minutes. This means a change made in the database may take up to 5 minutes to be picked up.
  2. Execution cache: the script engine caches compiled JavaScript objects to speed up execution. This cache is least-recently-used (LRU) with a default TTL of 10 minutes.

5.2 Debugging tips

  • Development and testing: in a dev environment, restart the app temporarily or clear the cache through the admin API when you need changes to take effect immediately.
  • Production releases: after publishing a script to production, allow an observation window of at least 5-10 minutes to make sure every cluster node has refreshed its cache.

基于飞书知识库同步生成,内容以飞书源文档为准