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.
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
| Schema | Describes | Extends |
|---|---|---|
| Module input schema | What a module accepts | nothing |
| Model schema | A model's serialized form | the base model's schema |
| Anchor schema | What a model publishes | BaseAnchorSchema |
| Overlay schema | What an overlay holds | BaseOverlaySchema |
| Resource schema | A resource's properties and response | BaseResourceSchema |
The rule for all of them is the same. A schema is the part you promise to keep; the class behind it is not.