@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.
- package/README.md +14 -2
- package/dist/analyze.js +58 -17
- package/dist/authoring-input.d.ts +3 -0
- package/dist/authoring-input.js +107 -0
- package/dist/authoring-names.d.ts +4 -0
- package/dist/authoring-names.js +13 -0
- package/dist/authoring-records.d.ts +16 -0
- package/dist/authoring-records.js +144 -0
- package/dist/authoring.d.ts +12 -0
- package/dist/authoring.js +237 -0
- package/dist/connection.js +1 -1
- package/dist/entity.d.ts +1 -2
- package/dist/entity.js +31 -8
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/manifest.d.ts +468 -10
- package/dist/manifest.js +46 -6
- package/dist/migrate/apply.d.ts +1 -1
- package/dist/migrate/apply.js +5 -4
- package/dist/migrate/model.d.ts +112 -2
- package/dist/migrate/plan.js +11 -9
- package/dist/migrate/storage.d.ts +6 -6
- package/dist/migrate/storage.js +25 -15
- package/dist/names.d.ts +11 -11
- package/dist/names.js +36 -13
- package/dist/permissions.d.ts +2 -2
- package/dist/permissions.js +8 -8
- package/dist/property.js +2 -1
- package/dist/query.d.ts +6 -6
- package/dist/query.js +41 -30
- package/dist/runtime/cursor.js +1 -1
- package/dist/runtime/generated-actions.d.ts +4 -4
- package/dist/runtime/generated-actions.js +5 -9
- package/dist/runtime/history.js +1 -1
- package/dist/runtime/model.d.ts +2 -2
- package/dist/runtime/model.js +1 -1
- package/dist/runtime/principals.js +16 -13
- package/dist/runtime/reads.js +1 -1
- package/dist/runtime/records.js +1 -1
- package/dist/runtime/sql-runtime.js +1 -1
- package/dist/service/config.d.ts +1 -1
- package/dist/service/errors.d.ts +1 -0
- package/dist/service/errors.js +5 -1
- package/dist/service/handlers.js +27 -18
- package/dist/service/runtime-contract.d.ts +1 -1
- package/dist/service/schemas.d.ts +201 -26
- package/dist/service/schemas.js +30 -21
- package/dist/service/service.js +11 -4
- package/dist/user.js +3 -0
- package/dist/warehouse-backed.js +3 -3
- package/dist/wire.d.ts +5 -0
- package/dist/wire.js +21 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,6 +8,14 @@ database and the HTTP service under `/api/ontology`. Every read and every action
|
|
|
8
8
|
a permission that the caller's roles grant, and every change to a record goes through an
|
|
9
9
|
action and is kept in history.
|
|
10
10
|
|
|
11
|
+
TypeScript definitions, handler records and SDK calls use camelCase: `jobTitle`,
|
|
12
|
+
`assignedOwnerId`, `createdAt`, and `changeStatus`. The SDK converts ontology names,
|
|
13
|
+
select identifiers and declared action input fields to snake_case for the generated
|
|
14
|
+
REST API, version 4 manifest and storage. TypeScript helpers and options stay
|
|
15
|
+
camelCase. JSON property contents, provider payloads, opaque action results and
|
|
16
|
+
historical change names remain data and are preserved. `formatManifest` serializes
|
|
17
|
+
the canonical wire contract shared by REST, storage and future transports.
|
|
18
|
+
|
|
11
19
|
## Install
|
|
12
20
|
|
|
13
21
|
```sh
|
|
@@ -121,7 +129,7 @@ entity, the verb or both: `lead:*`, `*:read`, `*:*`.
|
|
|
121
129
|
- Reads require `<entity>:read`: lists, queries, records, history, and both entities of a
|
|
122
130
|
relationship traversal. `include` and a filter through a relationship require `read` on
|
|
123
131
|
the related entity. A missing permission answers 403 with `<permission> is required.`
|
|
124
|
-
- Each action requires one permission. The generated `
|
|
132
|
+
- Each action requires one permission. The generated `upsert_by_<property>` requires both
|
|
125
133
|
`<entity>:create` and `<entity>:update`.
|
|
126
134
|
- A principal's permissions are the union of its roles' permissions in the ontology.
|
|
127
135
|
Members hold `member` and their assigned roles; an API key holds exactly the roles it was
|
|
@@ -182,7 +190,7 @@ Sign-in finds the user linked to the token's provider and subject, else links th
|
|
|
182
190
|
invited with the same email, else follows the ontology's `signIn` policy: `invited` answers
|
|
183
191
|
403 `{ "error": "not_invited", "detail": "Ask your administrator for an invitation." }`,
|
|
184
192
|
`open` creates a member. Deactivated members are refused. Each sign-in sets the user's
|
|
185
|
-
`
|
|
193
|
+
`last_sign_in_at`, at most once per 15 minutes, so the user's history records it.
|
|
186
194
|
|
|
187
195
|
`GET /api/ontology/me` answers who the caller is, for the app to render what they may do:
|
|
188
196
|
`{ user: { id, name, email, kind, active }, roles, permissions }`. It needs only
|
|
@@ -234,3 +242,7 @@ usage error.
|
|
|
234
242
|
## License
|
|
235
243
|
|
|
236
244
|
Apache-2.0
|
|
245
|
+
|
|
246
|
+
## Entity sources and aggregates
|
|
247
|
+
|
|
248
|
+
Entities with a warehouse source declare mapped properties and measures without storage tables or record operations. See [ontology aggregates](../../docs/ontology-aggregates.md) for declaration rules, source verification, workspace authorization and the overlap release.
|
package/dist/analyze.js
CHANGED
|
@@ -3,14 +3,16 @@ import { Schema } from "effect";
|
|
|
3
3
|
import { isAction } from "./action.js";
|
|
4
4
|
import { CONNECTION_KEY, isConnection } from "./connection.js";
|
|
5
5
|
import { isEntity } from "./entity.js";
|
|
6
|
-
import {
|
|
6
|
+
import { LEGACY_MEMBER_NAME, ENTITY_ID, GENERATED_ACTIONS, GENERATED_UPSERT_PREFIX, HISTORY_SUFFIX, MEMBER_ROLE, RESERVED_ENTITY_NAMES, RESERVED_MEMBER_NAMES, RESERVED_RELATIONSHIP_NAMES, SNAKE_CASE_NAME, SYSTEM_PROPERTIES, isSystemProperty, } from "./names.js";
|
|
7
7
|
import { REFERENCE_ANNOTATION, propertyDefinition, referenceTarget, } from "./property.js";
|
|
8
8
|
import { generatedActionPermissions, parsePermission, permissionOf, READ_VERB, rolesGranting, WILDCARD, } from "./permissions.js";
|
|
9
9
|
import { ON_DELETE, isRelationship } from "./relationship.js";
|
|
10
10
|
import { isRole, member, role } from "./role.js";
|
|
11
11
|
import { DEFAULT_SIGN_IN_POLICY, isSignInPolicy } from "./sign-in.js";
|
|
12
12
|
import { User } from "./user.js";
|
|
13
|
-
|
|
13
|
+
import { authoringToWire } from "./authoring.js";
|
|
14
|
+
import { nameToWire } from "./authoring-names.js";
|
|
15
|
+
const GENERATED_UPSERT = new RegExp(`^${GENERATED_UPSERT_PREFIX}`);
|
|
14
16
|
function errorMessage(error) {
|
|
15
17
|
return error instanceof Error ? error.message : String(error);
|
|
16
18
|
}
|
|
@@ -29,7 +31,10 @@ export function analyze(config, options = {}) {
|
|
|
29
31
|
const report = (code, path, message, severity = "error") => {
|
|
30
32
|
diagnostics.push({ severity, code, path, message });
|
|
31
33
|
};
|
|
32
|
-
const
|
|
34
|
+
const original = config;
|
|
35
|
+
const normalized = authoringToWire(config, options.user ?? User, report);
|
|
36
|
+
config = normalized.config;
|
|
37
|
+
const user = normalized.user;
|
|
33
38
|
const entities = collectEntities(config, user, report);
|
|
34
39
|
const roles = collectRoles(config, report);
|
|
35
40
|
const signIn = signInOptions(config, report);
|
|
@@ -45,7 +50,7 @@ export function analyze(config, options = {}) {
|
|
|
45
50
|
user,
|
|
46
51
|
entitiesByName,
|
|
47
52
|
definitions: new Set(entities.map((candidate) => candidate.definition)),
|
|
48
|
-
entityProperties: new Set(entities.flatMap((candidate) => Object.values(candidate.definition.properties ?? {}))),
|
|
53
|
+
entityProperties: new Set([...entities, ...(original.entities ?? []).filter(isEntity)].flatMap((candidate) => Object.values(candidate.definition.properties ?? {}))),
|
|
49
54
|
connections: new Set(connections.map((candidate) => candidate.key)),
|
|
50
55
|
};
|
|
51
56
|
const resolved = entities.map((candidate) => resolveEntity(candidate, scope));
|
|
@@ -293,8 +298,8 @@ function entityPermissions(name, builtIn, relationships) {
|
|
|
293
298
|
};
|
|
294
299
|
}
|
|
295
300
|
function checkMemberName(kind, path, name, report) {
|
|
296
|
-
if (!
|
|
297
|
-
report("invalid-name", path, `${kind} names are
|
|
301
|
+
if (!SNAKE_CASE_NAME.test(name)) {
|
|
302
|
+
report("invalid-name", path, `${kind} names are lowercase snake_case, for example owned_leads.`);
|
|
298
303
|
}
|
|
299
304
|
else if (isSystemProperty(name)) {
|
|
300
305
|
report("reserved-name", path, `${name} is a system property of every entity.`);
|
|
@@ -314,7 +319,10 @@ function resolveProperties(candidate, scope) {
|
|
|
314
319
|
const properties = [];
|
|
315
320
|
for (const [name, schema] of entries) {
|
|
316
321
|
const path = `${candidate.name}.properties.${name}`;
|
|
317
|
-
const
|
|
322
|
+
const originalDefinition = propertyDefinition(schema);
|
|
323
|
+
const definition = !Array.isArray(originalDefinition?.scope)
|
|
324
|
+
? originalDefinition
|
|
325
|
+
: { ...originalDefinition, scope: originalDefinition.scope.map(nameToWire) };
|
|
318
326
|
checkMemberName("property", path, name, report);
|
|
319
327
|
if (definition === undefined) {
|
|
320
328
|
const hint = referenceTarget(schema) === undefined
|
|
@@ -325,7 +333,7 @@ function resolveProperties(candidate, scope) {
|
|
|
325
333
|
}
|
|
326
334
|
checkProperty(path, schema, definition, report);
|
|
327
335
|
if (definition.renamedFrom !== undefined) {
|
|
328
|
-
checkRenamedFrom(path, name, definition.renamedFrom, names,
|
|
336
|
+
checkRenamedFrom(path, name, definition.renamedFrom, names, LEGACY_MEMBER_NAME, report);
|
|
329
337
|
}
|
|
330
338
|
properties.push({ name, schema: schema, definition });
|
|
331
339
|
}
|
|
@@ -356,8 +364,8 @@ function checkSelectOptions(path, definition, report) {
|
|
|
356
364
|
}
|
|
357
365
|
options.forEach((option, index) => {
|
|
358
366
|
const optionPath = `${path}.options[${index}]`;
|
|
359
|
-
if (
|
|
360
|
-
report("invalid-options", optionPath, "option values
|
|
367
|
+
if (!SNAKE_CASE_NAME.test(option.value)) {
|
|
368
|
+
report("invalid-options", optionPath, "option values must be lowercase snake_case.");
|
|
361
369
|
}
|
|
362
370
|
else if (seen.has(option.value)) {
|
|
363
371
|
report("invalid-options", optionPath, `the option value "${option.value}" is listed twice.`);
|
|
@@ -500,8 +508,8 @@ function resolveActions(candidate, scope) {
|
|
|
500
508
|
const actions = [];
|
|
501
509
|
for (const [name, action] of actionEntries(candidate, report)) {
|
|
502
510
|
const path = `${candidate.name}.actions.${name}`;
|
|
503
|
-
if (!
|
|
504
|
-
report("invalid-name", path, "action names are
|
|
511
|
+
if (!SNAKE_CASE_NAME.test(name)) {
|
|
512
|
+
report("invalid-name", path, "action names are lowercase snake_case, for example change_status.");
|
|
505
513
|
}
|
|
506
514
|
else if (GENERATED_ACTIONS.includes(name) ||
|
|
507
515
|
GENERATED_UPSERT.test(name)) {
|
|
@@ -546,17 +554,23 @@ function resolvePermission(path, fallback, action, scope) {
|
|
|
546
554
|
}
|
|
547
555
|
function resolveInput(path, input, scope) {
|
|
548
556
|
const inputPath = `${path}.input`;
|
|
549
|
-
if (!Schema.isSchema(input) || input.ast._tag !== "Objects"
|
|
557
|
+
if (!Schema.isSchema(input) || Schema.toEncoded(input).ast._tag !== "Objects") {
|
|
550
558
|
scope.report("invalid-action-input", inputPath, "must be a Schema.Struct of the action's inputs.");
|
|
551
559
|
return undefined;
|
|
552
560
|
}
|
|
553
|
-
|
|
561
|
+
if ("fields" in input) {
|
|
562
|
+
checkInputFields(inputPath, input.fields, scope);
|
|
563
|
+
}
|
|
554
564
|
try {
|
|
555
565
|
const document = Schema.toJsonSchemaDocument(input, {
|
|
556
566
|
includeAnnotationKey: (key) => key === REFERENCE_ANNOTATION,
|
|
557
567
|
onExcessProperty: "error",
|
|
558
568
|
});
|
|
559
569
|
const schema = document.schema;
|
|
570
|
+
checkEncodedInputNames(schema, inputPath, scope, !("fields" in input));
|
|
571
|
+
for (const definition of Object.values(document.definitions)) {
|
|
572
|
+
checkEncodedInputNames(definition, inputPath, scope, !("fields" in input));
|
|
573
|
+
}
|
|
560
574
|
return Object.keys(document.definitions).length === 0
|
|
561
575
|
? schema
|
|
562
576
|
: { ...schema, $defs: document.definitions };
|
|
@@ -566,11 +580,38 @@ function resolveInput(path, input, scope) {
|
|
|
566
580
|
return undefined;
|
|
567
581
|
}
|
|
568
582
|
}
|
|
583
|
+
/** Check named input fields on the wire side; codecs may decode them to camelCase handler values. */
|
|
584
|
+
function checkEncodedInputNames(schema, path, scope, checkReferences) {
|
|
585
|
+
const { report } = scope;
|
|
586
|
+
const reference = schema[REFERENCE_ANNOTATION];
|
|
587
|
+
if (checkReferences && typeof reference === "string" && !scope.entitiesByName.has(reference)) {
|
|
588
|
+
report("unknown-reference", path, `references "${reference}", which is not an entity of this ontology.`);
|
|
589
|
+
}
|
|
590
|
+
const properties = schema.properties;
|
|
591
|
+
if (properties !== null && typeof properties === "object" && !Array.isArray(properties)) {
|
|
592
|
+
for (const [name, child] of Object.entries(properties)) {
|
|
593
|
+
if (!SNAKE_CASE_NAME.test(name)) {
|
|
594
|
+
report("invalid-name", `${path}.${name}`, "action input names are lowercase snake_case.");
|
|
595
|
+
}
|
|
596
|
+
if (child !== null && typeof child === "object" && !Array.isArray(child)) {
|
|
597
|
+
checkEncodedInputNames(child, `${path}.${name}`, scope, checkReferences);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
for (const key of ["items", "additionalProperties", "anyOf", "oneOf", "allOf"]) {
|
|
602
|
+
const children = schema[key];
|
|
603
|
+
for (const child of Array.isArray(children) ? children : [children]) {
|
|
604
|
+
if (child !== null && typeof child === "object" && !Array.isArray(child)) {
|
|
605
|
+
checkEncodedInputNames(child, path, scope, checkReferences);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
569
610
|
function checkInputFields(path, fields, scope) {
|
|
570
611
|
for (const [name, field] of Object.entries(fields)) {
|
|
571
612
|
const fieldPath = `${path}.${name}`;
|
|
572
613
|
const reference = referenceTarget(field);
|
|
573
|
-
if (reference !== undefined && !scope.entitiesByName.has(reference)) {
|
|
614
|
+
if (reference !== undefined && !scope.entitiesByName.has(nameToWire(reference))) {
|
|
574
615
|
scope.report("unknown-reference", fieldPath, `references "${reference}", which is not an entity of this ontology.`);
|
|
575
616
|
}
|
|
576
617
|
if (scope.entityProperties.has(field)) {
|
|
@@ -621,7 +662,7 @@ function checkEntityIdentities(entities, report) {
|
|
|
621
662
|
}
|
|
622
663
|
}
|
|
623
664
|
/**
|
|
624
|
-
* Properties, relationships, `<relationship>
|
|
665
|
+
* Properties, relationships, `<relationship>_id` fields and the inverses that land on an entity share
|
|
625
666
|
* one namespace: they are the record's fields and the API's traversal paths.
|
|
626
667
|
*/
|
|
627
668
|
function checkMemberNames(entities, report) {
|
|
@@ -666,7 +707,7 @@ function ownMembers(candidate) {
|
|
|
666
707
|
const path = `${candidate.name}.relationships.${name}`;
|
|
667
708
|
const own = { name, path, inverse: false };
|
|
668
709
|
return relationship.cardinality === "one" && candidate.entity.definition.source === undefined
|
|
669
|
-
? [own, { ...own, name: `${name}
|
|
710
|
+
? [own, { ...own, name: `${name}_id` }]
|
|
670
711
|
: [own];
|
|
671
712
|
});
|
|
672
713
|
return [...properties, ...relationships];
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { Schema, SchemaAST, SchemaTransformation } from "effect";
|
|
2
|
+
import { propertyDefinition } from "./property.js";
|
|
3
|
+
import { isRecord, nameToWire } from "./authoring-names.js";
|
|
4
|
+
function mapChildren(ast, convert) {
|
|
5
|
+
switch (ast._tag) {
|
|
6
|
+
case "Objects":
|
|
7
|
+
return new SchemaAST.Objects(ast.propertySignatures.map((field) => new SchemaAST.PropertySignature(field.name, convert(field.type))), ast.indexSignatures.map((field) => new SchemaAST.IndexSignature(field.parameter, convert(field.type))), ast.annotations, ast.checks, ast.encoding, ast.context);
|
|
8
|
+
case "Arrays":
|
|
9
|
+
return new SchemaAST.Arrays(ast.isMutable, ast.elements.map(convert), ast.rest.map(convert), ast.annotations, ast.checks, ast.encoding, ast.context);
|
|
10
|
+
case "Union":
|
|
11
|
+
return new SchemaAST.Union(ast.types.map(convert), ast.options, ast.annotations, ast.checks, ast.encoding, ast.context);
|
|
12
|
+
case "Suspend":
|
|
13
|
+
return new SchemaAST.Suspend(() => convert(ast.thunk()), ast.annotations, undefined, ast.encoding, ast.context);
|
|
14
|
+
default:
|
|
15
|
+
return ast;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function selectToWire(ast) {
|
|
19
|
+
if (ast._tag === "Literal" && typeof ast.literal === "string") {
|
|
20
|
+
return new SchemaAST.Literal(nameToWire(ast.literal), ast.annotations, ast.checks, ast.encoding, ast.context);
|
|
21
|
+
}
|
|
22
|
+
return mapChildren(ast, selectToWire);
|
|
23
|
+
}
|
|
24
|
+
/** A schema-aware codec: declared fields convert, JSON/dictionary keys and provider data do not. */
|
|
25
|
+
export function actionInputToWire(input) {
|
|
26
|
+
const encoded = Schema.toEncoded(input).ast;
|
|
27
|
+
const nodes = new WeakMap();
|
|
28
|
+
const wire = (ast) => {
|
|
29
|
+
const existing = nodes.get(ast);
|
|
30
|
+
if (existing !== undefined) {
|
|
31
|
+
return existing;
|
|
32
|
+
}
|
|
33
|
+
const property = propertyDefinition(Schema.make(ast));
|
|
34
|
+
if (property?.type === "json") {
|
|
35
|
+
return ast;
|
|
36
|
+
}
|
|
37
|
+
if (property?.type === "select") {
|
|
38
|
+
return selectToWire(ast);
|
|
39
|
+
}
|
|
40
|
+
const nested = mapChildren(ast, wire);
|
|
41
|
+
if (nested._tag !== "Objects") {
|
|
42
|
+
nodes.set(ast, nested);
|
|
43
|
+
return nested;
|
|
44
|
+
}
|
|
45
|
+
const names = new Set();
|
|
46
|
+
const fields = nested.propertySignatures.map((field) => {
|
|
47
|
+
const name = typeof field.name === "string" ? nameToWire(field.name) : field.name;
|
|
48
|
+
if (names.has(name)) {
|
|
49
|
+
throw new Error(`Input fields collide on wire name ${String(name)}.`);
|
|
50
|
+
}
|
|
51
|
+
names.add(name);
|
|
52
|
+
return new SchemaAST.PropertySignature(name, field.type);
|
|
53
|
+
});
|
|
54
|
+
// Object checks run on the original camelCase shape after decoding below.
|
|
55
|
+
const result = new SchemaAST.Objects(fields, nested.indexSignatures, nested.annotations, undefined, undefined, nested.context);
|
|
56
|
+
nodes.set(ast, result);
|
|
57
|
+
return result;
|
|
58
|
+
};
|
|
59
|
+
const rename = (value, ast, decode) => {
|
|
60
|
+
const property = propertyDefinition(Schema.make(ast));
|
|
61
|
+
if (property?.type === "json") {
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
if (property?.type === "select" && typeof value === "string") {
|
|
65
|
+
return decode
|
|
66
|
+
? (property.options?.find((option) => nameToWire(option.value) === value)?.value ?? value)
|
|
67
|
+
: nameToWire(value);
|
|
68
|
+
}
|
|
69
|
+
if (ast._tag === "Suspend") {
|
|
70
|
+
return rename(value, ast.thunk(), decode);
|
|
71
|
+
}
|
|
72
|
+
if (ast._tag === "Union") {
|
|
73
|
+
const member = ast.types.find((candidate) => Schema.is(Schema.make(decode ? wire(candidate) : candidate))(value));
|
|
74
|
+
return member === undefined ? value : rename(value, member, decode);
|
|
75
|
+
}
|
|
76
|
+
if (ast._tag === "Arrays" && Array.isArray(value)) {
|
|
77
|
+
return value.map((child, index) => {
|
|
78
|
+
const member = ast.elements[index] ?? ast.rest[0];
|
|
79
|
+
return member === undefined ? child : rename(child, member, decode);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
if (ast._tag !== "Objects" || !isRecord(value)) {
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
return Object.fromEntries(Object.entries(value).map(([name, child]) => {
|
|
86
|
+
const field = ast.propertySignatures.find((candidate) => (decode && typeof candidate.name === "string"
|
|
87
|
+
? nameToWire(candidate.name)
|
|
88
|
+
: candidate.name) === name);
|
|
89
|
+
return field === undefined
|
|
90
|
+
? [
|
|
91
|
+
name,
|
|
92
|
+
ast.indexSignatures[0] === undefined
|
|
93
|
+
? child
|
|
94
|
+
: rename(child, ast.indexSignatures[0].type, decode),
|
|
95
|
+
]
|
|
96
|
+
: [
|
|
97
|
+
decode ? field.name : nameToWire(String(field.name)),
|
|
98
|
+
rename(child, field.type, decode),
|
|
99
|
+
];
|
|
100
|
+
}));
|
|
101
|
+
};
|
|
102
|
+
const codec = Schema.make(wire(encoded)).pipe(Schema.decodeTo(input, SchemaTransformation.transform({
|
|
103
|
+
decode: (value) => rename(value, encoded, true),
|
|
104
|
+
encode: (value) => rename(value, encoded, false),
|
|
105
|
+
})));
|
|
106
|
+
return "fields" in input ? Object.assign(codec, { fields: input.fields }) : codec;
|
|
107
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Leave malformed identifiers intact so validation still reports them. */
|
|
2
|
+
export declare function nameToWire(name: string): string;
|
|
3
|
+
export declare function permissionToWire(permission: string): string;
|
|
4
|
+
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { toSnakeCase } from "./names.js";
|
|
2
|
+
/** Leave malformed identifiers intact so validation still reports them. */
|
|
3
|
+
export function nameToWire(name) {
|
|
4
|
+
return typeof name === "string" && /^[a-z][a-zA-Z0-9]*$/.test(name) ? toSnakeCase(name) : name;
|
|
5
|
+
}
|
|
6
|
+
export function permissionToWire(permission) {
|
|
7
|
+
return typeof permission === "string"
|
|
8
|
+
? permission.split(":").map(nameToWire).join(":")
|
|
9
|
+
: permission;
|
|
10
|
+
}
|
|
11
|
+
export function isRecord(value) {
|
|
12
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
13
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type AnyEntity, type Properties, type Relationships } from "./entity.js";
|
|
2
|
+
import type { Edit } from "./runtime/history.js";
|
|
3
|
+
import type { FindOptions, StoredValues } from "./runtime.js";
|
|
4
|
+
interface Definition {
|
|
5
|
+
readonly properties: Properties;
|
|
6
|
+
readonly relationships?: Relationships;
|
|
7
|
+
}
|
|
8
|
+
/** Only record keys and select identifiers convert; property values remain data. */
|
|
9
|
+
export declare function recordFromWire<A>(value: unknown, definition?: Definition): A;
|
|
10
|
+
export declare function valuesToWire(values: StoredValues, definition: Definition): StoredValues;
|
|
11
|
+
export declare function findToWire(options: FindOptions, definition: Definition): FindOptions;
|
|
12
|
+
/** Current action edits use the same property names and values as authored entity records. */
|
|
13
|
+
export declare function editsFromWire(edits: ReadonlyArray<Edit>, entities: ReadonlyArray<AnyEntity>): ReadonlyArray<Edit>;
|
|
14
|
+
/** Generated actions return entity records; authored action results remain application data. */
|
|
15
|
+
export declare function generatedResultFromWire(value: unknown, action: string, definition?: Definition): unknown;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { isEntity } from "./entity.js";
|
|
2
|
+
import { toCamelCase } from "./names.js";
|
|
3
|
+
import { propertyDefinition } from "./property.js";
|
|
4
|
+
import { isRecord, nameToWire } from "./authoring-names.js";
|
|
5
|
+
function targetDefinition(target) {
|
|
6
|
+
const resolved = typeof target === "function" ? target() : target;
|
|
7
|
+
return isEntity(resolved) ? resolved.definition : undefined;
|
|
8
|
+
}
|
|
9
|
+
function selectValue(value, property, decode) {
|
|
10
|
+
const definition = propertyDefinition(property);
|
|
11
|
+
if (definition?.type !== "select") {
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
const convert = (child) => typeof child !== "string"
|
|
15
|
+
? child
|
|
16
|
+
: decode
|
|
17
|
+
? (definition.options?.find((option) => nameToWire(option.value) === child)?.value ?? child)
|
|
18
|
+
: nameToWire(child);
|
|
19
|
+
return Array.isArray(value) ? value.map(convert) : convert(value);
|
|
20
|
+
}
|
|
21
|
+
/** Only record keys and select identifiers convert; property values remain data. */
|
|
22
|
+
export function recordFromWire(value, definition) {
|
|
23
|
+
if (!isRecord(value)) {
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
return Object.fromEntries(Object.entries(value).map(([name, child]) => {
|
|
27
|
+
const property = Object.entries(definition?.properties ?? {}).find(([key]) => nameToWire(key) === name);
|
|
28
|
+
if (property !== undefined) {
|
|
29
|
+
return [property[0], selectValue(child, property[1], true)];
|
|
30
|
+
}
|
|
31
|
+
const relationship = Object.entries(definition?.relationships ?? {}).find(([key]) => nameToWire(key) === name);
|
|
32
|
+
if (relationship !== undefined) {
|
|
33
|
+
const target = targetDefinition(relationship[1].target);
|
|
34
|
+
return [
|
|
35
|
+
relationship[0],
|
|
36
|
+
Array.isArray(child)
|
|
37
|
+
? child.map((item) => recordFromWire(item, target))
|
|
38
|
+
: recordFromWire(child, target),
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
return [toCamelCase(name), child];
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
export function valuesToWire(values, definition) {
|
|
45
|
+
return Object.fromEntries(Object.entries(values).map(([name, value]) => [
|
|
46
|
+
nameToWire(name),
|
|
47
|
+
selectValue(value, definition.properties[name], false),
|
|
48
|
+
]));
|
|
49
|
+
}
|
|
50
|
+
/** Translate query fields and relationships; operands of JSON filters stay opaque. */
|
|
51
|
+
function whereToWire(where, definition) {
|
|
52
|
+
return Object.fromEntries(Object.entries(where).map(([name, condition]) => {
|
|
53
|
+
if ((name === "and" || name === "or") && Array.isArray(condition)) {
|
|
54
|
+
return [name, condition.map((child) => whereToWire(child, definition))];
|
|
55
|
+
}
|
|
56
|
+
if (name === "not" && isRecord(condition)) {
|
|
57
|
+
return [name, whereToWire(condition, definition)];
|
|
58
|
+
}
|
|
59
|
+
const property = definition?.properties[name];
|
|
60
|
+
const relationship = definition?.relationships?.[name];
|
|
61
|
+
if (relationship !== undefined && isRecord(condition)) {
|
|
62
|
+
const target = targetDefinition(relationship.target);
|
|
63
|
+
return [
|
|
64
|
+
nameToWire(name),
|
|
65
|
+
relationship.cardinality === "many"
|
|
66
|
+
? Object.fromEntries(Object.entries(condition).map(([quantifier, child]) => [
|
|
67
|
+
quantifier,
|
|
68
|
+
isRecord(child) ? whereToWire(child, target) : child,
|
|
69
|
+
]))
|
|
70
|
+
: whereToWire(condition, target),
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
if (isRecord(condition)) {
|
|
74
|
+
const operators = [
|
|
75
|
+
"eq",
|
|
76
|
+
"ne",
|
|
77
|
+
"in",
|
|
78
|
+
"isNull",
|
|
79
|
+
"contains",
|
|
80
|
+
"startsWith",
|
|
81
|
+
"gt",
|
|
82
|
+
"gte",
|
|
83
|
+
"lt",
|
|
84
|
+
"lte",
|
|
85
|
+
"between",
|
|
86
|
+
];
|
|
87
|
+
if (Object.keys(condition).every((key) => operators.includes(key))) {
|
|
88
|
+
return [
|
|
89
|
+
nameToWire(name),
|
|
90
|
+
Object.fromEntries(Object.entries(condition).map(([key, child]) => [
|
|
91
|
+
key,
|
|
92
|
+
selectValue(child, property, false),
|
|
93
|
+
])),
|
|
94
|
+
];
|
|
95
|
+
}
|
|
96
|
+
if (propertyDefinition(property)?.type !== "json") {
|
|
97
|
+
return [nameToWire(name), whereToWire(condition)];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return [nameToWire(name), selectValue(condition, property, false)];
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
export function findToWire(options, definition) {
|
|
104
|
+
return {
|
|
105
|
+
...options,
|
|
106
|
+
...(options.where === undefined ? {} : { where: whereToWire(options.where, definition) }),
|
|
107
|
+
...(options.orderBy === undefined
|
|
108
|
+
? {}
|
|
109
|
+
: {
|
|
110
|
+
orderBy: options.orderBy.map((order) => ({ ...order, field: nameToWire(order.field) })),
|
|
111
|
+
}),
|
|
112
|
+
...(options.include === undefined
|
|
113
|
+
? {}
|
|
114
|
+
: {
|
|
115
|
+
include: Object.fromEntries(Object.entries(options.include).map(([name, value]) => [nameToWire(name), value])),
|
|
116
|
+
}),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/** Current action edits use the same property names and values as authored entity records. */
|
|
120
|
+
export function editsFromWire(edits, entities) {
|
|
121
|
+
return edits.map((edit) => {
|
|
122
|
+
const definition = entities.find((entity) => nameToWire(entity.name) === edit.entity)?.definition;
|
|
123
|
+
const before = recordFromWire(Object.fromEntries(Object.entries(edit.changes).map(([field, change]) => [field, change.before])), definition);
|
|
124
|
+
const after = recordFromWire(Object.fromEntries(Object.entries(edit.changes).map(([field, change]) => [field, change.after])), definition);
|
|
125
|
+
return {
|
|
126
|
+
...edit,
|
|
127
|
+
entity: toCamelCase(edit.entity),
|
|
128
|
+
changes: Object.fromEntries(Object.keys(before).map((field) => [
|
|
129
|
+
field,
|
|
130
|
+
{ before: before[field], after: after[field] },
|
|
131
|
+
])),
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/** Generated actions return entity records; authored action results remain application data. */
|
|
136
|
+
export function generatedResultFromWire(value, action, definition) {
|
|
137
|
+
if (action === "create" || action === "update") {
|
|
138
|
+
return recordFromWire(value, definition);
|
|
139
|
+
}
|
|
140
|
+
if (action.startsWith("upsert_by_") && isRecord(value)) {
|
|
141
|
+
return { ...value, record: recordFromWire(value.record, definition) };
|
|
142
|
+
}
|
|
143
|
+
return value;
|
|
144
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { OntologyConfig } from "./analyze.js";
|
|
2
|
+
import type { DiagnosticCode } from "./diagnostics.js";
|
|
3
|
+
import { type AnyEntity } from "./entity.js";
|
|
4
|
+
import type { Principal } from "./runtime.js";
|
|
5
|
+
type Report = (code: DiagnosticCode, path: string, message: string) => void;
|
|
6
|
+
export declare function principalFromWire(principal: Principal): Principal;
|
|
7
|
+
/** Canonical definitions feed REST, storage and future transports; the public config stays camelCase. */
|
|
8
|
+
export declare function authoringToWire(config: OntologyConfig, user: AnyEntity, report: Report): {
|
|
9
|
+
readonly config: OntologyConfig;
|
|
10
|
+
readonly user: AnyEntity;
|
|
11
|
+
};
|
|
12
|
+
export {};
|