@segmentstream-cloud/ontology 0.6.0 → 0.7.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 -3
- package/dist/analyze.js +46 -13
- package/dist/connection.js +1 -1
- package/dist/entity.d.ts +6 -6
- 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 +7 -6
- package/dist/migrate/storage.d.ts +6 -6
- package/dist/migrate/storage.js +25 -15
- package/dist/names.d.ts +7 -10
- package/dist/names.js +10 -13
- package/dist/permissions.d.ts +2 -2
- package/dist/permissions.js +4 -4
- 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 +4 -7
- 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 +15 -12
- 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/errors.d.ts +1 -0
- package/dist/service/errors.js +5 -1
- package/dist/service/handlers.js +2 -2
- 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 +2 -2
- package/dist/user.d.ts +3 -3
- package/dist/user.js +7 -4
- 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,13 @@ 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
|
+
Ontology names and record fields use lowercase snake_case, including relationships,
|
|
12
|
+
actions, measures, select values, and system fields such as `created_at`. Generated
|
|
13
|
+
actions use names such as `upsert_by_email`. TypeScript helpers and configuration
|
|
14
|
+
options retain camelCase (`defineOntology`, `upsertBy`, `orderBy`, `renamedFrom`).
|
|
15
|
+
The generated REST API and version 4 manifest encode their metadata keys in
|
|
16
|
+
snake_case; `formatManifest` is the serializer for that wire contract.
|
|
17
|
+
|
|
11
18
|
## Install
|
|
12
19
|
|
|
13
20
|
```sh
|
|
@@ -51,7 +58,7 @@ export const Lead = entity("lead", {
|
|
|
51
58
|
relationships: {
|
|
52
59
|
owner: rel.one(User, {
|
|
53
60
|
optional: true,
|
|
54
|
-
inverse: "
|
|
61
|
+
inverse: "owned_leads",
|
|
55
62
|
onDelete: "setNull",
|
|
56
63
|
phrase: ["is owned by", "owns"],
|
|
57
64
|
description: "The team member responsible for the next step.",
|
|
@@ -121,7 +128,7 @@ entity, the verb or both: `lead:*`, `*:read`, `*:*`.
|
|
|
121
128
|
- Reads require `<entity>:read`: lists, queries, records, history, and both entities of a
|
|
122
129
|
relationship traversal. `include` and a filter through a relationship require `read` on
|
|
123
130
|
the related entity. A missing permission answers 403 with `<permission> is required.`
|
|
124
|
-
- Each action requires one permission. The generated `
|
|
131
|
+
- Each action requires one permission. The generated `upsert_by_<property>` requires both
|
|
125
132
|
`<entity>:create` and `<entity>:update`.
|
|
126
133
|
- A principal's permissions are the union of its roles' permissions in the ontology.
|
|
127
134
|
Members hold `member` and their assigned roles; an API key holds exactly the roles it was
|
|
@@ -182,7 +189,7 @@ Sign-in finds the user linked to the token's provider and subject, else links th
|
|
|
182
189
|
invited with the same email, else follows the ontology's `signIn` policy: `invited` answers
|
|
183
190
|
403 `{ "error": "not_invited", "detail": "Ask your administrator for an invitation." }`,
|
|
184
191
|
`open` creates a member. Deactivated members are refused. Each sign-in sets the user's
|
|
185
|
-
`
|
|
192
|
+
`last_sign_in_at`, at most once per 15 minutes, so the user's history records it.
|
|
186
193
|
|
|
187
194
|
`GET /api/ontology/me` answers who the caller is, for the app to render what they may do:
|
|
188
195
|
`{ user: { id, name, email, kind, active }, roles, permissions }`. It needs only
|
|
@@ -234,3 +241,7 @@ usage error.
|
|
|
234
241
|
## License
|
|
235
242
|
|
|
236
243
|
Apache-2.0
|
|
244
|
+
|
|
245
|
+
## Entity sources and aggregates
|
|
246
|
+
|
|
247
|
+
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,14 @@ 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
|
-
const GENERATED_UPSERT = new RegExp(`^${GENERATED_UPSERT_PREFIX}
|
|
13
|
+
const GENERATED_UPSERT = new RegExp(`^${GENERATED_UPSERT_PREFIX}`);
|
|
14
14
|
function errorMessage(error) {
|
|
15
15
|
return error instanceof Error ? error.message : String(error);
|
|
16
16
|
}
|
|
@@ -293,8 +293,8 @@ function entityPermissions(name, builtIn, relationships) {
|
|
|
293
293
|
};
|
|
294
294
|
}
|
|
295
295
|
function checkMemberName(kind, path, name, report) {
|
|
296
|
-
if (!
|
|
297
|
-
report("invalid-name", path, `${kind} names are
|
|
296
|
+
if (!SNAKE_CASE_NAME.test(name)) {
|
|
297
|
+
report("invalid-name", path, `${kind} names are lowercase snake_case, for example owned_leads.`);
|
|
298
298
|
}
|
|
299
299
|
else if (isSystemProperty(name)) {
|
|
300
300
|
report("reserved-name", path, `${name} is a system property of every entity.`);
|
|
@@ -325,7 +325,7 @@ function resolveProperties(candidate, scope) {
|
|
|
325
325
|
}
|
|
326
326
|
checkProperty(path, schema, definition, report);
|
|
327
327
|
if (definition.renamedFrom !== undefined) {
|
|
328
|
-
checkRenamedFrom(path, name, definition.renamedFrom, names,
|
|
328
|
+
checkRenamedFrom(path, name, definition.renamedFrom, names, LEGACY_MEMBER_NAME, report);
|
|
329
329
|
}
|
|
330
330
|
properties.push({ name, schema: schema, definition });
|
|
331
331
|
}
|
|
@@ -356,8 +356,8 @@ function checkSelectOptions(path, definition, report) {
|
|
|
356
356
|
}
|
|
357
357
|
options.forEach((option, index) => {
|
|
358
358
|
const optionPath = `${path}.options[${index}]`;
|
|
359
|
-
if (
|
|
360
|
-
report("invalid-options", optionPath, "option values
|
|
359
|
+
if (!SNAKE_CASE_NAME.test(option.value)) {
|
|
360
|
+
report("invalid-options", optionPath, "option values must be lowercase snake_case.");
|
|
361
361
|
}
|
|
362
362
|
else if (seen.has(option.value)) {
|
|
363
363
|
report("invalid-options", optionPath, `the option value "${option.value}" is listed twice.`);
|
|
@@ -500,8 +500,8 @@ function resolveActions(candidate, scope) {
|
|
|
500
500
|
const actions = [];
|
|
501
501
|
for (const [name, action] of actionEntries(candidate, report)) {
|
|
502
502
|
const path = `${candidate.name}.actions.${name}`;
|
|
503
|
-
if (!
|
|
504
|
-
report("invalid-name", path, "action names are
|
|
503
|
+
if (!SNAKE_CASE_NAME.test(name)) {
|
|
504
|
+
report("invalid-name", path, "action names are lowercase snake_case, for example change_status.");
|
|
505
505
|
}
|
|
506
506
|
else if (GENERATED_ACTIONS.includes(name) ||
|
|
507
507
|
GENERATED_UPSERT.test(name)) {
|
|
@@ -546,17 +546,23 @@ function resolvePermission(path, fallback, action, scope) {
|
|
|
546
546
|
}
|
|
547
547
|
function resolveInput(path, input, scope) {
|
|
548
548
|
const inputPath = `${path}.input`;
|
|
549
|
-
if (!Schema.isSchema(input) || input.ast._tag !== "Objects"
|
|
549
|
+
if (!Schema.isSchema(input) || Schema.toEncoded(input).ast._tag !== "Objects") {
|
|
550
550
|
scope.report("invalid-action-input", inputPath, "must be a Schema.Struct of the action's inputs.");
|
|
551
551
|
return undefined;
|
|
552
552
|
}
|
|
553
|
-
|
|
553
|
+
if ("fields" in input) {
|
|
554
|
+
checkInputFields(inputPath, input.fields, scope);
|
|
555
|
+
}
|
|
554
556
|
try {
|
|
555
557
|
const document = Schema.toJsonSchemaDocument(input, {
|
|
556
558
|
includeAnnotationKey: (key) => key === REFERENCE_ANNOTATION,
|
|
557
559
|
onExcessProperty: "error",
|
|
558
560
|
});
|
|
559
561
|
const schema = document.schema;
|
|
562
|
+
checkEncodedInputNames(schema, inputPath, scope, !("fields" in input));
|
|
563
|
+
for (const definition of Object.values(document.definitions)) {
|
|
564
|
+
checkEncodedInputNames(definition, inputPath, scope, !("fields" in input));
|
|
565
|
+
}
|
|
560
566
|
return Object.keys(document.definitions).length === 0
|
|
561
567
|
? schema
|
|
562
568
|
: { ...schema, $defs: document.definitions };
|
|
@@ -566,6 +572,33 @@ function resolveInput(path, input, scope) {
|
|
|
566
572
|
return undefined;
|
|
567
573
|
}
|
|
568
574
|
}
|
|
575
|
+
/** Check named input fields on the wire side; codecs may decode them to camelCase handler values. */
|
|
576
|
+
function checkEncodedInputNames(schema, path, scope, checkReferences) {
|
|
577
|
+
const { report } = scope;
|
|
578
|
+
const reference = schema[REFERENCE_ANNOTATION];
|
|
579
|
+
if (checkReferences && typeof reference === "string" && !scope.entitiesByName.has(reference)) {
|
|
580
|
+
report("unknown-reference", path, `references "${reference}", which is not an entity of this ontology.`);
|
|
581
|
+
}
|
|
582
|
+
const properties = schema.properties;
|
|
583
|
+
if (properties !== null && typeof properties === "object" && !Array.isArray(properties)) {
|
|
584
|
+
for (const [name, child] of Object.entries(properties)) {
|
|
585
|
+
if (!SNAKE_CASE_NAME.test(name)) {
|
|
586
|
+
report("invalid-name", `${path}.${name}`, "action input names are lowercase snake_case.");
|
|
587
|
+
}
|
|
588
|
+
if (child !== null && typeof child === "object" && !Array.isArray(child)) {
|
|
589
|
+
checkEncodedInputNames(child, `${path}.${name}`, scope, checkReferences);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
for (const key of ["items", "additionalProperties", "anyOf", "oneOf", "allOf"]) {
|
|
594
|
+
const children = schema[key];
|
|
595
|
+
for (const child of Array.isArray(children) ? children : [children]) {
|
|
596
|
+
if (child !== null && typeof child === "object" && !Array.isArray(child)) {
|
|
597
|
+
checkEncodedInputNames(child, path, scope, checkReferences);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
569
602
|
function checkInputFields(path, fields, scope) {
|
|
570
603
|
for (const [name, field] of Object.entries(fields)) {
|
|
571
604
|
const fieldPath = `${path}.${name}`;
|
|
@@ -621,7 +654,7 @@ function checkEntityIdentities(entities, report) {
|
|
|
621
654
|
}
|
|
622
655
|
}
|
|
623
656
|
/**
|
|
624
|
-
* Properties, relationships, `<relationship>
|
|
657
|
+
* Properties, relationships, `<relationship>_id` fields and the inverses that land on an entity share
|
|
625
658
|
* one namespace: they are the record's fields and the API's traversal paths.
|
|
626
659
|
*/
|
|
627
660
|
function checkMemberNames(entities, report) {
|
|
@@ -666,7 +699,7 @@ function ownMembers(candidate) {
|
|
|
666
699
|
const path = `${candidate.name}.relationships.${name}`;
|
|
667
700
|
const own = { name, path, inverse: false };
|
|
668
701
|
return relationship.cardinality === "one" && candidate.entity.definition.source === undefined
|
|
669
|
-
? [own, { ...own, name: `${name}
|
|
702
|
+
? [own, { ...own, name: `${name}_id` }]
|
|
670
703
|
: [own];
|
|
671
704
|
});
|
|
672
705
|
return [...properties, ...relationships];
|
package/dist/connection.js
CHANGED
|
@@ -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-
|
|
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
|
@@ -20,11 +20,11 @@ export interface SystemFields {
|
|
|
20
20
|
/** A UUID. */
|
|
21
21
|
readonly id: string;
|
|
22
22
|
/** ISO 8601 timestamps. */
|
|
23
|
-
readonly
|
|
24
|
-
readonly
|
|
23
|
+
readonly created_at: string;
|
|
24
|
+
readonly updated_at: string;
|
|
25
25
|
/** The `user` who created or last updated the record; `null` when the platform wrote it. */
|
|
26
|
-
readonly
|
|
27
|
-
readonly
|
|
26
|
+
readonly created_by: string | null;
|
|
27
|
+
readonly updated_by: string | null;
|
|
28
28
|
}
|
|
29
29
|
/** A record id, or a record. */
|
|
30
30
|
export type RecordRef = string | {
|
|
@@ -58,9 +58,9 @@ type UniqueKeys<P> = {
|
|
|
58
58
|
} ? K : never;
|
|
59
59
|
}[keyof P] & string;
|
|
60
60
|
type RelationshipIds<R> = {
|
|
61
|
-
readonly [K in OneKeys<R> as `${K}
|
|
61
|
+
readonly [K in OneKeys<R> as `${K}_id`]: R[K] extends OneRelationship<true> ? string | null : string;
|
|
62
62
|
};
|
|
63
|
-
/** A record of the entity: system fields, declared properties, and `<relationship>
|
|
63
|
+
/** A record of the entity: system fields, declared properties, and `<relationship>_id` per `rel.one`. */
|
|
64
64
|
export type EntityRecord<P, R> = Simplify<SystemFields & {
|
|
65
65
|
readonly [K in keyof P]: ValueOf<P[K]>;
|
|
66
66
|
} & RelationshipIds<R>>;
|
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";
|