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.
/**
* 标准入口函数
* @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:
| Attribute | Type | Description |
|---|---|---|
context.userInfo | Object | Current logged-in user info (such as id, username, tenantCode). |
context.appCode | String | Current app code. |
context.tenantCode | String | Current tenant code. |
context.client | Object | Database 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.
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.
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.
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)
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.
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)
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)
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.
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.
// 获取数据集访问器
const ds = context.client.models.dataset_XXXXXXXXXX // "XXXXXXXXXX" 是数据集编码| Method | Description | Example arguments | Return value |
getOne(params) | Fetch one record | { id: 1 } | Object |
getList(params) | List query (paginated) | { page: 1, size: 20 } | List |
filter(params) | Advanced filter query | {
} | 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.
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
- 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 receivenull. - Await async calls: all database operations (
context.client.models) are asynchronous — you mustawaitthem, or the script continues executing without the data. - Exception handling: an
Errorthrown in the script is caught by the system and returned to the frontend, usually with status 500 (Failure). Keep error messages user-friendly. - 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
- 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.
- 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.