@segmentstream-cloud/ontology 0.6.0 → 0.8.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 (53) hide show
  1. package/README.md +14 -2
  2. package/dist/analyze.js +58 -17
  3. package/dist/authoring-input.d.ts +3 -0
  4. package/dist/authoring-input.js +107 -0
  5. package/dist/authoring-names.d.ts +4 -0
  6. package/dist/authoring-names.js +13 -0
  7. package/dist/authoring-records.d.ts +16 -0
  8. package/dist/authoring-records.js +144 -0
  9. package/dist/authoring.d.ts +12 -0
  10. package/dist/authoring.js +237 -0
  11. package/dist/connection.js +1 -1
  12. package/dist/entity.d.ts +1 -2
  13. package/dist/entity.js +31 -8
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.js +1 -1
  16. package/dist/manifest.d.ts +468 -10
  17. package/dist/manifest.js +46 -6
  18. package/dist/migrate/apply.d.ts +1 -1
  19. package/dist/migrate/apply.js +5 -4
  20. package/dist/migrate/model.d.ts +112 -2
  21. package/dist/migrate/plan.js +11 -9
  22. package/dist/migrate/storage.d.ts +6 -6
  23. package/dist/migrate/storage.js +25 -15
  24. package/dist/names.d.ts +11 -11
  25. package/dist/names.js +36 -13
  26. package/dist/permissions.d.ts +2 -2
  27. package/dist/permissions.js +8 -8
  28. package/dist/property.js +2 -1
  29. package/dist/query.d.ts +6 -6
  30. package/dist/query.js +41 -30
  31. package/dist/runtime/cursor.js +1 -1
  32. package/dist/runtime/generated-actions.d.ts +4 -4
  33. package/dist/runtime/generated-actions.js +5 -9
  34. package/dist/runtime/history.js +1 -1
  35. package/dist/runtime/model.d.ts +2 -2
  36. package/dist/runtime/model.js +1 -1
  37. package/dist/runtime/principals.js +16 -13
  38. package/dist/runtime/reads.js +1 -1
  39. package/dist/runtime/records.js +1 -1
  40. package/dist/runtime/sql-runtime.js +1 -1
  41. package/dist/service/config.d.ts +1 -1
  42. package/dist/service/errors.d.ts +1 -0
  43. package/dist/service/errors.js +5 -1
  44. package/dist/service/handlers.js +27 -18
  45. package/dist/service/runtime-contract.d.ts +1 -1
  46. package/dist/service/schemas.d.ts +201 -26
  47. package/dist/service/schemas.js +30 -21
  48. package/dist/service/service.js +11 -4
  49. package/dist/user.js +3 -0
  50. package/dist/warehouse-backed.js +3 -3
  51. package/dist/wire.d.ts +5 -0
  52. package/dist/wire.js +21 -0
  53. package/package.json +1 -1
@@ -0,0 +1,237 @@
1
+ import { Schema } from "effect";
2
+ import { isAction } from "./action.js";
3
+ import { actionInputToWire } from "./authoring-input.js";
4
+ import { isRecord, nameToWire, permissionToWire } from "./authoring-names.js";
5
+ import { isEntity } from "./entity.js";
6
+ import { toCamelCase } from "./names.js";
7
+ import { p, propertyDefinition } from "./property.js";
8
+ import { isRelationship } from "./relationship.js";
9
+ import { isRole, member } from "./role.js";
10
+ export function principalFromWire(principal) {
11
+ return {
12
+ ...principal,
13
+ roles: principal.roles.map(toCamelCase),
14
+ permissions: principal.permissions.map(toCamelCase),
15
+ };
16
+ }
17
+ function indexToWire(index) {
18
+ if (Array.isArray(index)) {
19
+ return index.map(nameToWire);
20
+ }
21
+ const definition = index;
22
+ return { ...definition, properties: (definition.properties ?? []).map(nameToWire) };
23
+ }
24
+ function expressionToWire(value) {
25
+ if (!isRecord(value)) {
26
+ return value;
27
+ }
28
+ const expression = { ...value };
29
+ if (typeof value.name === "string" && (value.op === "measure" || value.op === "column")) {
30
+ expression.name = nameToWire(value.name);
31
+ }
32
+ if (Array.isArray(value.args)) {
33
+ expression.args = value.args.map(expressionToWire);
34
+ }
35
+ return expression;
36
+ }
37
+ /** Preserve shared definitions and lazy relationship targets while building the canonical model. */
38
+ class AuthoringEncoder {
39
+ report;
40
+ entities = new WeakMap();
41
+ definitions = new WeakMap();
42
+ properties = new WeakMap();
43
+ roles = new WeakMap();
44
+ constructor(report) {
45
+ this.report = report;
46
+ }
47
+ name(name, path) {
48
+ const encoded = nameToWire(name);
49
+ if (typeof name === "string" &&
50
+ /^[a-z][a-zA-Z0-9]*$/.test(name) &&
51
+ name !== toCamelCase(encoded)) {
52
+ this.report("invalid-name", path, `Use lower camelCase ${toCamelCase(encoded)} ` +
53
+ "so the TypeScript and wire names round-trip.");
54
+ }
55
+ return encoded;
56
+ }
57
+ named(values, path, convert) {
58
+ const result = Object.create(null);
59
+ for (const [name, value] of Object.entries(values ?? {})) {
60
+ const fieldPath = `${path}.${name}`;
61
+ const wireName = this.name(name, fieldPath);
62
+ if (Object.hasOwn(result, wireName)) {
63
+ this.report("duplicate-name", fieldPath, `Names collide on ${wireName} after snake_case conversion.`);
64
+ }
65
+ result[wireName] = convert(value, fieldPath);
66
+ }
67
+ return result;
68
+ }
69
+ property(value, path) {
70
+ if (!Schema.isSchema(value)) {
71
+ return value;
72
+ }
73
+ const cached = this.properties.get(value);
74
+ if (cached !== undefined) {
75
+ return cached;
76
+ }
77
+ let normalized = value;
78
+ const definition = propertyDefinition(value);
79
+ if (definition?.type === "select") {
80
+ const options = (definition.options ?? []).map((option) => ({
81
+ ...option,
82
+ description: option.description ?? "",
83
+ value: nameToWire(option.value),
84
+ }));
85
+ for (const option of definition.options ?? []) {
86
+ this.name(option.value, `${path}.options`);
87
+ }
88
+ const settings = { ...definition };
89
+ if (typeof settings.default === "string") {
90
+ settings.default = nameToWire(settings.default);
91
+ }
92
+ normalized = p.select(options, settings);
93
+ }
94
+ this.properties.set(value, normalized);
95
+ return normalized;
96
+ }
97
+ target(value) {
98
+ if (typeof value === "string") {
99
+ return nameToWire(value);
100
+ }
101
+ if (typeof value === "function") {
102
+ return () => this.entity(value());
103
+ }
104
+ return this.entity(value);
105
+ }
106
+ relationship(value) {
107
+ if (!isRelationship(value)) {
108
+ return value;
109
+ }
110
+ const normalized = {
111
+ ...value,
112
+ inverse: nameToWire(value.inverse),
113
+ target: this.target(value.target),
114
+ };
115
+ if ("on" in value && value.on !== undefined) {
116
+ return {
117
+ ...normalized,
118
+ on: Object.fromEntries(Object.entries(value.on).map(([property, target]) => [
119
+ nameToWire(property),
120
+ nameToWire(target),
121
+ ])),
122
+ };
123
+ }
124
+ return normalized;
125
+ }
126
+ definition(entity) {
127
+ const original = entity.definition;
128
+ const cached = this.definitions.get(original);
129
+ if (cached !== undefined) {
130
+ return cached;
131
+ }
132
+ const normalized = {
133
+ ...original,
134
+ title: nameToWire(original.title),
135
+ ...(original.source?.currency === undefined
136
+ ? {}
137
+ : { source: { ...original.source, currency: nameToWire(original.source.currency) } }),
138
+ properties: this.named(original.properties, `${entity.name}.properties`, (value, path) => this.property(value, path)),
139
+ relationships: this.named(original.relationships ?? {}, `${entity.name}.relationships`, (value) => this.relationship(value)),
140
+ ...(original.indexes === undefined ? {} : { indexes: original.indexes.map(indexToWire) }),
141
+ ...(original.measures === undefined
142
+ ? {}
143
+ : {
144
+ measures: this.named(original.measures, `${entity.name}.measures`, (value) => expressionToWire(value)),
145
+ }),
146
+ };
147
+ this.definitions.set(original, normalized);
148
+ return normalized;
149
+ }
150
+ action(value, path) {
151
+ if (!isAction(value)) {
152
+ return value;
153
+ }
154
+ let input = value.input;
155
+ try {
156
+ if (Schema.isSchema(input)) {
157
+ input = actionInputToWire(input);
158
+ }
159
+ }
160
+ catch (error) {
161
+ this.report("invalid-action-input", `${path}.input`, error instanceof Error ? error.message : String(error));
162
+ }
163
+ const normalized = {
164
+ ...value,
165
+ input,
166
+ permission: value.permission === undefined ? undefined : permissionToWire(value.permission),
167
+ };
168
+ if (typeof value.handler === "function") {
169
+ normalized.handler = (context) => value.handler({ ...context, principal: principalFromWire(context.principal) });
170
+ }
171
+ if (typeof value.prepare === "function") {
172
+ const prepare = value.prepare;
173
+ normalized.prepare = (context) => prepare({ ...context, principal: principalFromWire(context.principal) });
174
+ }
175
+ return normalized;
176
+ }
177
+ entity(value) {
178
+ if (!isEntity(value)) {
179
+ return value;
180
+ }
181
+ const cached = this.entities.get(value);
182
+ if (cached !== undefined) {
183
+ return cached;
184
+ }
185
+ // Register the handle before resolving relationships, which can point back to it.
186
+ const normalized = { ...value, name: this.name(value.name, value.name) };
187
+ this.entities.set(value, normalized);
188
+ const definition = this.definition(value);
189
+ Object.assign(normalized, {
190
+ definition,
191
+ properties: definition.properties,
192
+ relationships: definition.relationships,
193
+ });
194
+ if (isRecord(value.actions)) {
195
+ Object.assign(normalized, {
196
+ actions: this.named(value.actions, `${value.name}.actions`, (action, path) => this.action(action, path)),
197
+ });
198
+ }
199
+ return normalized;
200
+ }
201
+ role(value) {
202
+ if (!isRole(value) || value === member) {
203
+ return value;
204
+ }
205
+ const cached = this.roles.get(value);
206
+ if (cached !== undefined) {
207
+ return cached;
208
+ }
209
+ const normalized = {
210
+ ...value,
211
+ name: this.name(value.name, `roles.${value.name}`),
212
+ permissions: Array.isArray(value.permissions)
213
+ ? value.permissions.map(permissionToWire)
214
+ : value.permissions,
215
+ };
216
+ this.roles.set(value, normalized);
217
+ return normalized;
218
+ }
219
+ }
220
+ /** Canonical definitions feed REST, storage and future transports; the public config stays camelCase. */
221
+ export function authoringToWire(config, user, report) {
222
+ const encoder = new AuthoringEncoder(report);
223
+ const normalized = {
224
+ ...config,
225
+ entities: (config.entities ?? []).map((entity) => encoder.entity(entity)),
226
+ ...(config.roles === undefined
227
+ ? {}
228
+ : { roles: config.roles.map((role) => encoder.role(role)) }),
229
+ };
230
+ if (isRecord(config.member) && Array.isArray(config.member.permissions)) {
231
+ normalized.member = {
232
+ ...config.member,
233
+ permissions: config.member.permissions.map(permissionToWire),
234
+ };
235
+ }
236
+ return { config: normalized, user: encoder.entity(user) };
237
+ }
@@ -1,7 +1,7 @@
1
1
  import { Context, Effect, Layer, Schema } from "effect";
2
2
  export const ConnectionTypeId = Symbol.for("@segmentstream-cloud/ontology/Connection");
3
3
  /** A connection key, as `connections/connections.py` declares it. */
4
- export const CONNECTION_KEY = /^[a-z][a-z0-9_]{0,63}$/;
4
+ export const CONNECTION_KEY = /^(?=.{1,64}$)[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/;
5
5
  export const CONNECTION_METHODS = [
6
6
  "GET",
7
7
  "POST",
package/dist/entity.d.ts CHANGED
@@ -3,7 +3,6 @@ import type { Measures } from "./measure.js";
3
3
  import type { WarehouseSource } from "./source.js";
4
4
  import type { Actions } from "./action.js";
5
5
  import type { Conflict, NotFound } from "./errors.js";
6
- import type { SystemProperty } from "./names.js";
7
6
  import { type AnyProperty } from "./property.js";
8
7
  import type { AnyRelationship, ManyRelationship, OneRelationship } from "./relationship.js";
9
8
  import { OntologyReads, OntologyWrites, type FindOptions, type GetOptions, type UpsertResult } from "./runtime.js";
@@ -88,7 +87,7 @@ export type UpdateValues<P, R> = Simplify<{
88
87
  readonly [K in ManyKeys<R>]?: ReadonlyArray<string>;
89
88
  }>;
90
89
  /** Fields an index may cover: declared properties, system properties and `rel.one` relationships. */
91
- export type IndexField<P, R> = (keyof P & string) | SystemProperty | OneKeys<R>;
90
+ export type IndexField<P, R> = (keyof P & string) | keyof SystemFields | OneKeys<R>;
92
91
  /** An index: the covered fields in order, or the fields with options. */
93
92
  export type IndexDefinition<Field extends string = string> = ReadonlyArray<Field> | {
94
93
  readonly properties: ReadonlyArray<Field>;
package/dist/entity.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { Effect } from "effect";
2
2
  import { p } from "./property.js";
3
+ import { recordFromWire, valuesToWire, findToWire } from "./authoring-records.js";
4
+ import { toSnakeCase } from "./names.js";
3
5
  import { OntologyReads, OntologyWrites, } from "./runtime.js";
4
6
  export const EntityTypeId = Symbol.for("@segmentstream-cloud/ontology/Entity");
5
7
  function idOf(record) {
@@ -25,17 +27,38 @@ export function entity(name, definition) {
25
27
  relationships: definition.relationships ?? {},
26
28
  ref: p.ref(name),
27
29
  get: (record, options) => withReads((runtime) => runtime.get({
28
- entity: name,
30
+ entity: toSnakeCase(name),
29
31
  id: idOf(record),
30
32
  ...(options?.forUpdate === true ? { forUpdate: true } : {}),
33
+ })).pipe(Effect.map((record) => recordFromWire(record, definition))),
34
+ find: (options) => withReads((runtime) => runtime.find({ entity: toSnakeCase(name), options: findToWire(options ?? {}, definition) })).pipe(Effect.map((records) => records.map((record) => recordFromWire(record, definition)))),
35
+ create: (values) => withWrites((runtime) => runtime.create({ entity: toSnakeCase(name), values: valuesToWire(values, definition) })).pipe(Effect.map((record) => recordFromWire(record, definition))),
36
+ update: (record, values) => withWrites((runtime) => runtime.update({
37
+ entity: toSnakeCase(name),
38
+ id: idOf(record),
39
+ values: valuesToWire(values, definition),
40
+ })).pipe(Effect.map((record) => recordFromWire(record, definition))),
41
+ upsertBy: (property, values) => withWrites((runtime) => runtime.upsertBy({
42
+ entity: toSnakeCase(name),
43
+ property: toSnakeCase(property),
44
+ values: valuesToWire(values, definition),
45
+ })).pipe(Effect.map((result) => ({
46
+ ...result,
47
+ record: recordFromWire(result.record, definition),
48
+ }))),
49
+ delete: (record) => withWrites((runtime) => runtime.delete({ entity: toSnakeCase(name), id: idOf(record) })),
50
+ link: (relationship, record, target) => withWrites((runtime) => runtime.link({
51
+ entity: toSnakeCase(name),
52
+ relationship: toSnakeCase(relationship),
53
+ id: idOf(record),
54
+ targetId: idOf(target),
55
+ })),
56
+ unlink: (relationship, record, target) => withWrites((runtime) => runtime.unlink({
57
+ entity: toSnakeCase(name),
58
+ relationship: toSnakeCase(relationship),
59
+ id: idOf(record),
60
+ targetId: idOf(target),
31
61
  })),
32
- find: (options) => withReads((runtime) => runtime.find({ entity: name, options: options ?? {} })),
33
- create: (values) => withWrites((runtime) => runtime.create({ entity: name, values })),
34
- update: (record, values) => withWrites((runtime) => runtime.update({ entity: name, id: idOf(record), values })),
35
- upsertBy: (property, values) => withWrites((runtime) => runtime.upsertBy({ entity: name, property, values })),
36
- delete: (record) => withWrites((runtime) => runtime.delete({ entity: name, id: idOf(record) })),
37
- link: (relationship, record, target) => withWrites((runtime) => runtime.link({ entity: name, relationship, id: idOf(record), targetId: idOf(target) })),
38
- unlink: (relationship, record, target) => withWrites((runtime) => runtime.unlink({ entity: name, relationship, id: idOf(record), targetId: idOf(target) })),
39
62
  };
40
63
  // Every form of the entity shares `definition`, which is how defineOntology recognises them.
41
64
  const self = { ...handle, actions: {} };
package/dist/index.d.ts CHANGED
@@ -13,7 +13,7 @@ export { entity, isEntity } from "./entity.js";
13
13
  export type { ActionsBuilder, AnyEntity, AnyStoredEntity, CreateValues, DraftEntity, Entity, EntityDefinition, EntityRecord, IndexDefinition, IndexField, NoActions, Properties, RecordRef, Relationships, SystemFields, UpdateValues, } from "./entity.js";
14
14
  export { ActionRejected, Conflict, NotFound, PermissionDenied } from "./errors.js";
15
15
  export type { ActionError } from "./errors.js";
16
- export { ActionManifest, actionConnectionKeys, canonicalJson, ConnectionManifest, EntityManifest, EntityPermissionsManifest, formatManifest, IndexManifest, MANIFEST_VERSION, manifestHash, OntologyManifest, PropertyManifest, RelationshipManifest, RoleManifest, SelectOptionManifest, SignInManifest, signInPolicyOf, } from "./manifest.js";
16
+ 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
17
  export { SYSTEM_PROPERTIES } from "./names.js";
18
18
  export type { SystemProperty } from "./names.js";
19
19
  export { check, defineOntology } from "./ontology.js";
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ export { CONNECTION_ERROR_REASONS, CONNECTION_KEY, CONNECTION_METHODS, connectio
5
5
  export { Diagnostic, DIAGNOSTIC_CODES, formatDiagnostics, InvalidOntology } from "./diagnostics.js";
6
6
  export { entity, isEntity } from "./entity.js";
7
7
  export { ActionRejected, Conflict, NotFound, PermissionDenied } from "./errors.js";
8
- export { ActionManifest, actionConnectionKeys, canonicalJson, ConnectionManifest, EntityManifest, EntityPermissionsManifest, formatManifest, IndexManifest, MANIFEST_VERSION, manifestHash, OntologyManifest, PropertyManifest, RelationshipManifest, RoleManifest, SelectOptionManifest, SignInManifest, signInPolicyOf, } from "./manifest.js";
8
+ 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
9
  export { SYSTEM_PROPERTIES } from "./names.js";
10
10
  export { check, defineOntology } from "./ontology.js";
11
11
  export { DEFAULT_MEMBER_PERMISSIONS, generatedActionPermissions, grants, isAllowed, parsePermission, permissionsOf, READ_VERB, rolesGranting, WILDCARD, } from "./permissions.js";