Skip to main content

Backend Function (I): Pre-validation - Data Validation and Interception

This is the first article in the Backend Function series, which consists of three articles:


Previous sections implemented list, detail, and create features, with basic validation added on the frontend. However, some validations must be done on the backend — such as phone number uniqueness, complex business rules, and data access control, which cannot be bypassed through the frontend.

This section introduces the Pre-validation Function (Before Script) of Backend Function: it is automatically triggered before data operations execute. If validation fails, the operation is blocked; if it passes, the data is written to the database.

What You'll Learn in This Section
  • What is a pre-validation function (Before Script)
  • How to configure pre-validation functions to intercept data operations
  • Common validation scenario implementations
  • Differences between pre-validation functions and frontend validation

Requirements

Before data is written to the database, automatically execute business rule validation:

  • Prevent duplicate data (phone number, email uniqueness)
  • Business rule validation (e.g., age restrictions)
  • Data access control (e.g., only operate on data within your department)
  • Auto-populate data (e.g., set department affiliation)

The overall workflow is:

User submits form

Frontend validation (basic format)

Pre-validation function (business rule validation)

Validation passed → Write to database
Validation failed → Return error message

Core Concepts

Pre-validation functions are also called Before Scripts or HOOK pre-scripts, which are automatically triggered before data operations execute. Throwing an exception in the function will block the operation, making it suitable for data validation, permission control, and data population.

FeatureDescription
Trigger timingBefore create, update, delete, filter operations
Execution locationBackend (Lovrabet server environment)
Blocking capabilityThrowing an exception blocks the operation
Function namingbeforeCreate, beforeUpdate, beforeFilter, etc.

Implementation Steps

Step 1: Create a Pre-validation Function on the Platform

Create an interceptor function on the Lovrabet platform:

Function Type: RequestInception (Pre-validation Function/Before Script)

Function Name: beforeCreate

Platform Configuration URL: https://app.lovrabet.com/app/{appCode}/data/dataset/{datasetId}#api-list

/**
* Customer creation pre-validation function - Data uniqueness validation and business rule verification
*
* [API Path] POST /api/{appCode}/customers/create
* Rule: /api/{applicationCode}/{datasetCode}/create
*
* [Platform Configuration] https://app.lovrabet.com/app/{appCode}/data/dataset/{datasetId}#api-list
* Note: appCode is the application code, datasetId is the dataset ID (integer, obtained from MCP get_dataset_detail)
*
* [HTTP Request Body Parameters]
* { "name": "customer_name", "phone": "phone_number", "email": "email" }
*
* [Return Data Structure]
* HOOK: Returns modified params object
*
* @param {Object} params - Request parameters
* @param {Object} context - Execution context (automatically injected by platform, no need to pass when calling)
* @param {Object} context.userInfo - Current user information
* @param {Object} context.client - Database operation entry point
* @returns {Object} Returns modified params
*/
export default async function beforeCreate(params, context) {
// Dataset code mapping table
// Comment format: Dataset: {dataset_name} | Table: {table_name}
const TABLES = {
customers: "dataset_XXXXXXXXXX", // Dataset: Customers | Table: customers (replace with actual 32-digit code)
};
const models = context.client.models;

console.log("[Pre-validation] Starting customer creation validation:", params);

// ========== Validation 1: Phone Number Uniqueness ==========
if (params.phone) {
// Dataset: Customers | Table: customers
const existingPhone = await models[TABLES.customers].filter({
where: {
phone: { $eq: params.phone },
...(params.id ? { id: { $ne: params.id } } : {}), // Exclude current record (for updates)
},
});

if (existingPhone.tableData && existingPhone.tableData.length > 0) {
throw new Error("Phone number is already in use, please use a different number");
}
}

// ========== Validation 2: Email Uniqueness ==========
if (params.email) {
// Dataset: Customers | Table: customers
const existingEmail = await models[TABLES.customers].filter({
where: {
email: { $eq: params.email },
...(params.id ? { id: { $ne: params.id } } : {}),
},
});

if (existingEmail.tableData && existingEmail.tableData.length > 0) {
throw new Error("Email is already in use, please use a different email");
}
}

// ========== Validation 3: Age Must Be At Least 18 ==========
if (params.birthday) {
const today = new Date();
const birthDate = new Date(params.birthday);
const age = today.getFullYear() - birthDate.getFullYear();
const monthDiff = today.getMonth() - birthDate.getMonth();

if (
monthDiff < 0 ||
(monthDiff === 0 && today.getDate() < birthDate.getDate())
) {
age--;
}

if (age < 18) {
throw new Error("Customer must be at least 18 years old");
}
}

// ========== Auto-populate: Set Department ==========
if (!params.department && context.userInfo?.department) {
params.department = context.userInfo.department;
}

// System fields (create_by, create_time, etc.) are automatically maintained by the platform, no manual setting required

return params;
}

Key Changes:

  • Use models[TABLES.customers] to access the dataset
  • context.userInfo replaces context.currentUser
  • filter returns { tableData, paging, tableColumns } structure
  • System fields (create_by, create_time) are automatically maintained by the platform

Step 2: Configure Pre-validation Function Binding

Configure the Before Script on the Lovrabet platform:

Platform Configuration URL: https://app.lovrabet.com/app/{appCode}/data/dataset/{datasetId}#api-list

Pre-validation Function Configuration

  1. Navigate to the dataset's API list page
  2. Select the operation to configure (e.g., create operation)
  3. In the "Business Logic Extension" section, find the "Pre-validation Function" card
  4. Click the "Write" button to open the script editor
  5. Write the Before Script code and save

Notes:

  • Each operation (create/update/delete/filter) can have its Before Script configured independently
  • Before Scripts are triggered before operation execution, and can be used for input validation, permission filtering, etc.
  • Status shows "None" if not configured; after configuration, the function name will be displayed

Step 3: No Frontend Code Changes Required

Frontend code remains unchanged; the SDK will automatically trigger the pre-validation function:

// Frontend submits form
const result = await lovrabetClient.models.dataset_customers.create({
name: "John Doe",
phone: "13800138000",
email: "johndoe@example.com",
birthday: "1990-01-01",
});

// If pre-validation fails, an exception will be thrown
try {
const result = await lovrabetClient.models.dataset_customers.create(data);
console.log("Created successfully:", result);
} catch (error) {
console.error("Creation failed:", error.message);
// Output: Phone number is already in use, please use a different number
}

Advanced Scenarios

Multi-Tenant Data Isolation

/**
* Multi-tenant data isolation pre-validation function (Before Create)
* Ensures users can only operate on data within their own tenant
*
* [API Path] POST /api/{appCode}/customers/create
* Rule: /api/{applicationCode}/{datasetCode}/create
*
* [Platform Configuration] https://app.lovrabet.com/app/{appCode}/data/dataset/{datasetId}#api-list
* Note: appCode is the application code, datasetId is the dataset ID (integer, obtained from MCP get_dataset_detail)
*
* [HTTP Request Body Parameters]
* { "name": "customer_name", ... }
*
* [Return Data Structure]
* HOOK: Returns modified params object
*
* @param {Object} params - Request parameters
* @param {Object} context - Execution context (automatically injected by platform, no need to pass when calling)
* @param {Object} context.userInfo - Current user information
* @returns {Object} Returns modified params
*/
export default async function beforeCreate(params, context) {
const TABLES = {
customers: "dataset_XXXXXXXXXX", // Dataset: Customers | Table: customers (replace with actual 32-digit code)
};
const models = context.client.models;

// Get user's tenant (from userInfo)
const userTenantId = context.userInfo?.tenantCode;

if (!userTenantId) {
throw new Error("User is not assigned to a tenant, cannot perform operation");
}

// Create operation: Automatically set tenant ID
params.tenant_id = userTenantId;

return params;
}

Data Status Transition Validation

/**
* Customer status transition validation (Before Update)
* Restricts status changes to follow specified workflow
*
* [API Path] POST /api/{appCode}/customers/update
* Rule: /api/{applicationCode}/{datasetCode}/update
*
* [Platform Configuration] https://app.lovrabet.com/app/{appCode}/data/dataset/{datasetId}#api-list
* Note: appCode is the application code, datasetId is the dataset ID (integer, obtained from MCP get_dataset_detail)
*
* [HTTP Request Body Parameters]
* { "id": 123, "status": "active" }
*
* [Return Data Structure]
* HOOK: Returns modified params object
*
* @param {Object} params - Request parameters
* @param {string} params.id - Record ID
* @param {string} params.status - New status
* @param {Object} context - Execution context (automatically injected by platform, no need to pass when calling)
* @returns {Object} Returns modified params
*/
export default async function beforeUpdate(params, context) {
const TABLES = {
customers: "dataset_XXXXXXXXXX", // Dataset: Customers | Table: customers (replace with actual 32-digit code)
};
const models = context.client.models;

if (!params.status || !params.id) {
return params;
}

// Get current record
// Dataset: Customers | Table: customers
const current = await models[TABLES.customers].findOne({ id: params.id });

if (!current) {
throw new Error("Record does not exist");
}

const currentStatus = current.status;
const newStatus = params.status;

// Define allowed status transitions
const allowedTransitions = {
potential: ["dealed", "lost"], // Potential → Closed/Lost
dealed: ["active", "inactive"], // Closed → Active/Inactive
lost: ["potential"], // Lost → Potential (reactivation)
active: ["inactive", "completed"], // Active → Inactive/Completed
inactive: ["active"], // Inactive → Active
};

const allowed = allowedTransitions[currentStatus] || [];

if (!allowed.includes(newStatus)) {
throw new Error(`Status change from "${currentStatus}" to "${newStatus}" is not allowed`);
}

return params;
}

function getStatusName(status) {
const names = {
potential: "Potential",
dealed: "Closed",
lost: "Lost",
active: "Active",
inactive: "Inactive",
completed: "Completed",
};
return names[status] || status;
}

Dynamically Fetch External Data for Validation

/**
* Validate customer credit limit
* Fetch customer credit information via external API
*/
export default async function validateCreditLimit(params, context) {
const { data } = params;

if (!data.customerId) {
return { data };
}

// Call external service to query customer credit
const creditInfo = await fetchCreditInfo(data.customerId);

if (creditInfo.blacklisted) {
throw new Error("This customer is blacklisted and cannot create orders");
}

const orderAmount = data.totalAmount || 0;
const availableCredit = creditInfo.creditLimit - creditInfo.usedCredit;

if (orderAmount > availableCredit) {
throw new Error(
`Exceeds credit limit. Available credit: ${availableCredit}, Order amount: ${orderAmount}`
);
}

return { data };
}

/**
* Simulate calling external credit service
* Replace with actual API call in production
*/
async function fetchCreditInfo(customerId) {
// This could be an HTTP request or other method to call external services
// For example: fetch('https://api.credit-service.com/check/' + customerId)

return {
blacklisted: false,
creditLimit: 100000,
usedCredit: 20000,
};
}

Key Concepts Summary

Before Script Function Signature

export default async function beforeCreate(params, context) {
// params - Request parameters (varies by operation type)
// context - Execution context (automatically injected by platform)

// Database operations
const models = context.client.models;

// Return modified params
return params;
}

params Parameter (varies by operation type)

Operation Typeparams Content
createSubmitted form data { name: "xxx", phone: "xxx" }
updateUpdated data { id: 123, name: "new" }
deleteDelete condition { id: 123 }
filterQuery parameters { where: {...}, select: [...] }

context Object

FieldTypeDescription
userInfoObjectCurrent logged-in user information (id, username, tenantCode)
clientObjectDatabase operation entry point, access datasets via models
appCodeStringCurrent application code

Dataset Operation Methods

// Define dataset mapping (at function start)
const TABLES = {
customers: "dataset_XXXXXXXXXX", // 32-digit dataset code
};
const models = context.client.models;

// Query single record
const record = await models[TABLES.customers].findOne({ id: 123 });

// Query list
const result = await models[TABLES.customers].filter({
where: { status: { $eq: "active" } },
});
// result.tableData is the data array

// Create
const id = await models[TABLES.customers].create({
name: "John Doe",
phone: "13800138000",
});

// Update
await models[TABLES.customers].update({
id: 123,
name: "New Name",
});

How to Block Operations

// Throwing an exception will block the operation and return an error message
throw new Error('Error message');

// Normal return continues execution
return params;

// Return value can be empty object or modified data
return {}; // No data modification
return { data: newData }; // Return modified data

FAQ

Q: What's the difference between pre-validation functions and frontend validation?

A: Both should be used, each serving its own purpose:

Validation LocationPurposeAdvantage
Frontend ValidationUser ExperienceImmediate feedback, reduces invalid requests
Pre-validation FunctionData SecurityMust execute, cannot be bypassed

Recommended Practice: Frontend handles basic format validation (e.g., phone number format), pre-validation functions handle business rule validation (e.g., phone number uniqueness).

Q: Can pre-validation functions modify data?

A: Yes. Returning { data: newData } will affect subsequent operations:

export default async function validate(params, context) {
const { data } = params;

// Auto-populate fields
if (!data.createTime) {
data.createTime = new Date().toISOString();
}

// Return modified data
return { data };
}

Q: How to debug pre-validation functions?

A: Use console.log to output logs:

export default async function beforeCreate(params, context) {
console.log("[Pre-validation] Input parameters:", JSON.stringify(params));
console.log("[Pre-validation] Current user:", context.userInfo?.username);

// Business logic...

console.log("[Pre-validation] Execution completed");
return params;
}

Then view the output in the Backend Function logs on the platform.

Q: Do pre-validation functions affect performance?

A: Pre-validation functions are executed for every data operation, so:

  1. Avoid time-consuming operations: Don't call slow external APIs in pre-validation functions
  2. Reduce database queries: Combine multiple queries, use indexes
  3. Use caching: For infrequently changing data (like configuration info), caching can be used
// Not recommended: Multiple queries
const TABLES = {
customers: "dataset_XXXXXXXXXX",
};
const models = context.client.models;

const existing1 = await models[TABLES.customers].filter({
where: { phone: { $eq: "13800138000" } },
});
const existing2 = await models[TABLES.customers].filter({
where: { email: { $eq: "test@example.com" } },
});

// Recommended: Single query
const existing = await models[TABLES.customers].filter({
where: {
$or: [
{ phone: { $eq: "13800138000" } },
{ email: { $eq: "test@example.com" } },
],
},
});

Section Summary

Congratulations on learning pre-validation functions! Key takeaways:

ConceptDescription
Before ScriptValidation function that executes automatically before data operations
throw Error()Throwing an exception blocks operation execution
context.userInfoGet current logged-in user information
models[TABLES.xxx]Access datasets in BF
Best Practices
  • Frontend validation for format checking, pre-validation functions for business rules
  • Avoid executing time-consuming operations in pre-validation functions
  • Use batch queries to optimize performance
  • Add logs for easier debugging

Next Steps

Core Documentation

Advanced Topics


Difficulty Level: L2 | Estimated Time: 35 minutes