@segmentstream-cloud/ontology 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +51 -15
  2. package/dist/aggregate-routes.d.ts +95 -0
  3. package/dist/aggregate-routes.js +332 -0
  4. package/dist/analyze.js +83 -16
  5. package/dist/entity.d.ts +31 -15
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.js +1 -1
  8. package/dist/manifest.d.ts +57 -23
  9. package/dist/manifest.js +77 -30
  10. package/dist/measure.d.ts +11 -2
  11. package/dist/measure.js +14 -1
  12. package/dist/migrate/apply.d.ts +1 -1
  13. package/dist/migrate/apply.js +36 -3
  14. package/dist/migrate/plan.d.ts +1 -1
  15. package/dist/migrate/plan.js +38 -13
  16. package/dist/migrate/storage.d.ts +3 -2
  17. package/dist/migrate/storage.js +15 -5
  18. package/dist/names.d.ts +4 -2
  19. package/dist/names.js +4 -1
  20. package/dist/query.d.ts +5 -2
  21. package/dist/query.js +6 -2
  22. package/dist/relationship.d.ts +36 -5
  23. package/dist/relationship.js +5 -3
  24. package/dist/render.d.ts +47 -3
  25. package/dist/render.js +117 -11
  26. package/dist/replication/baseline.d.ts +19 -0
  27. package/dist/replication/baseline.js +97 -0
  28. package/dist/replication/bigquery.d.ts +23 -0
  29. package/dist/replication/bigquery.js +63 -0
  30. package/dist/replication/model.d.ts +38 -0
  31. package/dist/replication/model.js +99 -0
  32. package/dist/replication/schema.d.ts +7 -0
  33. package/dist/replication/schema.js +123 -0
  34. package/dist/replication/store.d.ts +35 -0
  35. package/dist/replication/store.js +88 -0
  36. package/dist/replication/transaction.d.ts +4 -0
  37. package/dist/replication/transaction.js +11 -0
  38. package/dist/runtime/pipeline.js +2 -1
  39. package/dist/runtime/principals.js +6 -0
  40. package/dist/runtime/service-binding.js +8 -0
  41. package/dist/runtime/sql-runtime.js +2 -1
  42. package/dist/service/aggregate-platform.d.ts +60 -0
  43. package/dist/service/aggregate-platform.js +189 -0
  44. package/dist/service/aggregate.d.ts +42 -0
  45. package/dist/service/aggregate.js +145 -0
  46. package/dist/service/api.d.ts +5 -3
  47. package/dist/service/api.js +52 -6
  48. package/dist/service/catalog.d.ts +10 -1
  49. package/dist/service/catalog.js +12 -3
  50. package/dist/service/config.d.ts +13 -1
  51. package/dist/service/config.js +11 -1
  52. package/dist/service/errors.d.ts +32 -0
  53. package/dist/service/errors.js +102 -0
  54. package/dist/service/handlers.d.ts +3 -1
  55. package/dist/service/handlers.js +67 -29
  56. package/dist/service/mcp/server.d.ts +5 -0
  57. package/dist/service/mcp/server.js +61 -9
  58. package/dist/service/mcp/tools.d.ts +13 -2
  59. package/dist/service/mcp/tools.js +165 -6
  60. package/dist/service/operations.d.ts +2 -1
  61. package/dist/service/operations.js +16 -6
  62. package/dist/service/plan.js +7 -1
  63. package/dist/service/replication.d.ts +5 -0
  64. package/dist/service/replication.js +25 -0
  65. package/dist/service/runtime-contract.d.ts +1 -0
  66. package/dist/service/schemas.d.ts +99 -1
  67. package/dist/service/schemas.js +150 -2
  68. package/dist/service/service.js +10 -2
  69. package/dist/user.d.ts +1 -1
  70. package/dist/warehouse-backed.js +222 -121
  71. package/package.json +1 -1
package/dist/analyze.js CHANGED
@@ -5,7 +5,7 @@ import { BLOB_CONTENT_TYPES, MAX_BLOB_BYTES } from "./blob.js";
5
5
  import { isAction } from "./action.js";
6
6
  import { CONNECTION_KEY, isConnection } from "./connection.js";
7
7
  import { isEntity } from "./entity.js";
8
- import { LEGACY_MEMBER_NAME, ENTITY_ID, GENERATED_ACTIONS, GENERATED_UPSERT_PREFIX, HISTORY_SUFFIX, MCP_READ_TOOLS, MCP_TOOL_NAME_MAX_LENGTH, MEMBER_ROLE, mcpToolName, RESERVED_ENTITY_NAMES, RESERVED_MEMBER_NAMES, RESERVED_RELATIONSHIP_NAMES, SNAKE_CASE_NAME, SYSTEM_PROPERTIES, isSystemProperty, } from "./names.js";
8
+ import { AGGREGATE_ROUTE, LEGACY_MEMBER_NAME, ENTITY_ID, GENERATED_ACTIONS, GENERATED_UPSERT_PREFIX, HISTORY_SUFFIX, MCP_READ_TOOLS, MCP_TOOL_NAME_MAX_LENGTH, MEMBER_ROLE, mcpToolName, RESERVED_ENTITY_NAMES, RESERVED_MEMBER_NAMES, RESERVED_RELATIONSHIP_NAMES, SNAKE_CASE_NAME, SYSTEM_PROPERTIES, isSystemProperty, } from "./names.js";
9
9
  import { REFERENCE_ANNOTATION, propertyDefinition, referenceTarget, } from "./property.js";
10
10
  import { generatedActionPermissions, parsePermission, permissionOf, READ_VERB, rolesGranting, WILDCARD, } from "./permissions.js";
11
11
  import { ON_DELETE, isRelationship } from "./relationship.js";
@@ -63,6 +63,7 @@ export function analyze(config, options = {}) {
63
63
  checkConnectionsUsed(resolved, connections, config.connections ?? [], report);
64
64
  if (options.checkPermissions !== false) {
65
65
  checkPermissions(resolved, roles, report);
66
+ checkOpenWarehouseReads({ entities: resolved, roles, signIn, report });
66
67
  }
67
68
  return { entities: resolved, roles, signIn, connections, diagnostics };
68
69
  }
@@ -305,6 +306,10 @@ function resolveEntity(candidate, scope) {
305
306
  else if (name.endsWith(HISTORY_SUFFIX)) {
306
307
  report("reserved-name", name, `names ending in ${HISTORY_SUFFIX} are reserved for history tables.`);
307
308
  }
309
+ else if (name === AGGREGATE_ROUTE) {
310
+ report("reserved-name", name, `/api/ontology/${name} answers aggregates across the ontology's entities; ` +
311
+ "choose another entity name.");
312
+ }
308
313
  else if (RESERVED_ENTITY_NAMES.includes(name)) {
309
314
  report("reserved-name", name, `/api/ontology/${name} is a route of the ontology service.`);
310
315
  }
@@ -514,22 +519,44 @@ function checkRelationship(path, relationship, report) {
514
519
  (relationship.phrase.length !== 2 || relationship.phrase.some((part) => isBlank(part)))) {
515
520
  report("invalid-phrase", path, "phrase names both directions: [forward, inverse].");
516
521
  }
517
- if (relationship.cardinality === "many") {
522
+ // checkWarehouseSource refuses a delete rule or optionality on a relationship matched by value.
523
+ if (relationship.cardinality === "many" || relationship.on !== undefined) {
518
524
  return;
519
525
  }
520
- if (!ON_DELETE.includes(relationship.onDelete)) {
526
+ // Undeclared means restrict.
527
+ const onDelete = relationship.onDelete ?? "restrict";
528
+ if (!ON_DELETE.includes(onDelete)) {
521
529
  report("invalid-on-delete", path, `onDelete is one of ${ON_DELETE.join(", ")}.`);
522
530
  }
523
- else if (relationship.onDelete === "setNull" && !relationship.optional) {
531
+ else if (onDelete === "setNull" && !relationship.optional) {
524
532
  report("invalid-on-delete", path, 'onDelete "setNull" needs an optional relationship; use "cascade" or "restrict".');
525
533
  }
526
534
  }
535
+ /**
536
+ * A relationship with a warehouse-backed end is matched by value, through `on`, and has no
537
+ * `<name>_id` key. Its declaration may still lack `on`, which checkWarehouseSource reports.
538
+ */
539
+ function isValueMatchedDeclaration(input) {
540
+ const { owner, relationship, target } = input;
541
+ return (relationship.cardinality === "one" &&
542
+ (relationship.on !== undefined ||
543
+ owner.definition.source !== undefined ||
544
+ target?.definition.source !== undefined));
545
+ }
527
546
  function resolveIndexes(candidate, properties, relationships, scope) {
547
+ const valueMatched = new Set(relationships
548
+ .filter(({ relationship, target }) => isValueMatchedDeclaration({
549
+ owner: candidate,
550
+ relationship,
551
+ target: target === undefined ? undefined : scope.entitiesByName.get(target),
552
+ }))
553
+ .map((relationship) => relationship.name));
528
554
  const fields = new Set([
529
555
  ...SYSTEM_PROPERTIES,
530
556
  ...properties.map((property) => property.name),
531
557
  ...relationships
532
558
  .filter((relationship) => relationship.relationship.cardinality === "one")
559
+ .filter((relationship) => !valueMatched.has(relationship.name))
533
560
  .map((relationship) => relationship.name),
534
561
  ]);
535
562
  const indexes = candidate.definition.indexes ?? [];
@@ -545,7 +572,10 @@ function resolveIndexes(candidate, properties, relationships, scope) {
545
572
  scope.report("invalid-index", path, "an index covers at least one field.");
546
573
  }
547
574
  for (const field of resolved.properties) {
548
- if (!fields.has(field)) {
575
+ if (valueMatched.has(field)) {
576
+ scope.report("invalid-index", path, `"${field}" is matched by value and has no key column. Index the property named in on instead.`);
577
+ }
578
+ else if (!fields.has(field)) {
549
579
  scope.report("invalid-index", path, `"${field}" is not a property, system property or rel.one relationship of ${candidate.name}.`);
550
580
  }
551
581
  }
@@ -761,22 +791,22 @@ function checkEntityIdentities(entities, report) {
761
791
  * one namespace: they are the record's fields and the API's traversal paths.
762
792
  */
763
793
  function checkMemberNames(entities, report) {
764
- const members = new Map(entities.map((candidate) => [candidate, ownMembers(candidate)]));
765
794
  const entitiesByName = new Map();
766
795
  for (const candidate of [...entities].reverse()) {
767
796
  entitiesByName.set(candidate.name, candidate);
768
797
  }
798
+ const members = new Map(entities.map((candidate) => [candidate, ownMembers(candidate, entitiesByName)]));
769
799
  for (const candidate of entities) {
770
800
  for (const { name, relationship, target } of candidate.relationships) {
771
801
  const landing = target === undefined ? undefined : entitiesByName.get(target);
772
802
  if (landing === undefined) {
773
803
  continue;
774
804
  }
775
- members.get(landing)?.push({
776
- name: relationship.inverse,
777
- path: `${candidate.name}.relationships.${name}.inverse`,
778
- inverse: true,
779
- });
805
+ const path = `${candidate.name}.relationships.${name}.inverse`;
806
+ members.get(landing)?.push({ name: relationship.inverse, path, inverse: true });
807
+ if (Object.hasOwn(landing.entity.definition.measures ?? {}, relationship.inverse)) {
808
+ report("inverse-collision", path, `${relationship.inverse} is already a measure of ${landing.name}.`);
809
+ }
780
810
  }
781
811
  }
782
812
  for (const [candidate, list] of members) {
@@ -792,18 +822,24 @@ function checkMemberNames(entities, report) {
792
822
  }
793
823
  }
794
824
  }
795
- function ownMembers(candidate) {
825
+ function ownMembers(candidate, entitiesByName) {
796
826
  const properties = candidate.properties.map((property) => ({
797
827
  name: property.name,
798
828
  path: `${candidate.name}.properties.${property.name}`,
799
829
  inverse: false,
800
830
  }));
801
- const relationships = candidate.relationships.flatMap(({ name, relationship }) => {
831
+ const relationships = candidate.relationships.flatMap(({ name, relationship, target }) => {
802
832
  const path = `${candidate.name}.relationships.${name}`;
803
833
  const own = { name, path, inverse: false };
804
- return relationship.cardinality === "one" && candidate.entity.definition.source === undefined
805
- ? [own, { ...own, name: `${name}_id` }]
806
- : [own];
834
+ const landing = target === undefined ? undefined : entitiesByName.get(target);
835
+ // Only a stored → stored rel.one has a `<name>_id` key.
836
+ const keyed = relationship.cardinality === "one" &&
837
+ !isValueMatchedDeclaration({
838
+ owner: candidate.entity,
839
+ relationship,
840
+ target: landing?.entity,
841
+ });
842
+ return keyed ? [own, { ...own, name: `${name}_id` }] : [own];
807
843
  });
808
844
  return [...properties, ...relationships];
809
845
  }
@@ -880,6 +916,37 @@ function checkRolePermissions(candidate, verbs, report) {
880
916
  }
881
917
  });
882
918
  }
919
+ /**
920
+ * Under the open policy anyone who signs in holds `member`. Warehouse-backed entities hold company
921
+ * figures, such as revenue or stock, that aggregates read with `<entity>:read`; a wildcard of
922
+ * member's (`*:read`, the default, or `*:*`) must not hand them to strangers. Granting a
923
+ * warehouse-backed entity by name, `<entity>:read` or `<entity>:*`, is a deliberate choice.
924
+ */
925
+ function checkOpenWarehouseReads(input) {
926
+ const { entities, roles, signIn, report } = input;
927
+ const warehouseBacked = entities
928
+ .filter((candidate) => candidate.entity.definition.source !== undefined)
929
+ .map((candidate) => candidate.name);
930
+ const memberPermissions = roles.find((candidate) => candidate.name === MEMBER_ROLE)?.permissions;
931
+ if (signIn.policy !== "open" ||
932
+ warehouseBacked.length === 0 ||
933
+ !Array.isArray(memberPermissions)) {
934
+ return;
935
+ }
936
+ const wildcards = memberPermissions.filter((permission) => {
937
+ const parsed = parsePermission(permission);
938
+ return (parsed !== undefined &&
939
+ parsed.entity === WILDCARD &&
940
+ (parsed.verb === WILDCARD || parsed.verb === READ_VERB));
941
+ });
942
+ if (wildcards.length === 0) {
943
+ return;
944
+ }
945
+ report("invalid-sign-in", "signIn.policy", `signIn policy open lets anyone who signs in hold member; member's ${wildcards.join(", ")} ` +
946
+ `${wildcards.length === 1 ? "reaches" : "reach"} warehouse-backed entities ` +
947
+ `(${warehouseBacked.join(", ")}). Grant them to member by name, such as ` +
948
+ `${warehouseBacked[0]}:read, or use the invited policy.`);
949
+ }
883
950
  /**
884
951
  * Every permission a role lists must match an entity and a verb of the ontology, and an authored
885
952
  * action that no role grants is a warning. Generated actions are not warned about: they stay closed
package/dist/entity.d.ts CHANGED
@@ -50,9 +50,16 @@ type KeysWhere<T, Condition> = {
50
50
  type RequiredPropertyKeys<P> = {
51
51
  [K in keyof P]-?: RequiredOnCreate<P[K]> extends true ? K : never;
52
52
  }[keyof P] & string;
53
- type OneKeys<R> = KeysWhere<R, OneRelationship>;
54
- type RequiredOneKeys<R> = KeysWhere<R, OneRelationship<false>>;
53
+ /** Relationships declared with `on`: matched by value, so with no `<relationship>Id`. */
54
+ type MatchedKeys<R> = KeysWhere<R, {
55
+ readonly "~ontology.matched": true;
56
+ }>;
57
+ /** `rel.one` relationships that hold a `<relationship>Id` key. */
58
+ type OneKeys<R> = Exclude<KeysWhere<R, OneRelationship>, MatchedKeys<R>>;
59
+ type RequiredOneKeys<R> = Exclude<KeysWhere<R, OneRelationship<false>>, MatchedKeys<R>>;
55
60
  type ManyKeys<R> = KeysWhere<R, ManyRelationship>;
61
+ /** Relationships `find` can include and filter through. */
62
+ type LinkKeys<R> = Exclude<keyof R & string, MatchedKeys<R>>;
56
63
  type UniqueKeys<P> = {
57
64
  [K in keyof P]-?: Unwrapped<P[K]> extends {
58
65
  readonly "~ontology.unique": true;
@@ -61,7 +68,10 @@ type UniqueKeys<P> = {
61
68
  type RelationshipIds<R> = {
62
69
  readonly [K in OneKeys<R> as `${K}Id`]: R[K] extends OneRelationship<true> ? string | null : string;
63
70
  };
64
- /** A record of the entity: system fields, declared properties, and `<relationship>Id` per `rel.one`. */
71
+ /**
72
+ * A record of the entity: system fields, declared properties, and `<relationship>Id` per `rel.one`
73
+ * between stored entities.
74
+ */
65
75
  export type EntityRecord<P, R> = Simplify<SystemFields & {
66
76
  readonly [K in keyof P]: ValueOf<P[K]>;
67
77
  } & RelationshipIds<R>>;
@@ -88,14 +98,19 @@ export type UpdateValues<P, R> = Simplify<{
88
98
  } & {
89
99
  readonly [K in ManyKeys<R>]?: ReadonlyArray<string>;
90
100
  }>;
91
- /** Fields an index may cover: declared properties, system properties and `rel.one` relationships. */
101
+ /**
102
+ * Fields an index may cover: declared properties, system properties and `rel.one` relationships
103
+ * between stored entities.
104
+ */
92
105
  export type IndexField<P, R> = (keyof P & string) | keyof SystemFields | OneKeys<R>;
93
106
  /** An index: the covered fields in order, or the fields with options. */
94
107
  export type IndexDefinition<Field extends string = string> = ReadonlyArray<Field> | {
95
108
  readonly properties: ReadonlyArray<Field>;
96
109
  readonly unique?: boolean;
97
110
  };
98
- export interface EntityDefinition<P extends Properties, R extends Relationships> {
111
+ /** The measures of an entity that declares none. */
112
+ type NoMeasures = Readonly<Record<never, never>>;
113
+ export interface EntityDefinition<P extends Properties, R extends Relationships, M extends Measures = Measures> {
99
114
  /** The stable identity. It never changes; a changed id is a new entity. */
100
115
  readonly id: string;
101
116
  readonly label: string;
@@ -105,7 +120,7 @@ export interface EntityDefinition<P extends Properties, R extends Relationships>
105
120
  readonly description: string;
106
121
  readonly properties: P;
107
122
  readonly source?: WarehouseSource;
108
- readonly measures?: Measures;
123
+ readonly measures?: M;
109
124
  readonly relationships?: R;
110
125
  readonly indexes?: ReadonlyArray<IndexDefinition<IndexField<P, R>>>;
111
126
  /** A Markdown file with process knowledge, relative to the ontology folder. */
@@ -126,23 +141,24 @@ export type NoActions = Readonly<Record<never, never>>;
126
141
  * self-reference error, because the constant's type depends on the callback's result. Other
127
142
  * entities may be named as usual inside handlers. The callback runs once, when `.actions` is called.
128
143
  */
129
- export interface ActionsBuilder<Name extends string, P extends Properties, R extends Relationships> {
130
- <const A extends Actions>(define: (self: Entity<Name, P, R>) => A): Entity<Name, P, R, A>;
144
+ export interface ActionsBuilder<Name extends string, P extends Properties, R extends Relationships, M extends Measures = Measures> {
145
+ <const A extends Actions>(define: (self: Entity<Name, P, R, NoActions, M>) => A): Entity<Name, P, R, A, M>;
131
146
  }
132
147
  /**
133
148
  * An entity as `entity()` returns it: no actions yet, and `actions` is the builder that adds them.
134
149
  * It has no keys, so it reads as an empty set of actions.
135
150
  */
136
- export type DraftEntity<Name extends string, P extends Properties, R extends Relationships> = Entity<Name, P, R, ActionsBuilder<Name, P, R>>;
151
+ export type DraftEntity<Name extends string, P extends Properties, R extends Relationships, M extends Measures = Measures> = Entity<Name, P, R, ActionsBuilder<Name, P, R, M>, M>;
137
152
  /**
138
153
  * An entity: its definition, the record type (`typeof Lead.Type`), a reference schema for action
139
- * inputs (`Lead.ref`), its actions, and data access for action handlers.
154
+ * inputs (`Lead.ref`), its actions, and data access for action handlers. `M` is the type of its
155
+ * declared measures, which typed aggregate clients read from `definition.measures`.
140
156
  */
141
- export interface Entity<Name extends string = string, P extends Properties = Properties, R extends Relationships = Relationships, A extends object = NoActions> {
157
+ export interface Entity<Name extends string = string, P extends Properties = Properties, R extends Relationships = Relationships, A extends object = NoActions, M extends Measures = Measures> {
142
158
  readonly [EntityTypeId]: typeof EntityTypeId;
143
159
  /** The API name and the table name. */
144
160
  readonly name: Name;
145
- readonly definition: EntityDefinition<P, R>;
161
+ readonly definition: EntityDefinition<P, R, M>;
146
162
  readonly properties: P;
147
163
  readonly relationships: R;
148
164
  /** The authored actions by name, as declared with `.actions(…)`. */
@@ -152,7 +168,7 @@ export interface Entity<Name extends string = string, P extends Properties = Pro
152
168
  /** A reference to one of this entity's records, by id, for action inputs. */
153
169
  readonly ref: Schema.Codec<string>;
154
170
  get(record: RecordRef, options?: GetOptions): Effect.Effect<EntityRecord<P, R>, NotFound, OntologyReads>;
155
- find(options?: FindOptions<EntityRecord<P, R>, keyof R & string>): Effect.Effect<ReadonlyArray<EntityRecord<P, R>>, never, OntologyReads>;
171
+ find(options?: FindOptions<EntityRecord<P, R>, LinkKeys<R>>): Effect.Effect<ReadonlyArray<EntityRecord<P, R>>, never, OntologyReads>;
156
172
  create(values: CreateValues<P, R>): Effect.Effect<EntityRecord<P, R>, Conflict | NotFound, OntologyWrites>;
157
173
  update(record: RecordRef, values: UpdateValues<P, R>): Effect.Effect<EntityRecord<P, R>, Conflict | NotFound, OntologyWrites>;
158
174
  upsertBy(property: UniqueKeys<P>, values: CreateValues<P, R>): Effect.Effect<UpsertResult<EntityRecord<P, R>>, Conflict | NotFound, OntologyWrites>;
@@ -183,10 +199,10 @@ interface WarehouseBackedEntity<Name extends string, P extends Properties, R ext
183
199
  * The data access methods delegate to {@link OntologyReads} and {@link OntologyWrites}, which the
184
200
  * ontology service provides to actions.
185
201
  */
186
- export declare function entity<const Name extends string, const P extends Properties, const R extends Relationships = NoRelationships, const M extends Measures = Readonly<Record<never, never>>>(name: Name, definition: EntityDefinition<P, R> & {
202
+ export declare function entity<const Name extends string, const P extends Properties, const R extends Relationships = NoRelationships, const M extends Measures = NoMeasures>(name: Name, definition: EntityDefinition<P, R> & {
187
203
  readonly source: WarehouseSource;
188
204
  readonly measures?: M;
189
205
  }): WarehouseBackedEntity<Name, P, R, M>;
190
- export declare function entity<const Name extends string, const P extends Properties, const R extends Relationships = NoRelationships>(name: Name, definition: EntityDefinition<P, R>): DraftEntity<Name, P, R>;
206
+ export declare function entity<const Name extends string, const P extends Properties, const R extends Relationships = NoRelationships, const M extends Measures = NoMeasures>(name: Name, definition: EntityDefinition<P, R, M>): DraftEntity<Name, P, R, M>;
191
207
  export declare function isEntity(value: unknown): value is AnyEntity;
192
208
  export {};
package/dist/index.d.ts CHANGED
@@ -14,7 +14,7 @@ export { entity, isEntity } from "./entity.js";
14
14
  export type { ActionsBuilder, AnyEntity, AnyStoredEntity, CreateValues, DraftEntity, Entity, EntityDefinition, EntityRecord, IndexDefinition, IndexField, NoActions, Properties, RecordRef, Relationships, SystemFields, UpdateValues, } from "./entity.js";
15
15
  export { ActionRejected, Conflict, NotFound, PermissionDenied } from "./errors.js";
16
16
  export type { ActionError } from "./errors.js";
17
- export { ActionManifest, actionConnectionKeys, canonicalJson, ConnectionManifest, EntityManifest, EntityPermissionsManifest, decodeManifestDocument, encodeManifest, formatManifest, IndexManifest, MANIFEST_VERSION, manifestHash, OntologyManifest, OntologyManifestWire, PropertyManifest, RelationshipManifest, RoleManifest, SelectOptionManifest, SignInManifest, signInPolicyOf, } from "./manifest.js";
17
+ export { ActionManifest, actionConnectionKeys, canonicalJson, ConnectionManifest, EntityManifest, EntityPermissionsManifest, decodeManifestDocument, encodeManifest, formatManifest, IndexManifest, isValueMatched, MANIFEST_VERSION, manifestHash, OntologyManifest, OntologyManifestWire, PropertyManifest, RelationshipManifest, RoleManifest, SelectOptionManifest, SignInManifest, signInPolicyOf, } from "./manifest.js";
18
18
  export { SYSTEM_PROPERTIES } from "./names.js";
19
19
  export type { SystemProperty } from "./names.js";
20
20
  export { check, defineOntology } from "./ontology.js";
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ export { CONNECTION_ERROR_REASONS, CONNECTION_KEY, CONNECTION_METHODS, connectio
6
6
  export { Diagnostic, DIAGNOSTIC_CODES, formatDiagnostics, InvalidOntology } from "./diagnostics.js";
7
7
  export { entity, isEntity } from "./entity.js";
8
8
  export { ActionRejected, Conflict, NotFound, PermissionDenied } from "./errors.js";
9
- export { ActionManifest, actionConnectionKeys, canonicalJson, ConnectionManifest, EntityManifest, EntityPermissionsManifest, decodeManifestDocument, encodeManifest, formatManifest, IndexManifest, MANIFEST_VERSION, manifestHash, OntologyManifest, OntologyManifestWire, PropertyManifest, RelationshipManifest, RoleManifest, SelectOptionManifest, SignInManifest, signInPolicyOf, } from "./manifest.js";
9
+ export { ActionManifest, actionConnectionKeys, canonicalJson, ConnectionManifest, EntityManifest, EntityPermissionsManifest, decodeManifestDocument, encodeManifest, formatManifest, IndexManifest, isValueMatched, MANIFEST_VERSION, manifestHash, OntologyManifest, OntologyManifestWire, PropertyManifest, RelationshipManifest, RoleManifest, SelectOptionManifest, SignInManifest, signInPolicyOf, } from "./manifest.js";
10
10
  export { SYSTEM_PROPERTIES } from "./names.js";
11
11
  export { check, defineOntology } from "./ontology.js";
12
12
  export { DEFAULT_MEMBER_PERMISSIONS, generatedActionPermissions, grants, isAllowed, parsePermission, permissionsOf, READ_VERB, rolesGranting, WILDCARD, } from "./permissions.js";
@@ -7,9 +7,14 @@ import { type SignInOptions, type SignInPolicy } from "./sign-in.js";
7
7
  * 2 replaced the actions' `allow` with `permission`, and added the roles' `permissions` and the
8
8
  * entities' `permissions`; those fields are optional when decoding, so version 1 manifests still read.
9
9
  * `signIn` came later within version 2 and is optional too, as are the actions' `connections` and the
10
- * top-level `connections`, which are written only when an action calls a connection.
10
+ * top-level `connections`, which are written only when an action calls a connection. Version 3 added
11
+ * warehouse sources and measures. Version 4 writes snake_case names on the wire. Version 5 lets
12
+ * relationships target warehouse-backed entities and warehouse-backed properties declare `unique`:
13
+ * a relationship with a warehouse-backed end carries `on` and omits `optional` and `onDelete`.
14
+ * Version 6 adds the `countDistinct` measure operator and lets stored entities declare `measures`,
15
+ * whose columns name the stored properties' replication view columns.
11
16
  */
12
- export declare const MANIFEST_VERSION = 4;
17
+ export declare const MANIFEST_VERSION = 6;
13
18
  /** The phrase used for a direction the author did not phrase. */
14
19
  export declare const DEFAULT_PHRASE = "has";
15
20
  export declare const SelectOptionManifest: Schema.Struct<{
@@ -47,6 +52,10 @@ export type PropertyManifest = typeof PropertyManifest.Type;
47
52
  * from `to`, `inverse` and the second `phrase`.
48
53
  */
49
54
  export declare const RelationshipManifest: Schema.Struct<{
55
+ /**
56
+ * The property pair of a value-matched relationship, one with a warehouse-backed end. Absent
57
+ * between stored entities, which use a `<name>_id` key instead.
58
+ */
50
59
  readonly on: Schema.optionalKey<Schema.Struct<{
51
60
  readonly property: Schema.String;
52
61
  readonly targetProperty: Schema.String;
@@ -55,9 +64,12 @@ export declare const RelationshipManifest: Schema.Struct<{
55
64
  readonly to: Schema.String;
56
65
  readonly inverse: Schema.String;
57
66
  readonly cardinality: Schema.Literals<readonly ["one", "many"]>;
58
- /** `rel.one` only. */
67
+ /** `rel.one` between stored entities only. */
59
68
  readonly optional: Schema.optionalKey<Schema.Boolean>;
60
- /** `rel.one` only. */
69
+ /**
70
+ * `rel.one` between stored entities only. Before version 5, warehouse → stored relationships
71
+ * carried it too, as `restrict`, though nothing enforced it.
72
+ */
61
73
  readonly onDelete: Schema.optionalKey<Schema.Literals<readonly ["setNull", "cascade", "restrict"]>>;
62
74
  /** `rel.many` only. */
63
75
  readonly ordered: Schema.optionalKey<Schema.Boolean>;
@@ -66,6 +78,14 @@ export declare const RelationshipManifest: Schema.Struct<{
66
78
  readonly description: Schema.String;
67
79
  }>;
68
80
  export type RelationshipManifest = typeof RelationshipManifest.Type;
81
+ /**
82
+ * Whether a relationship is matched by value: either end is warehouse-backed. A value-matched
83
+ * relationship always declares `on`; a stored → stored relationship never does. It has no
84
+ * `<name>_id` column, foreign key, index or traversal route.
85
+ */
86
+ export declare function isValueMatched(relationship: {
87
+ readonly on?: unknown;
88
+ }): boolean;
69
89
  export declare const IndexManifest: Schema.Struct<{
70
90
  readonly properties: Schema.$Array<Schema.String>;
71
91
  readonly unique: Schema.Boolean;
@@ -157,6 +177,10 @@ export declare const EntityManifest: Schema.Struct<{
157
177
  }>>>;
158
178
  }>>;
159
179
  readonly relationships: Schema.$Array<Schema.Struct<{
180
+ /**
181
+ * The property pair of a value-matched relationship, one with a warehouse-backed end. Absent
182
+ * between stored entities, which use a `<name>_id` key instead.
183
+ */
160
184
  readonly on: Schema.optionalKey<Schema.Struct<{
161
185
  readonly property: Schema.String;
162
186
  readonly targetProperty: Schema.String;
@@ -165,9 +189,12 @@ export declare const EntityManifest: Schema.Struct<{
165
189
  readonly to: Schema.String;
166
190
  readonly inverse: Schema.String;
167
191
  readonly cardinality: Schema.Literals<readonly ["one", "many"]>;
168
- /** `rel.one` only. */
192
+ /** `rel.one` between stored entities only. */
169
193
  readonly optional: Schema.optionalKey<Schema.Boolean>;
170
- /** `rel.one` only. */
194
+ /**
195
+ * `rel.one` between stored entities only. Before version 5, warehouse → stored relationships
196
+ * carried it too, as `restrict`, though nothing enforced it.
197
+ */
171
198
  readonly onDelete: Schema.optionalKey<Schema.Literals<readonly ["setNull", "cascade", "restrict"]>>;
172
199
  /** `rel.many` only. */
173
200
  readonly ordered: Schema.optionalKey<Schema.Boolean>;
@@ -240,7 +267,7 @@ export declare const SignInManifest: Schema.Struct<{
240
267
  readonly policy: Schema.String;
241
268
  }>;
242
269
  export type SignInManifest = typeof SignInManifest.Type;
243
- /** The JSON contract written by version 4; decoded SDK models remain camelCase. */
270
+ /** The JSON contract written since version 4; decoded SDK models remain camelCase. */
244
271
  export declare const OntologyManifestWire: Schema.encodeKeys<Schema.Struct<{
245
272
  readonly entities: Schema.$Array<Schema.encodeKeys<Schema.Struct<{
246
273
  readonly properties: Schema.$Array<Schema.encodeKeys<Schema.Struct<{
@@ -283,7 +310,7 @@ export declare const OntologyManifestWire: Schema.encodeKeys<Schema.Struct<{
283
310
  readonly to: Schema.String;
284
311
  readonly inverse: Schema.String;
285
312
  readonly cardinality: Schema.Literals<readonly ["one", "many"]>;
286
- /** `rel.one` only. */
313
+ /** `rel.one` between stored entities only. */
287
314
  readonly optional: Schema.optionalKey<Schema.Boolean>;
288
315
  /** `rel.many` only. */
289
316
  readonly ordered: Schema.optionalKey<Schema.Boolean>;
@@ -426,7 +453,7 @@ export declare const OntologyManifest: Schema.Union<readonly [Schema.encodeKeys<
426
453
  readonly to: Schema.String;
427
454
  readonly inverse: Schema.String;
428
455
  readonly cardinality: Schema.Literals<readonly ["one", "many"]>;
429
- /** `rel.one` only. */
456
+ /** `rel.one` between stored entities only. */
430
457
  readonly optional: Schema.optionalKey<Schema.Boolean>;
431
458
  /** `rel.many` only. */
432
459
  readonly ordered: Schema.optionalKey<Schema.Boolean>;
@@ -577,6 +604,10 @@ export declare const OntologyManifest: Schema.Union<readonly [Schema.encodeKeys<
577
604
  }>>>;
578
605
  }>>;
579
606
  readonly relationships: Schema.$Array<Schema.Struct<{
607
+ /**
608
+ * The property pair of a value-matched relationship, one with a warehouse-backed end. Absent
609
+ * between stored entities, which use a `<name>_id` key instead.
610
+ */
580
611
  readonly on: Schema.optionalKey<Schema.Struct<{
581
612
  readonly property: Schema.String;
582
613
  readonly targetProperty: Schema.String;
@@ -585,9 +616,12 @@ export declare const OntologyManifest: Schema.Union<readonly [Schema.encodeKeys<
585
616
  readonly to: Schema.String;
586
617
  readonly inverse: Schema.String;
587
618
  readonly cardinality: Schema.Literals<readonly ["one", "many"]>;
588
- /** `rel.one` only. */
619
+ /** `rel.one` between stored entities only. */
589
620
  readonly optional: Schema.optionalKey<Schema.Boolean>;
590
- /** `rel.one` only. */
621
+ /**
622
+ * `rel.one` between stored entities only. Before version 5, warehouse → stored relationships
623
+ * carried it too, as `restrict`, though nothing enforced it.
624
+ */
591
625
  readonly onDelete: Schema.optionalKey<Schema.Literals<readonly ["setNull", "cascade", "restrict"]>>;
592
626
  /** `rel.many` only. */
593
627
  readonly ordered: Schema.optionalKey<Schema.Boolean>;
@@ -661,6 +695,7 @@ export declare const decodeManifestDocument: (value: unknown) => Effect.Effect<{
661
695
  readonly id: string;
662
696
  readonly description: string;
663
697
  readonly label: string;
698
+ readonly title: string;
664
699
  readonly actions: readonly {
665
700
  readonly name: string;
666
701
  readonly description: string;
@@ -674,18 +709,15 @@ export declare const decodeManifestDocument: (value: unknown) => Effect.Effect<{
674
709
  readonly idempotent?: boolean | undefined;
675
710
  } | undefined;
676
711
  }[];
677
- readonly title: string;
678
712
  readonly properties: readonly {
679
713
  readonly optional: boolean;
680
714
  readonly name: string;
681
715
  readonly label: string;
682
- readonly type: "number" | "boolean" | "text" | "integer" | "date" | "timestamp" | "select" | "email" | "url" | "json" | "blob";
716
+ readonly type: "number" | "boolean" | "text" | "email" | "url" | "integer" | "date" | "timestamp" | "select" | "json" | "blob";
683
717
  readonly unique: boolean;
684
718
  readonly managed: boolean;
685
719
  readonly column?: string | undefined;
686
720
  readonly description?: string | undefined;
687
- readonly default?: Schema.Json | undefined;
688
- readonly scope?: readonly string[] | undefined;
689
721
  readonly maxLength?: number | undefined;
690
722
  readonly min?: number | undefined;
691
723
  readonly max?: number | undefined;
@@ -694,6 +726,8 @@ export declare const decodeManifestDocument: (value: unknown) => Effect.Effect<{
694
726
  readonly label: string;
695
727
  readonly description: string;
696
728
  }[] | undefined;
729
+ readonly scope?: readonly string[] | undefined;
730
+ readonly default?: Schema.Json | undefined;
697
731
  readonly renamedFrom?: string | undefined;
698
732
  readonly display?: string | undefined;
699
733
  readonly accept?: readonly string[] | undefined;
@@ -707,11 +741,11 @@ export declare const decodeManifestDocument: (value: unknown) => Effect.Effect<{
707
741
  readonly to: string;
708
742
  readonly inverse: string;
709
743
  readonly optional?: boolean | undefined;
744
+ readonly onDelete?: "setNull" | "cascade" | "restrict" | undefined;
710
745
  readonly on?: {
711
746
  readonly property: string;
712
747
  readonly targetProperty: string;
713
748
  } | undefined;
714
- readonly onDelete?: "setNull" | "cascade" | "restrict" | undefined;
715
749
  readonly ordered?: boolean | undefined;
716
750
  }[];
717
751
  readonly indexes: readonly {
@@ -720,6 +754,7 @@ export declare const decodeManifestDocument: (value: unknown) => Effect.Effect<{
720
754
  }[];
721
755
  readonly plural: string;
722
756
  readonly builtIn: boolean;
757
+ readonly renamedFrom?: string | undefined;
723
758
  readonly source?: {
724
759
  readonly key: readonly string[];
725
760
  readonly asset: string;
@@ -733,7 +768,6 @@ export declare const decodeManifestDocument: (value: unknown) => Effect.Effect<{
733
768
  readonly format: "currency" | "percent" | "decimal";
734
769
  readonly expression: Schema.Json;
735
770
  }[] | undefined;
736
- readonly renamedFrom?: string | undefined;
737
771
  readonly guide?: string | undefined;
738
772
  readonly permissions?: {
739
773
  readonly read: string;
@@ -765,6 +799,7 @@ export declare const decodeManifestDocument: (value: unknown) => Effect.Effect<{
765
799
  readonly id: string;
766
800
  readonly description: string;
767
801
  readonly label: string;
802
+ readonly title: string;
768
803
  readonly actions: readonly {
769
804
  readonly name: string;
770
805
  readonly description: string;
@@ -778,18 +813,15 @@ export declare const decodeManifestDocument: (value: unknown) => Effect.Effect<{
778
813
  readonly idempotent?: boolean | undefined;
779
814
  } | undefined;
780
815
  }[];
781
- readonly title: string;
782
816
  readonly properties: readonly {
783
817
  readonly optional: boolean;
784
818
  readonly name: string;
785
819
  readonly label: string;
786
- readonly type: "number" | "boolean" | "text" | "integer" | "date" | "timestamp" | "select" | "email" | "url" | "json" | "blob";
820
+ readonly type: "number" | "boolean" | "text" | "email" | "url" | "integer" | "date" | "timestamp" | "select" | "json" | "blob";
787
821
  readonly unique: boolean;
788
822
  readonly managed: boolean;
789
823
  readonly column?: string | undefined;
790
824
  readonly description?: string | undefined;
791
- readonly default?: Schema.Json | undefined;
792
- readonly scope?: readonly string[] | undefined;
793
825
  readonly maxLength?: number | undefined;
794
826
  readonly min?: number | undefined;
795
827
  readonly max?: number | undefined;
@@ -798,6 +830,8 @@ export declare const decodeManifestDocument: (value: unknown) => Effect.Effect<{
798
830
  readonly label: string;
799
831
  readonly description: string;
800
832
  }[] | undefined;
833
+ readonly scope?: readonly string[] | undefined;
834
+ readonly default?: Schema.Json | undefined;
801
835
  readonly renamedFrom?: string | undefined;
802
836
  readonly display?: string | undefined;
803
837
  readonly accept?: readonly string[] | undefined;
@@ -811,11 +845,11 @@ export declare const decodeManifestDocument: (value: unknown) => Effect.Effect<{
811
845
  readonly to: string;
812
846
  readonly inverse: string;
813
847
  readonly optional?: boolean | undefined;
848
+ readonly onDelete?: "setNull" | "cascade" | "restrict" | undefined;
814
849
  readonly on?: {
815
850
  readonly property: string;
816
851
  readonly targetProperty: string;
817
852
  } | undefined;
818
- readonly onDelete?: "setNull" | "cascade" | "restrict" | undefined;
819
853
  readonly ordered?: boolean | undefined;
820
854
  }[];
821
855
  readonly indexes: readonly {
@@ -824,6 +858,7 @@ export declare const decodeManifestDocument: (value: unknown) => Effect.Effect<{
824
858
  }[];
825
859
  readonly plural: string;
826
860
  readonly builtIn: boolean;
861
+ readonly renamedFrom?: string | undefined;
827
862
  readonly source?: {
828
863
  readonly key: readonly string[];
829
864
  readonly asset: string;
@@ -837,7 +872,6 @@ export declare const decodeManifestDocument: (value: unknown) => Effect.Effect<{
837
872
  readonly format: "currency" | "percent" | "decimal";
838
873
  readonly expression: Schema.Json;
839
874
  }[] | undefined;
840
- readonly renamedFrom?: string | undefined;
841
875
  readonly guide?: string | undefined;
842
876
  readonly permissions?: {
843
877
  readonly read: string;