Data Fetcher & System Pagination in fetcher.ts
#
The fetcher.ts file defines the asynchronous server-side data resolver for your custom listing page. Whenever a user applies a filter, changes a dropdown, or pages through records, Dolfin invokes the exported run function inside an isolated V8 sandbox.
The run Entry Point Signature
#
Every fetcher.ts file must export an asynchronous run function:
export async function run(
context: CustomListContext,
dolfin: DolfinSdk
): Promise<CustomListResponse> {
// 1. Destructure context inputs (filter is automatically typed from filters.ts)
const { filter, filters, pagination, moduleId, tenantId } = context;
// 2. Fetch or compute data rows
// ...
// 3. Return items matching the declared columns in filters.ts and total count
return {
items: [],
total: 0,
};
}
Execution Context (CustomListContext)
#
The context object provides sanitized, validated inputs for the current execution:
interface CustomListContext<TFilters = unknown> {
/** Active filter values selected by the user in the UI controls (typed from filters.ts) */
filter: InferFilterValues<TFilters>;
/** Complete list of filter configurations defined in filters.ts */
filters: FilterDefinition[];
/** System-managed pagination parameters */
pagination: {
/** Current 1-based page index (e.g. 1, 2, 3) */
page: number;
/** Number of rows requested per page (1 to 100) */
limit: number;
/** Computed database offset ((page - 1) * limit) */
offset: number;
};
/** Tenant ID of the active workspace */
tenantId: number;
/** Active module identifier */
moduleId: number;
}
Accessing Filter Values (context.filter)
#
context.filter exposes properties matching the key attributes defined in filters.ts. Values are automatically sanitized and typed:
const { status, search, stageId, minScore, isVip } = context.filter;
Response Return Contract (CustomListResponse)
#
Your run function returns an object satisfying CustomListResponse. The items array is strictly typed against the columns exported in filters.ts:
interface CustomListResponse<TRow = DeclaredRowType> {
/** Array of row objects matching declared column types */
items: TRow[];
/** Total count of all matching records across all pages */
total?: number;
}
Table columns are defined centrally in filters.ts via export const columns = defineColumns([...]);. The backend automatically projects returned rows against the declared columns, while the frontend formats values using the column's designated type renderer.
Common Implementation Patterns #
Pattern 1: Scoped CRM Leads Query #
Query leads within the current module using crm.leads.list. The list method accepts { page, limit, status, query, assignedTo, stageId } and returns { data: Lead[], total: number }:
export async function run(
context: CustomListContext,
dolfin: DolfinSdk
): Promise<CustomListResponse> {
const { filter, pagination, moduleId } = context;
const { page, limit } = pagination;
const crm = await dolfin.crm.module(moduleId);
const leadsResult = await crm.leads.list({
page,
limit,
status: filter.status ? [filter.status] : undefined,
stageId: filter.stageId ? Number(filter.stageId) : undefined,
query: filter.search || undefined,
});
return {
items: leadsResult.data || [],
total: leadsResult.total || 0,
};
}
Pattern 2: External REST API Integration #
Fetch external records from a microservice or billing provider via dolfin.http.get. Remember that dolfin.http options accept { headers?, timeoutMs? } (query parameters belong in the URL string) and the response body is available at res.body:
interface InvoiceRecord {
id: string;
customerName: string;
amount: number;
currency: string;
status: string;
issuedAt: string;
}
export async function run(
context: CustomListContext,
dolfin: DolfinSdk
): Promise<CustomListResponse> {
const { filter, pagination } = context;
const { page, limit } = pagination;
// Build query string directly in URL
const queryParams = new URLSearchParams({
page: String(page),
limit: String(limit),
});
if (filter.status) queryParams.set("status", String(filter.status));
if (filter.search) queryParams.set("q", String(filter.search));
const res = await dolfin.http.get<{ items: InvoiceRecord[]; count: number }>(
`https://billing.example.com/api/v1/invoices?${queryParams.toString()}`,
{
headers: {
"Authorization": `Bearer ${context.secrets.BILLING_API_KEY}`,
"Accept": "application/json",
},
timeoutMs: 8000,
}
);
// Response payload is accessed via res.body
const payload = res.body;
return {
items: payload.items || [],
total: payload.count || 0,
};
}
Pattern 3: Hybrid CRM Data with External Enrichment #
Retrieve CRM leads and decorate each row with external fraud or risk scoring data:
export async function run(
context: CustomListContext,
dolfin: DolfinSdk
): Promise<CustomListResponse> {
const { filter, pagination, moduleId } = context;
const crm = await dolfin.crm.module(moduleId);
// 1. Fetch paginated leads from CRM
const leadsResult = await crm.leads.list({
page: pagination.page,
limit: pagination.limit,
status: filter.status ? [filter.status] : undefined,
});
const leads = leadsResult.data || [];
// 2. Enrich with external verification service if leads exist
let enrichedRows = leads;
if (leads.length > 0) {
try {
const emailList = leads.map((l) => l.email).filter(Boolean);
const enrichRes = await dolfin.http.post<Record<string, { riskScore: number }>>(
"https://risk.example.com/v1/batch-verify",
{ emails: emailList },
{ timeoutMs: 5000 }
);
const riskMap = enrichRes.body || {};
enrichedRows = leads.map((lead) => ({
...lead,
riskScore: lead.email ? riskMap[lead.email]?.riskScore ?? "N/A" : "N/A",
}));
} catch (err) {
console.warn("External enrichment failed, displaying raw leads:", err);
}
}
return {
items: enrichedRows,
total: leadsResult.total || 0,
};
}
System Pagination & Viewport Management #
- Server-Side Pagination: Always forward
pagination.pageandpagination.limitto your backend query or API endpoint. - Accurate Total Count: Return the exact total count in
total. The frontend pagination toolbar computes total pages fromMath.ceil(total / limit). Iftotalis omitted, pagination controls will show a single page. - Clamped Limits: The platform automatically sanitizes
limitto fall between1and100(default:25or50). - Calculated Offset:
pagination.offsetis precalculated as(page - 1) * limitfor direct consumption in SQL queries or microservices.
Safety Constraints & Error Handling #
- Sandbox Timeout: Fetcher execution is terminated after
10,000 ms(10 seconds). - HTTP Call Budget: Max
5 requestsper execution. Consolidate external calls using batch endpoints where possible. - CRM Call Budget: Max
50 CRM operationsper execution. - Logging: Use
console.log()orconsole.warn()freely. All output is safely captured and streamed to the execution logs.