Records
Read, filter, search and write the records of a synced object with a query that stays typed from your builder to the wire.
standards.from(builder) returns a typed query. Every filter, sort and
pagination call returns a new query; the terminals send one request.
import { getStandards } from "./client";
import { INCIDENT } from "./schema";
const incidents = getStandards().from(INCIDENT);
const open = await incidents
.eq("severity", "critical")
.isEmpty("resolvedAt")
.orderBy("startedAt", "desc")
.limit(10)
.fetch();
open.records[0]?.startedAt; // Date
open.total; // number | nullTyped or untyped
from(builder) reads the definition once. Attribute names are checked,
select and status values narrow to their options, and every date
attribute is revived to a Date on the way in.
from("incidents") is the untyped escape hatch. Records are
Record<string, unknown> plus metadata, and only the three system
timestamps (createdAt, updatedAt, deletedAt) are revived; your own
date attributes stay ISO strings.
Both refuse an empty, . or .. path segment (object name or id) with a
ValidationError before any request leaves: an id can never climb out of
/records/<object>/.
Filters
Queries are immutable: each method returns a new query, so a base query can be branched.
| Method | Sent as |
|---|---|
eq(attr, value) | is |
neq(attr, value) | is_not |
gt(attr, value) | greater_than |
gte(attr, value) | greater_or_equal |
lt(attr, value) | less_than |
lte(attr, value) | less_or_equal |
contains(attr, value) | contains |
notContains(attr, value) | not_contains |
startsWith(attr, text) | starts_with |
endsWith(attr, text) | ends_with |
in(attr, values) | any_of |
notIn(attr, values) | none_of |
isEmpty(attr) | is_empty |
isNotEmpty(attr) | is_not_empty |
Rules combine with and. or() flips the whole combinator; there is no
nesting.
const urgent = incidents.eq("severity", "critical").or().gte("impact", 8);
// filters: { combinator: "or", rules: [severity is critical, impact >= 8] }orderBy(attr, direction = "asc") is repeatable and appends. limit(n) and
offset(n) map to the page; the API answers 20 by default and never more
than 100.
Search
search(q) is a modifier, not a terminal. It routes the next fetch() to
POST /records/<object>/search with q, and keeps the filters, sorts and
page you set.
const hits = await incidents.search("latency").eq("severity", "major").limit(5).fetch();Terminals
fetch() resolves to FetchResult, whose shape is { records, total: number | null }: total is null unless the server counted, so never treat it as zero.
| Terminal | Request | Returns |
|---|---|---|
fetch() | POST /records/<object>/list (or /search) | { records, total: number | null } |
single() | fetch() with limit(1) | The first record or null |
get(id) | GET /records/<object>/<id> | The record; throws on 404 |
create(input) | POST /records/<object> with { data } | The created record |
update(id, patch) | PUT /records/<object>/<id>, patch unwrapped | The updated record |
delete(id) | DELETE /records/<object>/<id> | void |
const created = await incidents.create({
title: "API latency",
severity: "major",
startedAt: new Date(),
});
const same = await incidents.get(created.id);
await incidents.update(created.id, { severity: "minor" });
await incidents.delete(created.id);create wraps the input in { data }; update sends the patch as the body,
unwrapped, on a PUT. Only the keys you pass travel.
From the SDK's point of view delete is final: there is no restore, no purge, no bulk verb, no hydration of relations and no field projection. Deleting a record other records point at is never refused, see Relations and documents.
Dates on the wire
Outbound, every Date becomes an ISO string. Inbound, the system timestamps
and every date attribute of the builder become Date objects; a
.endDate() range has both start and end revived. Statically a date is
string | Date, so pass either.
Walk every page
fetch() returns one page. To read everything, walk the offsets until a
short page arrives.
import type { FetchResult, RecordsQuery } from "@stndrds/client";
const PAGE_SIZE = 100;
export async function fetchAll<TBuilder>(
query: RecordsQuery<TBuilder>
): Promise<FetchResult<TBuilder>["records"]> {
const all: FetchResult<TBuilder>["records"] = [];
for (let offset = 0; ; offset += PAGE_SIZE) {
const { records } = await query.limit(PAGE_SIZE).offset(offset).fetch();
all.push(...records);
if (records.length < PAGE_SIZE) return all;
}
}
const everyIncident = await fetchAll(incidents.orderBy("startedAt"));When a records route answers 404
The workspace answers, but has none of your objects: the schema was never
synced, or was released. Run the sync before reading, and treat this 404
as "never synced" rather than as a missing record. The isSchemaMissing predicate that sorts it out is on Errors.
Next steps
- Type safety: what
createandupdateaccept, and the six helpers - Errors:
status: 0, the auth errors, one handling pattern - Sync: push the schema before the first query
- Objects and records: system fields and metadata