Kysoql

Native REST execution

Run SOQL with native fetch, paginate safely, stream root results, and control requests.

@kysoql/rest is the native fetch executor for Kysoql. Use it when you want Salesforce REST execution without adding JSforce to your application.

Configure an executor

import { Kysoql } from "@kysoql/core";
import { createRestExecutor } from "@kysoql/rest";
import type { SalesforceSchema } from "./kysoql/salesforce.generated";

export function createDatabase(instanceUrl: string, accessToken: string) {
  const executor = createRestExecutor(
    {
      instanceUrl,
      accessToken,
      apiVersion: "65.0",
      timeoutMs: 30_000,
    },
    {
      batchSize: 1000,
      maxPages: 100,
      maxRecords: 100_000,
    },
  );

  return {
    db: new Kysoql<SalesforceSchema>({ executor }),
    executor,
  };
}

Use the authenticated org's HTTPS instance URL. batchSize, maxPages, and maxRecords are client-side controls, not changes to your SOQL LIMIT.

The REST client defaults to API version 65.0. Set the version explicitly when your application needs a different supported Salesforce API version. Runtime options do not read kysoql.config.ts or environment variables automatically.

Execute records and counts

Once the executor is attached, normal builder methods can run directly:

import { db } from "./db";

const accounts = await db
  .selectFrom("Account")
  .select(["Id", "Name"])
  .where("Name", "like", "Acme%")
  .limit(100)
  .execute();

const count = await db
  .selectFrom("Account")
  .select(({ fn }) => fn.count())
  .execute();

Record and grouped aggregate queries return readonly arrays. Bare COUNT() returns a number.

The executor follows Salesforce root continuation pages automatically. Selected child relationship queries are completed too, including supported nested child subqueries. Each compiled LIMIT remains the record ceiling for that query level.

No matches return [] for row queries or 0 for a bare count. Errors are never converted into empty success results.

QueryAll is a separate query mode

Use .executeAll() only when you need Salesforce QueryAll semantics:

import { db } from "./db";

const recordsIncludingDeleted = await db
  .selectFrom("Account")
  .select(["Id", "Name", "IsDeleted"])
  .limit(100)
  .executeAll();

execute() starts with Salesforce /query. executeAll() starts with /queryAll so eligible deleted records and archived activities can be included.

Both modes paginate. QueryAll does not bypass permissions, and it does not recover permanently removed records. Apex ALL ROWS is a separate compile-only feature.

Iterate large root results

.execute() collects the root result in memory. When you want to process root rows progressively, call the executor with a compiled query:

import type { RestExecutor } from "@kysoql/rest";
import { db } from "./db";

export async function processAccounts(
  executor: RestExecutor,
  signal: AbortSignal,
) {
  const query = db.selectFrom("Account").select(["Id", "Name"]).compile();

  for await (const account of executor.iterateQuery(query, { signal })) {
    console.log(account.Id);
  }
}

queryPages() yields complete root response pages. iterateQuery() yields root records.

Before a root page is yielded, any selected child relationship continuations in that page are completed. This keeps relationship results convenient, but it also means one root page can involve several Salesforce requests.

Stopping iteration prevents later root pages from being fetched. Rows already yielded cannot be rolled back if a later request fails.

Cancellation and timeouts

Builder execution accepts { signal }:

import { db } from "./db";

const accounts = await db
  .selectFrom("Account")
  .select(["Id", "Name"])
  .execute({ signal: AbortSignal.timeout(60_000) });

The executor also accepts signal and timeoutMs on direct calls.

The configured timeoutMs applies to each HTTP request rather than the whole multi-page query. Use an overall AbortSignal when you need a deadline for the complete operation.

Child relationship pagination

You do not need to follow child nextRecordsUrl values yourself when using the native executor:

import { db } from "./db";

const accounts = await db
  .selectFrom("Account")
  .select("Id")
  .selectSubquery("Contacts", (contacts) =>
    contacts
      .select(["Id", "LastName"])
      .selectSubquery("Cases", (cases) =>
        cases.select(["Id", "Subject"]).limit(50),
      )
      .limit(500),
  )
  .limit(25)
  .execute();

Put deliberate limits on relationships that can grow large. Without a child LIMIT, the executor drains that selected relationship until Salesforce marks it complete.

Resource limits

The default page budget is 10,000 requests across root and relationship continuation pages. There is no root record cap unless you set maxRecords.

A budget overflow throws SalesforceQueryLimitError rather than returning a truncated result.

batchSize can request a Salesforce query batch size from 200 to 2000. Salesforce may still return fewer root rows, especially when relationship rows contribute to the response size.

Errors and retries

ErrorWhat it means
SalesforceRestErrorSalesforce returned a non-success HTTP response.
SalesforceResponseErrorSalesforce returned malformed, incomplete, or unsafe response data.
SalesforceQueryLimitErrorA configured page or record budget would be exceeded.
SalesforceOAuthErrorToken acquisition or renewal failed.
Native fetch or abort errorThe network request failed, timed out, redirected, or was cancelled.

When an access-token provider is configured, the only automatic replay is one request after 401 INVALID_SESSION_ID. Rate limits, network failures, 5xx responses, and general query errors are not retried automatically.

Structured error messages avoid placing credentials or SOQL in the main message. Salesforce error details can still contain business data, so treat them as sensitive when logging.

Share a REST client

Codegen and query execution can share one native client:

import { createRestDescribeClient } from "@kysoql/codegen";
import { createRestClient, createRestExecutor } from "@kysoql/rest";

export function createClients(instanceUrl: string, accessToken: string) {
  const client = createRestClient({ instanceUrl, accessToken });

  return {
    executor: createRestExecutor(client),
    describe: createRestDescribeClient(client),
  };
}

The lower-level RestClient.request() is GET-only and returns unknown. Prefer the typed executor and Describe client for supported operations unless you have a specific reason to work at the raw REST layer.

On this page