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.

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