Schema and result types
Understand generated Salesforce metadata and infer query result types.
For application code, prefer a schema generated by @kysoql/codegen.
It reflects the objects, fields, relationships, and capabilities visible to the
Salesforce identity used during generation.
Use the generated schema
import { Kysoql } from "@kysoql/core";
import type { SalesforceSchema } from "./kysoql/salesforce.generated";
export const db = new Kysoql<SalesforceSchema>();The schema is a TypeScript snapshot, not a live permission check. Regenerate it when the Salesforce shape you rely on changes.
Handwritten schemas
Handwritten schemas are useful for small examples and isolated tests:
import {
Kysoql,
type SalesforceField,
type SalesforceObject,
} from "@kysoql/core";
interface DemoSchema {
readonly Account: SalesforceObject<{
readonly Id: SalesforceField<string, {
readonly salesforceType: "id";
readonly nullable: false;
readonly filterable: true;
readonly sortable: true;
readonly groupable: true;
readonly aggregatable: true;
}>;
readonly Name: SalesforceField<string, {
readonly salesforceType: "string";
readonly nullable: false;
readonly filterable: true;
readonly sortable: true;
readonly groupable: true;
}>;
}>;
}
const db = new Kysoql<DemoSchema>();
const query = db.selectFrom("Account").select(["Id", "Name"]).limit(10);
console.log(query.compile().soql);Do not copy guessed capabilities into a production schema. A field marked
filterable or groupable in TypeScript can still be rejected by Salesforce if
the metadata is wrong or stale.
Field metadata
SalesforceField<Value, Metadata> carries the field value type plus the
capabilities Kysoql needs while building a query.
| Property | What it controls |
|---|---|
salesforceType | Salesforce field type such as id, currency, or datetime |
nullable | Whether the value can be null |
filterable | Use in typed WHERE expressions |
sortable | Use in ORDER BY |
groupable | Use in GROUP BY |
aggregatable | Use with supported aggregate functions |
custom | Standard or custom grouping for FIELDS(...) |
polymorphic | Supported polymorphic relationship paths |
referenceTo, relationshipName | Parent relationship metadata |
activePicklistValue | Active multipicklist values used by typed filters |
Codegen maps Salesforce values to useful TypeScript types. Numeric fields become
number, booleans become boolean, common text and temporal fields become
string, and geolocation uses SalesforceGeolocation. Nullability is added from
the field metadata.
Object metadata
SalesforceObject also carries relationship and object-level capabilities such
as scopes, Knowledge data categories, MRU support, Data 360 options, and whether
the generated field list is complete.
A schema generated with an explicit field rule
marks that object as incomplete. Kysoql then disables selectFields(...) for the
object because Salesforce expands FIELDS(...) from live metadata, not from your
generated subset. Explicit .select(...) calls continue to work normally.
SalesforceParentRelationship records the referenced object, lookup field,
and nullability. SalesforceChildRelationship records the child object and
its reference field. The map keys are the relationship names used in SOQL.
Generated objects can expose "data360-dlo" or "data360-dmo" capabilities.
Use the generated value rather than enabling Data 360 options manually on a
normal CRM object.
Useful exported types
| Type | Use |
|---|---|
SalesforceFieldValue<Field> | Returned field value with nullability |
SalesforceFieldFilterValue<Field> | Value accepted by typed filters |
SalesforceRow<Object> | All direct fields on an object |
SalesforceQueryResult<Row> | Salesforce query envelope with records and paging metadata |
SalesforceGeolocation | { latitude, longitude } |
CompiledQuery<Output> | Compiled SOQL with a type-only output carrier |
Infer result types from the query
Avoid maintaining a second interface for selected rows when TypeScript can infer it from the query itself:
import { db } from "./db";
export function loadAccountNames() {
return db.selectFrom("Account").select(["Id", "Name"]).limit(100).execute();
}
export type AccountNames = Awaited<ReturnType<typeof loadAccountNames>>;
export type AccountName = AccountNames[number];The inferred type describes the expected result shape. It does not deeply validate or freeze values returned by an external transport at runtime.