standards
SyncSDK

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. label and icon are excluded from the hash.
  • The builders are kept on source.builders so standards.from(builder) can infer record types.

The result is a plain SchemaSource: { id, hash, objects, views, builders, label, icon }.

What one sync does

  1. GET /schema/sources/:id. A 404 is not an error: the source was never pushed.
  2. Same hash, and every declared label / icon matches the stored one: return { applied: false, hash }. One request, nothing written.
  3. Otherwise PUT /schema/sources/:id with { hash, objects, views } plus label and icon when 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.

ErrorStatusCarries
SchemaSourceConflictError409objectName and owner (core, bundle:<id> or source:<id>)
SchemaSourceValidationError400The 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 codeMeaning
0Applied, or unchanged
1Conflict (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