standards
RecordsSDK

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 | null

Typed 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.

MethodSent 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(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.

TerminalRequestReturns
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 unwrappedThe 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 create and update accept, 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