Sync
Push your objects and views to a workspace with one call, read what changed, and make the sync a CI step that fails for the right reasons.
defineSchemaSource turns your builders into one immutable payload.
standards.schema.sync pushes it, and only when something differs from the
workspace.
import { defineSchemaSource, object, text } from "@stndrds/client";
import { getStandards } from "./client";
const INCIDENT = object({ name: "incidents", label: "Incident" })
.attribute(text({ name: "title", label: "Title" }).required())
.labelExpression("{{ title }}");
export const statusSource = defineSchemaSource("status-site", {
objects: [INCIDENT],
views: [],
label: "Status page",
icon: "activity",
});
const result = await getStandards().schema.sync(statusSource);
console.log(result.applied ? "applied" : "unchanged", result.hash);Define a source
defineSchemaSource(id, { objects, views?, label?, icon? }) does its work at
definition time, before any request:
assertSchemaSourceId(id)refuses anything but a lowercase slug (^[a-z0-9][a-z0-9-]{0,62}$), with the same message the server would send.- Every builder is built once. A repeated object name throws
DuplicateError. - Every view is built and parsed through the canonical view schema.
hashSchemaSource({ objects, views })computes the sha256 of the canonical JSON.labelandiconare excluded from the hash.- The builders are kept on
source.builderssostandards.from(builder)can infer record types.
The result is a plain SchemaSource: { id, hash, objects, views, builders, label, icon }.
What one sync does
GET /schema/sources/:id. A404is not an error: the source was never pushed.- Same hash, and every declared
label/iconmatches the stored one: return{ applied: false, hash }. One request, nothing written. - Otherwise
PUT /schema/sources/:idwith{ hash, objects, views }pluslabelandiconwhen declared. The server recomputes the hash, checks ownership of every name, and applies the diff under a per-workspace lock.
A changed label still triggers the PUT, so the workspace picks it up
without re-applying any object. A label removed from your code is not sent
at all, so the stored label stays: a removed label never clears remotely.
The SyncResult
interface SyncResult {
applied: boolean;
hash: string;
objects?: { created: number; updated: number; deleted: number };
views?: { created: number; updated: number; adopted: number; released: number };
}objects and views are present after a PUT. deleted counts attributes
and objects that left the source; adopted counts workspace views that a
source claimed for the first time; released counts source views handed back
to the workspace as custom views. Print it in CI so a deploy log says what
the schema did:
const { applied, hash, objects, views } = await standards.schema.sync(statusSource);
console.log(JSON.stringify({ applied, hash, objects, views }));
// {"applied":true,"hash":"3f9c…","objects":{"created":1,"updated":0,"deleted":0},"views":{"created":0,"updated":0,"adopted":0,"released":0}}Errors
Two errors are mapped on the PUT only; the GET lets everything but a
404 through as a StandardsRequestError.
| Error | Status | Carries |
|---|---|---|
SchemaSourceConflictError | 409 | objectName and owner (core, bundle:<id> or source:<id>) |
SchemaSourceValidationError | 400 | The server message: bad id, oversize payload, hash mismatch, invalid definition, missing migration |
Both extend StandardsRequestError; a 401 or 403 arrives as
StandardsAuthError. Check the specific classes first.
import { SchemaSourceConflictError, SchemaSourceValidationError } from "@stndrds/client";
try {
await standards.schema.sync(statusSource);
} catch (error) {
if (error instanceof SchemaSourceConflictError) {
console.error(`"${error.objectName}" is owned by ${error.owner}`);
} else if (error instanceof SchemaSourceValidationError) {
console.error(error.message);
} else {
throw error;
}
}A sync script for CI
Give the sync its own script and let the process exit code carry the
outcome. 0 means applied or unchanged; 1 means the workspace refused the
source and a human has to look.
import {
SchemaSourceConflictError,
SchemaSourceValidationError,
StandardsAuthError,
} from "@stndrds/client";
import { getStandards } from "./client";
import { statusSource } from "./schema";
try {
const result = await getStandards().schema.sync(statusSource);
console.log(result.applied ? "schema.applied" : "schema.unchanged", result.hash);
process.exit(0);
} catch (error) {
if (error instanceof SchemaSourceConflictError) {
console.error("schema.conflict", error.objectName, error.owner);
process.exit(1);
}
if (error instanceof SchemaSourceValidationError || error instanceof StandardsAuthError) {
console.error("schema.rejected", error.message);
process.exit(1);
}
throw error;
}| Exit code | Meaning |
|---|---|
0 | Applied, or unchanged |
1 | Conflict (409), rejected definition (400), or refused key (401 / 403) |
Anything else (a timeout, a 5xx) is rethrown and fails the step with a
stack trace, which is what you want from an outage.
Next steps
- Schema sources: what the workspace keeps and what it demotes
- Errors: the whole error hierarchy and
instanceoforder - Records: operate the objects you just pushed
- Status page cookbook: the sync as one deploy step