Skip to main content

Schema

Two modules from two libraries have to agree about something without either author having met the other. TypeScript's own answer, a shared interface in a shared package, does not survive contact with that: somebody has to own the package, everybody has to depend on the same version of it, and the first library that needs a field the interface does not have is stuck.

A schema is Octo's answer. It is a class describing a shape, and things are matched against it structurally: anything whose serialized form satisfies the shape is a match, whoever built it. Every contract in Octo is one, from a module's inputs to what an anchor publishes.

Declaring one​

export class ShopQueueAnchorSchema extends BaseAnchorSchema {
parentInstance: Service;

override properties = Schema<{
awsAccountId: string;
awsRegionId: string;
queueName: string;
}>();
}

Schema<T>() looks unusual because it does two jobs at once: it declares the property's type for TypeScript, and at runtime it leaves the property undefined so Octo can tell a missing value from a present one.

An argument makes it a default, and therefore optional:

messageRetentionSeconds ? = Schema<number>(345600);

Octo fills defaults in before your code runs, so inside a module messageRetentionSeconds is always a number even when the definition file omitted it. Mark the property optional with ? as well, so the caller's type matches.

What matching means​

When a schema is used to check a value, Octo builds an instance of the schema class and copies each declared key across. There are two ways that fails, and the messages are different on purpose:

Property "region" in schema could not be resolved!

The key is missing and has no default. Nothing supplied a value.

Property "region" in schema could not be validated!

A value arrived and a validation rule on that property rejected it.

Only declared keys are looked at. A value carrying extra fields still matches, which is what lets a library add to a model without breaking anybody matching on an older shape.

The four searches​

Every "find what another module built" query works this way, and they differ only in what they look at:

await model.getAnchorsMatchingSchema(ShopQueueAnchorSchema, [{ key: 'queueName', value: 'shop-orders' }]);
await model.getModelsMatchingSchema(RegionSchema);
await model.getOverlaysMatchingSchema(SomeOverlaySchema);
await model.getResourcesMatchingSchema(S3StorageSchema);

Each takes filters on property values, and each takes { searchBoundaryMembers: false } to search only the node you called it on rather than everything reachable from it. Anything that does not satisfy the schema is skipped rather than reported, so the result is a list of things that fit.

getResourcesMatchingSchema is narrower than it looks: it only considers resources produced by the modules that own the models it searched, so it returns another module's resources rather than every resource in the run.

When a shape has to change​

Matching on shape means a shape change breaks consumers. Sometimes that is the right change anyway, and the consumers cannot all move on the same day.

Octo can translate between two schemas: given the shape consumers ask for, the shape producers now publish, and a function from the second to the first, a lookup for the old schema finds anchors publishing the new one and translates them on the way out. That keeps old consumers working while they catch up.

Not reachable from a definition file yet

There is no way to register a translation from octo.yaml. It has no key for one, and a hooks export is called with no arguments, so nothing a project writes ever holds the object the translation would be registered on.

Until it has one, plan a schema change the slower way: publish both the old anchor and the new one from your model for a release, and remove the old one once nothing matches it.

Where schemas show up​

SchemaDescribesExtends
Module input schemaWhat a module acceptsnothing
Model schemaA model's serialized formthe base model's schema
Anchor schemaWhat a model publishesBaseAnchorSchema
Overlay schemaWhat an overlay holdsBaseOverlaySchema
Resource schemaA resource's properties and responseBaseResourceSchema

The rule for all of them is the same. A schema is the part you promise to keep; the class behind it is not.