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.
Every Salesforce query page fetched for one execution, including root pages and selected child relationship continuations.
Root records only. Child relationship sizes are controlled by their own query LIMIT values or by Salesforce completion when no limit is present.
No. This is Salesforce Query REST. Async iteration avoids collecting every root row at once, but each response page is still received and parsed as a normal REST response.
Errors and retries
| Error | What it means |
|---|---|
SalesforceRestError | Salesforce returned a non-success HTTP response. |
SalesforceResponseError | Salesforce returned malformed, incomplete, or unsafe response data. |
SalesforceQueryLimitError | A configured page or record budget would be exceeded. |
SalesforceOAuthError | Token acquisition or renewal failed. |
| Native fetch or abort error | The 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.