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:

typescript
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:

typescript
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:

typescript
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:

typescript
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;
}
💡 Tip

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 }:

typescript
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:

typescript
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:

typescript
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 #

  1. Server-Side Pagination: Always forward pagination.page and pagination.limit to your backend query or API endpoint.
  2. Accurate Total Count: Return the exact total count in total. The frontend pagination toolbar computes total pages from Math.ceil(total / limit). If total is omitted, pagination controls will show a single page.
  3. Clamped Limits: The platform automatically sanitizes limit to fall between 1 and 100 (default: 25 or 50).
  4. Calculated Offset: pagination.offset is precalculated as (page - 1) * limit for 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 requests per execution. Consolidate external calls using batch endpoints where possible.
  • CRM Call Budget: Max 50 CRM operations per execution.
  • Logging: Use console.log() or console.warn() freely. All output is safely captured and streamed to the execution logs.