@segmentstream-cloud/ontology 0.12.0 → 0.14.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 +84 -17
- package/dist/aggregate-routes.d.ts +95 -0
- package/dist/aggregate-routes.js +332 -0
- package/dist/analyze.js +83 -16
- package/dist/entity.d.ts +31 -15
- package/dist/index.d.ts +6 -3
- package/dist/index.js +5 -2
- package/dist/manifest.d.ts +57 -23
- package/dist/manifest.js +77 -30
- package/dist/measure.d.ts +11 -2
- package/dist/measure.js +14 -1
- package/dist/migrate/apply.js +2 -1
- package/dist/migrate/plan.d.ts +1 -1
- package/dist/migrate/plan.js +59 -22
- package/dist/migrate/storage.d.ts +15 -2
- package/dist/migrate/storage.js +63 -5
- package/dist/names.d.ts +4 -2
- package/dist/names.js +5 -1
- package/dist/query.d.ts +5 -2
- package/dist/query.js +6 -2
- package/dist/relationship.d.ts +36 -5
- package/dist/relationship.js +5 -3
- package/dist/render.d.ts +47 -3
- package/dist/render.js +117 -11
- package/dist/replication/model.d.ts +1 -1
- package/dist/replication/model.js +11 -1
- package/dist/runtime/agents.d.ts +9 -0
- package/dist/runtime/agents.js +145 -0
- package/dist/runtime/service-binding.js +2 -0
- package/dist/service/agent/protocol.d.ts +47 -0
- package/dist/service/agent/protocol.js +59 -0
- package/dist/service/agent/server.d.ts +27 -0
- package/dist/service/agent/server.js +235 -0
- package/dist/service/aggregate-platform.d.ts +60 -0
- package/dist/service/aggregate-platform.js +189 -0
- package/dist/service/aggregate.d.ts +42 -0
- package/dist/service/aggregate.js +145 -0
- package/dist/service/api.d.ts +5 -3
- package/dist/service/api.js +52 -6
- package/dist/service/authentication.d.ts +16 -5
- package/dist/service/authentication.js +51 -6
- package/dist/service/catalog.d.ts +10 -1
- package/dist/service/catalog.js +12 -3
- package/dist/service/config.d.ts +30 -1
- package/dist/service/config.js +53 -1
- package/dist/service/errors.d.ts +38 -0
- package/dist/service/errors.js +118 -0
- package/dist/service/handlers.d.ts +2 -1
- package/dist/service/handlers.js +66 -28
- package/dist/service/mcp/server.d.ts +6 -0
- package/dist/service/mcp/server.js +74 -17
- package/dist/service/mcp/tools.d.ts +13 -2
- package/dist/service/mcp/tools.js +165 -6
- package/dist/service/plan.js +7 -1
- package/dist/service/runtime-contract.d.ts +68 -0
- package/dist/service/runtime-identity.d.ts +59 -0
- package/dist/service/runtime-identity.js +102 -0
- package/dist/service/schemas.d.ts +99 -1
- package/dist/service/schemas.js +150 -2
- package/dist/service/service.d.ts +2 -2
- package/dist/service/service.js +24 -6
- package/dist/user.d.ts +1 -1
- package/dist/warehouse-backed.js +222 -121
- package/package.json +1 -1
package/dist/manifest.js
CHANGED
|
@@ -10,9 +10,16 @@ import { DEFAULT_SIGN_IN_POLICY, isSignInPolicy, } from "./sign-in.js";
|
|
|
10
10
|
* 2 replaced the actions' `allow` with `permission`, and added the roles' `permissions` and the
|
|
11
11
|
* entities' `permissions`; those fields are optional when decoding, so version 1 manifests still read.
|
|
12
12
|
* `signIn` came later within version 2 and is optional too, as are the actions' `connections` and the
|
|
13
|
-
* top-level `connections`, which are written only when an action calls a connection.
|
|
13
|
+
* top-level `connections`, which are written only when an action calls a connection. Version 3 added
|
|
14
|
+
* warehouse sources and measures. Version 4 writes snake_case names on the wire. Version 5 lets
|
|
15
|
+
* relationships target warehouse-backed entities and warehouse-backed properties declare `unique`:
|
|
16
|
+
* a relationship with a warehouse-backed end carries `on` and omits `optional` and `onDelete`.
|
|
17
|
+
* Version 6 adds the `countDistinct` measure operator and lets stored entities declare `measures`,
|
|
18
|
+
* whose columns name the stored properties' replication view columns.
|
|
14
19
|
*/
|
|
15
|
-
export const MANIFEST_VERSION =
|
|
20
|
+
export const MANIFEST_VERSION = 6;
|
|
21
|
+
/** The first version whose documents use snake_case names on the wire. */
|
|
22
|
+
const WIRE_MANIFEST_VERSION = 4;
|
|
16
23
|
/** The phrase used for a direction the author did not phrase. */
|
|
17
24
|
export const DEFAULT_PHRASE = "has";
|
|
18
25
|
export const SelectOptionManifest = Schema.Struct({
|
|
@@ -45,14 +52,21 @@ export const PropertyManifest = Schema.Struct({
|
|
|
45
52
|
* from `to`, `inverse` and the second `phrase`.
|
|
46
53
|
*/
|
|
47
54
|
export 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
|
+
*/
|
|
48
59
|
on: Schema.optionalKey(Schema.Struct({ property: Schema.String, targetProperty: Schema.String })),
|
|
49
60
|
name: Schema.String,
|
|
50
61
|
to: Schema.String,
|
|
51
62
|
inverse: Schema.String,
|
|
52
63
|
cardinality: Schema.Literals(["one", "many"]),
|
|
53
|
-
/** `rel.one` only. */
|
|
64
|
+
/** `rel.one` between stored entities only. */
|
|
54
65
|
optional: Schema.optionalKey(Schema.Boolean),
|
|
55
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* `rel.one` between stored entities only. Before version 5, warehouse → stored relationships
|
|
68
|
+
* carried it too, as `restrict`, though nothing enforced it.
|
|
69
|
+
*/
|
|
56
70
|
onDelete: Schema.optionalKey(Schema.Literals(ON_DELETE)),
|
|
57
71
|
/** `rel.many` only. */
|
|
58
72
|
ordered: Schema.optionalKey(Schema.Boolean),
|
|
@@ -60,6 +74,14 @@ export const RelationshipManifest = Schema.Struct({
|
|
|
60
74
|
phrase: Schema.Tuple([Schema.String, Schema.String]),
|
|
61
75
|
description: Schema.String,
|
|
62
76
|
});
|
|
77
|
+
/**
|
|
78
|
+
* Whether a relationship is matched by value: either end is warehouse-backed. A value-matched
|
|
79
|
+
* relationship always declares `on`; a stored → stored relationship never does. It has no
|
|
80
|
+
* `<name>_id` column, foreign key, index or traversal route.
|
|
81
|
+
*/
|
|
82
|
+
export function isValueMatched(relationship) {
|
|
83
|
+
return relationship.on !== undefined;
|
|
84
|
+
}
|
|
63
85
|
export const IndexManifest = Schema.Struct({
|
|
64
86
|
properties: Schema.Array(Schema.String),
|
|
65
87
|
unique: Schema.Boolean,
|
|
@@ -190,7 +212,7 @@ const EntityManifestWire = Schema.Struct({
|
|
|
190
212
|
relationships: Schema.Array(RelationshipManifestWire),
|
|
191
213
|
}).pipe(Schema.encodeKeys({ builtIn: "built_in", renamedFrom: "renamed_from" }));
|
|
192
214
|
const RoleManifestWire = RoleManifest.pipe(Schema.encodeKeys({ builtIn: "built_in" }));
|
|
193
|
-
/** The JSON contract written
|
|
215
|
+
/** The JSON contract written since version 4; decoded SDK models remain camelCase. */
|
|
194
216
|
export const OntologyManifestWire = Schema.Struct({
|
|
195
217
|
...OntologyManifestModel.fields,
|
|
196
218
|
entities: Schema.Array(EntityManifestWire),
|
|
@@ -210,7 +232,7 @@ export function encodeManifest(manifest) {
|
|
|
210
232
|
if (original !== undefined) {
|
|
211
233
|
return original;
|
|
212
234
|
}
|
|
213
|
-
return manifest.version <
|
|
235
|
+
return manifest.version < WIRE_MANIFEST_VERSION
|
|
214
236
|
? manifest
|
|
215
237
|
: Schema.encodeSync(OntologyManifestWire)(manifest);
|
|
216
238
|
}
|
|
@@ -292,7 +314,13 @@ function propertyManifest({ name, definition }) {
|
|
|
292
314
|
: {}),
|
|
293
315
|
};
|
|
294
316
|
}
|
|
295
|
-
|
|
317
|
+
/**
|
|
318
|
+
* `stored` says whether both ends are stored: only then does a `rel.one` have a key column, and so
|
|
319
|
+
* an optionality and a delete rule.
|
|
320
|
+
*/
|
|
321
|
+
function relationshipManifest(input) {
|
|
322
|
+
const { resolved, stored } = input;
|
|
323
|
+
const { name, relationship, target } = resolved;
|
|
296
324
|
const common = {
|
|
297
325
|
name,
|
|
298
326
|
to: target ?? "",
|
|
@@ -306,8 +334,9 @@ function relationshipManifest({ name, relationship, target, }) {
|
|
|
306
334
|
return {
|
|
307
335
|
...common,
|
|
308
336
|
cardinality: "one",
|
|
309
|
-
|
|
310
|
-
|
|
337
|
+
...(stored
|
|
338
|
+
? { optional: relationship.optional, onDelete: relationship.onDelete ?? "restrict" }
|
|
339
|
+
: {}),
|
|
311
340
|
...(relationship.on === undefined
|
|
312
341
|
? {}
|
|
313
342
|
: {
|
|
@@ -318,27 +347,39 @@ function relationshipManifest({ name, relationship, target, }) {
|
|
|
318
347
|
}),
|
|
319
348
|
};
|
|
320
349
|
}
|
|
321
|
-
|
|
350
|
+
/**
|
|
351
|
+
* The measures of an entity. A warehouse-backed property's column is its `column`; a stored property
|
|
352
|
+
* has none, and its column in the replication view is its wire name.
|
|
353
|
+
*/
|
|
354
|
+
function measuresManifest(resolved) {
|
|
355
|
+
const columns = new Map(resolved.properties.map((property) => [
|
|
356
|
+
property.name,
|
|
357
|
+
property.definition.column ?? property.name,
|
|
358
|
+
]));
|
|
359
|
+
return Object.entries(resolved.entity.definition.measures ?? {}).map(([name, measure]) => ({
|
|
360
|
+
name,
|
|
361
|
+
label: measure.label ?? humanize(name),
|
|
362
|
+
description: measure.description ?? "",
|
|
363
|
+
unit: measure.unit ?? "number",
|
|
364
|
+
format: measure.format ??
|
|
365
|
+
(measure.unit === "currency"
|
|
366
|
+
? "currency"
|
|
367
|
+
: measure.unit === "percent"
|
|
368
|
+
? "percent"
|
|
369
|
+
: "decimal"),
|
|
370
|
+
expression: measureExpression(measure, columns),
|
|
371
|
+
}));
|
|
372
|
+
}
|
|
373
|
+
/** `warehouseBacked` names the ontology's warehouse-backed entities, which relationships may target. */
|
|
374
|
+
function entityManifest(input) {
|
|
375
|
+
const { resolved, warehouseBacked } = input;
|
|
322
376
|
const { definition } = resolved.entity;
|
|
377
|
+
const storedOwner = definition.source === undefined;
|
|
378
|
+
const measures = measuresManifest(resolved);
|
|
323
379
|
return {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
source: definition.source,
|
|
328
|
-
measures: Object.entries(definition.measures ?? {}).map(([name, measure]) => ({
|
|
329
|
-
name,
|
|
330
|
-
label: measure.label ?? humanize(name),
|
|
331
|
-
description: measure.description ?? "",
|
|
332
|
-
unit: measure.unit ?? "number",
|
|
333
|
-
format: measure.format ??
|
|
334
|
-
(measure.unit === "currency"
|
|
335
|
-
? "currency"
|
|
336
|
-
: measure.unit === "percent"
|
|
337
|
-
? "percent"
|
|
338
|
-
: "decimal"),
|
|
339
|
-
expression: measureExpression(measure, new Map(resolved.properties.map((p) => [p.name, p.definition.column]))),
|
|
340
|
-
})),
|
|
341
|
-
}),
|
|
380
|
+
// A warehouse-backed entity always lists its measures; a stored entity only when it has some.
|
|
381
|
+
...(definition.source === undefined ? {} : { source: definition.source, measures }),
|
|
382
|
+
...(definition.source === undefined && measures.length > 0 ? { measures } : {}),
|
|
342
383
|
id: definition.id,
|
|
343
384
|
name: resolved.name,
|
|
344
385
|
label: definition.label,
|
|
@@ -349,7 +390,10 @@ function entityManifest(resolved) {
|
|
|
349
390
|
...(definition.renamedFrom !== undefined ? { renamedFrom: definition.renamedFrom } : {}),
|
|
350
391
|
...(definition.guide !== undefined ? { guide: definition.guide } : {}),
|
|
351
392
|
properties: resolved.properties.map(propertyManifest),
|
|
352
|
-
relationships: resolved.relationships.map(relationshipManifest
|
|
393
|
+
relationships: resolved.relationships.map((relationship) => relationshipManifest({
|
|
394
|
+
resolved: relationship,
|
|
395
|
+
stored: storedOwner && !warehouseBacked.has(relationship.target ?? ""),
|
|
396
|
+
})),
|
|
353
397
|
indexes: resolved.indexes.map((index) => ({
|
|
354
398
|
properties: [...index.properties],
|
|
355
399
|
unique: index.unique,
|
|
@@ -368,9 +412,12 @@ function entityManifest(resolved) {
|
|
|
368
412
|
/** Builds the manifest of a resolved ontology that passed `check`. */
|
|
369
413
|
export function buildManifest(input) {
|
|
370
414
|
const connections = input.connections ?? [];
|
|
415
|
+
const warehouseBacked = new Set(input.entities
|
|
416
|
+
.filter((candidate) => candidate.entity.definition.source !== undefined)
|
|
417
|
+
.map((candidate) => candidate.name));
|
|
371
418
|
const body = {
|
|
372
419
|
version: MANIFEST_VERSION,
|
|
373
|
-
entities: input.entities.map(entityManifest),
|
|
420
|
+
entities: input.entities.map((candidate) => entityManifest({ resolved: candidate, warehouseBacked })),
|
|
374
421
|
roles: input.roles.map((candidate) => ({
|
|
375
422
|
name: candidate.name,
|
|
376
423
|
description: candidate.description,
|
package/dist/measure.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export type MeasureExpression = {
|
|
|
7
7
|
readonly op: "literal";
|
|
8
8
|
readonly value: string;
|
|
9
9
|
} | {
|
|
10
|
-
readonly op: "sum" | "add" | "subtract" | "multiply" | "divide";
|
|
10
|
+
readonly op: "sum" | "countDistinct" | "add" | "subtract" | "multiply" | "divide";
|
|
11
11
|
readonly args: ReadonlyArray<MeasureExpression>;
|
|
12
12
|
};
|
|
13
13
|
export interface MeasureOptions {
|
|
@@ -22,6 +22,11 @@ export type Measures = Readonly<Record<string, Measure>>;
|
|
|
22
22
|
export declare const m: {
|
|
23
23
|
readonly count: (options?: MeasureOptions) => Measure;
|
|
24
24
|
readonly sum: (property: string, options?: MeasureOptions) => Measure;
|
|
25
|
+
/**
|
|
26
|
+
* The number of distinct non-null values of a text, email, url, select, integer, date or timestamp
|
|
27
|
+
* property. Distinct counts do not add up across groups.
|
|
28
|
+
*/
|
|
29
|
+
readonly countDistinct: (property: string, options?: MeasureOptions) => Measure;
|
|
25
30
|
readonly ref: (name: string, options?: MeasureOptions) => Measure;
|
|
26
31
|
readonly literal: (value: number | string, options?: MeasureOptions) => Measure;
|
|
27
32
|
readonly add: (left: MeasureExpression, right: MeasureExpression, options?: MeasureOptions) => Measure;
|
|
@@ -29,5 +34,9 @@ export declare const m: {
|
|
|
29
34
|
readonly multiply: (left: MeasureExpression, right: MeasureExpression, options?: MeasureOptions) => Measure;
|
|
30
35
|
readonly divide: (left: MeasureExpression, right: MeasureExpression, options?: MeasureOptions) => Measure;
|
|
31
36
|
};
|
|
32
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* Strip authoring options and resolve logical properties to physical columns for the manifest: a
|
|
39
|
+
* warehouse-backed property's `column`, or a stored property's replication view column, which is its
|
|
40
|
+
* wire name.
|
|
41
|
+
*/
|
|
33
42
|
export declare function measureExpression(value: MeasureExpression, columns: ReadonlyMap<string, string>): MeasureExpression;
|
package/dist/measure.js
CHANGED
|
@@ -9,6 +9,15 @@ export const m = {
|
|
|
9
9
|
args: [{ op: "column", name: property }],
|
|
10
10
|
...options,
|
|
11
11
|
}),
|
|
12
|
+
/**
|
|
13
|
+
* The number of distinct non-null values of a text, email, url, select, integer, date or timestamp
|
|
14
|
+
* property. Distinct counts do not add up across groups.
|
|
15
|
+
*/
|
|
16
|
+
countDistinct: (property, options) => ({
|
|
17
|
+
op: "countDistinct",
|
|
18
|
+
args: [{ op: "column", name: property }],
|
|
19
|
+
...options,
|
|
20
|
+
}),
|
|
12
21
|
ref: (name, options) => ({ op: "measure", name, ...options }),
|
|
13
22
|
literal: (value, options) => ({
|
|
14
23
|
op: "literal",
|
|
@@ -20,7 +29,11 @@ export const m = {
|
|
|
20
29
|
multiply: binary("multiply"),
|
|
21
30
|
divide: binary("divide"),
|
|
22
31
|
};
|
|
23
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Strip authoring options and resolve logical properties to physical columns for the manifest: a
|
|
34
|
+
* warehouse-backed property's `column`, or a stored property's replication view column, which is its
|
|
35
|
+
* wire name.
|
|
36
|
+
*/
|
|
24
37
|
export function measureExpression(value, columns) {
|
|
25
38
|
switch (value.op) {
|
|
26
39
|
case "count":
|
package/dist/migrate/apply.js
CHANGED
|
@@ -7,7 +7,7 @@ import { planMigration } from "./plan.js";
|
|
|
7
7
|
import { REPLICATION_DDL, REPLICATION_SCHEMA_VERSION, replicationEntityDdl, } from "../replication/schema.js";
|
|
8
8
|
import { createReplicationBaseline } from "../replication/baseline.js";
|
|
9
9
|
import { resolveReplicationSelection } from "../replication/model.js";
|
|
10
|
-
import { API_KEYS_TABLE, CATALOG_TABLE, OAUTH_TABLES, USER_ROLES_TABLE, quoteIdentifier, quoteLiteral, } from "./storage.js";
|
|
10
|
+
import { AGENT_TABLES, API_KEYS_TABLE, CATALOG_TABLE, OAUTH_TABLES, USER_ROLES_TABLE, quoteIdentifier, quoteLiteral, } from "./storage.js";
|
|
11
11
|
const CATALOG = quoteIdentifier(CATALOG_TABLE);
|
|
12
12
|
/** Serializes migrations of one database; transaction-scoped, so it ends with the transaction. */
|
|
13
13
|
const MIGRATION_LOCK = "segmentstream.ontology.migration";
|
|
@@ -171,6 +171,7 @@ export const RUNTIME_TABLES = [
|
|
|
171
171
|
USER_ROLES_TABLE,
|
|
172
172
|
API_KEYS_TABLE,
|
|
173
173
|
...OAUTH_TABLES,
|
|
174
|
+
...AGENT_TABLES,
|
|
174
175
|
];
|
|
175
176
|
/**
|
|
176
177
|
* The runtime tables the database has, for `planMigration`'s `existingTables`: a database that an
|
package/dist/migrate/plan.d.ts
CHANGED
package/dist/migrate/plan.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { isValueMatched, } from "../manifest.js";
|
|
1
2
|
import { BLOB_TABLES_DDL } from "../storage/tables.js";
|
|
2
3
|
import { SNAKE_CASE_NAME, isSystemProperty, toSnakeCase } from "../names.js";
|
|
3
|
-
import { CATALOG_TABLE_DDL, MAX_IDENTIFIER_LENGTH, OAUTH_TABLES, OAUTH_TABLES_DDL, USER_TABLE, addConstraintStatement, columnDefinition, constraintDefinition, createEnumStatement, createIndexStatement, createTableStatement, defaultExpression, entityStorage, enumTypeName, fieldColumnName, joinTable, propertyCheck, blobForeignKey, propertyColumn, propertyEnum, quoteIdentifier, quoteLiteral, relationshipColumn, RUNTIME_TABLES_DDL, relationshipForeignKey, SYSTEM_COLUMNS, tableName, } from "./storage.js";
|
|
4
|
+
import { AGENT_TABLES, AGENT_TABLES_DDL, CATALOG_TABLE_DDL, MAX_IDENTIFIER_LENGTH, OAUTH_TABLES, OAUTH_TABLES_DDL, USER_TABLE, addConstraintStatement, columnDefinition, constraintDefinition, createEnumStatement, createIndexStatement, createTableStatement, defaultExpression, entityStorage, enumTypeName, fieldColumnName, joinTable, propertyCheck, blobForeignKey, propertyColumn, propertyEnum, quoteIdentifier, quoteLiteral, relationshipColumn, RUNTIME_TABLES_DDL, relationshipForeignKey, SYSTEM_COLUMNS, tableName, } from "./storage.js";
|
|
4
5
|
const GLOBAL_KINDS = new Set(["table", "type", "index"]);
|
|
5
6
|
function step(input) {
|
|
6
7
|
return {
|
|
@@ -69,17 +70,29 @@ export function planMigration(input) {
|
|
|
69
70
|
draft.runtimeTables.push(step({
|
|
70
71
|
kind: "createCatalog",
|
|
71
72
|
classification: "additive",
|
|
72
|
-
description: "Create the runtime's role assignment, API key and
|
|
73
|
+
description: "Create the runtime's role assignment, API key, OAuth and agent tables.",
|
|
73
74
|
sql: RUNTIME_TABLES_DDL,
|
|
74
75
|
}));
|
|
75
76
|
}
|
|
76
|
-
else
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
77
|
+
else {
|
|
78
|
+
// A database an earlier SDK migrated lacks the tables added since; each set arrives on its own.
|
|
79
|
+
const lacks = (tables) => tables.some((table) => input.existingTables?.has(table) === false);
|
|
80
|
+
if (lacks(OAUTH_TABLES)) {
|
|
81
|
+
draft.runtimeTables.push(step({
|
|
82
|
+
kind: "createCatalog",
|
|
83
|
+
classification: "additive",
|
|
84
|
+
description: "Create the runtime's OAuth tables, which connect agents to the MCP server.",
|
|
85
|
+
sql: OAUTH_TABLES_DDL,
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
if (lacks(AGENT_TABLES)) {
|
|
89
|
+
draft.runtimeTables.push(step({
|
|
90
|
+
kind: "createCatalog",
|
|
91
|
+
classification: "additive",
|
|
92
|
+
description: "Create the runtime's agent tables, which pair channel accounts with members.",
|
|
93
|
+
sql: AGENT_TABLES_DDL,
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
83
96
|
}
|
|
84
97
|
if (storedTargets.some((entity) => entity.properties.some((property) => property.type === "blob")) &&
|
|
85
98
|
!current?.entities.some((entity) => entity.properties.some((property) => property.type === "blob"))) {
|
|
@@ -150,29 +163,32 @@ function validateManifest(manifest, refusals) {
|
|
|
150
163
|
const invalid = (path, message) => {
|
|
151
164
|
refusals.push(refusal("invalid-manifest", path, message));
|
|
152
165
|
};
|
|
153
|
-
const
|
|
166
|
+
const entitiesByName = new Map();
|
|
154
167
|
const ids = new Set();
|
|
155
168
|
for (const entity of manifest.entities) {
|
|
156
169
|
if (!SNAKE_CASE_NAME.test(entity.name)) {
|
|
157
170
|
invalid(entity.name, "entity names are lowercase snake_case.");
|
|
158
171
|
}
|
|
159
|
-
if (
|
|
172
|
+
if (entitiesByName.has(entity.name)) {
|
|
160
173
|
invalid(entity.name, `two entities are named ${entity.name}.`);
|
|
161
174
|
}
|
|
175
|
+
else {
|
|
176
|
+
entitiesByName.set(entity.name, entity);
|
|
177
|
+
}
|
|
162
178
|
if (ids.has(entity.id)) {
|
|
163
179
|
invalid(entity.name, `two entities have the id ${entity.id}.`);
|
|
164
180
|
}
|
|
165
|
-
names.add(entity.name);
|
|
166
181
|
ids.add(entity.id);
|
|
167
182
|
}
|
|
168
|
-
if (!
|
|
183
|
+
if (!entitiesByName.has(USER_TABLE)) {
|
|
169
184
|
invalid(USER_TABLE, "the built-in user entity is missing; every ontology includes it.");
|
|
170
185
|
}
|
|
171
186
|
for (const entity of manifest.entities) {
|
|
172
|
-
validateEntity(entity,
|
|
187
|
+
validateEntity({ entity, entitiesByName, invalid });
|
|
173
188
|
}
|
|
174
189
|
}
|
|
175
|
-
function validateEntity(
|
|
190
|
+
function validateEntity(input) {
|
|
191
|
+
const { entity, entitiesByName, invalid } = input;
|
|
176
192
|
for (const property of entity.properties) {
|
|
177
193
|
const path = `${entity.name}.properties.${property.name}`;
|
|
178
194
|
const values = (property.options ?? []).map((option) => option.value);
|
|
@@ -195,11 +211,21 @@ function validateEntity(entity, entityNames, invalid) {
|
|
|
195
211
|
if (!SNAKE_CASE_NAME.test(relationship.name) || isSystemProperty(relationship.name)) {
|
|
196
212
|
invalid(path, "relationship names are lowercase snake_case and cannot be system properties.");
|
|
197
213
|
}
|
|
198
|
-
|
|
214
|
+
const target = entitiesByName.get(relationship.to);
|
|
215
|
+
if (target === undefined) {
|
|
199
216
|
invalid(path, `it points at ${relationship.to}, which is not an entity.`);
|
|
200
217
|
}
|
|
201
|
-
if (relationship.
|
|
202
|
-
|
|
218
|
+
else if (isValueMatched(relationship) !== (entity.source !== undefined || target.source !== undefined)) {
|
|
219
|
+
// `on` is what keeps a relationship with a warehouse-backed end out of storage.
|
|
220
|
+
invalid(path, isValueMatched(relationship)
|
|
221
|
+
? "a relationship between stored entities uses a key column and cannot declare on."
|
|
222
|
+
: "a relationship with a warehouse-backed end is matched by value and needs on.");
|
|
223
|
+
}
|
|
224
|
+
// A value-matched relationship has no foreign key, so no delete rule.
|
|
225
|
+
if (relationship.cardinality === "one" &&
|
|
226
|
+
!isValueMatched(relationship) &&
|
|
227
|
+
relationship.onDelete === undefined) {
|
|
228
|
+
invalid(path, "a rel.one relationship between stored entities needs onDelete.");
|
|
203
229
|
}
|
|
204
230
|
if (relationship.onDelete === "setNull" && relationship.optional !== true) {
|
|
205
231
|
invalid(path, 'onDelete "setNull" needs an optional relationship.');
|
|
@@ -344,12 +370,17 @@ function matchEntities(currentEntities, targetEntities, refusals) {
|
|
|
344
370
|
for (const relationship of target.relationships) {
|
|
345
371
|
const previous = current.relationships.find(({ name }) => name === relationship.name);
|
|
346
372
|
if (previous === undefined) {
|
|
347
|
-
|
|
373
|
+
if (!isValueMatched(relationship)) {
|
|
374
|
+
addedRelationships.push(relationship);
|
|
375
|
+
}
|
|
348
376
|
continue;
|
|
349
377
|
}
|
|
378
|
+
// Both lists hold stored entities only, so a warehouse-backed target has no id here.
|
|
350
379
|
const previousTarget = entityIdByName(currentEntities, previous.to);
|
|
351
380
|
const nextTarget = entityIdByName(targetEntities, relationship.to);
|
|
352
|
-
if (previous.cardinality !== relationship.cardinality ||
|
|
381
|
+
if (previous.cardinality !== relationship.cardinality ||
|
|
382
|
+
previousTarget !== nextTarget ||
|
|
383
|
+
isValueMatched(previous) !== isValueMatched(relationship)) {
|
|
353
384
|
changed.add(relationship.name);
|
|
354
385
|
refusals.push(refusal("relationship-change", `${target.name}.relationships.${relationship.name}`, `was rel.${previous.cardinality} to ${previous.to} and is now ` +
|
|
355
386
|
`rel.${relationship.cardinality} to ${relationship.to}; a relationship keeps its ` +
|
|
@@ -357,9 +388,15 @@ function matchEntities(currentEntities, targetEntities, refusals) {
|
|
|
357
388
|
continue;
|
|
358
389
|
}
|
|
359
390
|
kept.add(relationship.name);
|
|
360
|
-
|
|
391
|
+
if (!isValueMatched(relationship)) {
|
|
392
|
+
relationships.push({ current: previous, target: relationship });
|
|
393
|
+
}
|
|
361
394
|
}
|
|
362
|
-
|
|
395
|
+
// A value-matched relationship is stored as the property its `on` names: adding, keeping or
|
|
396
|
+
// removing one plans no statement and drops nothing.
|
|
397
|
+
const removedRelationships = current.relationships.filter((relationship) => !kept.has(relationship.name) &&
|
|
398
|
+
!changed.has(relationship.name) &&
|
|
399
|
+
!isValueMatched(relationship));
|
|
363
400
|
pairs.push({
|
|
364
401
|
current,
|
|
365
402
|
target,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type EntityManifest, type PropertyManifest, type RelationshipManifest } from "../manifest.js";
|
|
2
2
|
/**
|
|
3
3
|
* The physical mapping of an ontology onto Postgres. The planner creates exactly these objects and the
|
|
4
4
|
* runtime reads and writes them, so every name here is part of the storage contract.
|
|
@@ -121,7 +121,8 @@ export declare function primaryKeyName(table: string): string;
|
|
|
121
121
|
export declare function indexName(table: string, columns: ReadonlyArray<string>): string;
|
|
122
122
|
/**
|
|
123
123
|
* The column of an index field: a system property (`created_at` → `created_at`), a property, or a
|
|
124
|
-
* `rel.one` (`owner` → `owner_id`). Returns undefined for anything else
|
|
124
|
+
* stored `rel.one` (`owner` → `owner_id`). Returns undefined for anything else, value-matched
|
|
125
|
+
* relationships included: they have no column.
|
|
125
126
|
*/
|
|
126
127
|
export declare function fieldColumnName(entity: EntityManifest, field: string): string | undefined;
|
|
127
128
|
/** The SQL default of a property, or undefined when it declares none. */
|
|
@@ -163,6 +164,18 @@ export declare const OAUTH_TOKENS_TABLE = "_oauth_tokens";
|
|
|
163
164
|
export declare const OAUTH_TABLES: ReadonlyArray<string>;
|
|
164
165
|
/** The OAuth tables of the MCP server's authorization server. Every statement is idempotent. */
|
|
165
166
|
export declare const OAUTH_TABLES_DDL: ReadonlyArray<string>;
|
|
167
|
+
/** Channel accounts waiting, for ten minutes, for a member to claim them with a short code. */
|
|
168
|
+
export declare const AGENT_PAIRINGS_TABLE = "_agent_pairings";
|
|
169
|
+
/** Channel accounts paired with members: the app's agent acts as the member for their messages. */
|
|
170
|
+
export declare const AGENT_IDENTITIES_TABLE = "_agent_identities";
|
|
171
|
+
/** The agent's tables, in the order they are created. */
|
|
172
|
+
export declare const AGENT_TABLES: ReadonlyArray<string>;
|
|
173
|
+
/**
|
|
174
|
+
* The tables of the app's agent: pairing codes, stored only as SHA-256 hashes, and the identities
|
|
175
|
+
* they create. An account and a member each have at most one live identity per provider. Every
|
|
176
|
+
* statement is idempotent.
|
|
177
|
+
*/
|
|
178
|
+
export declare const AGENT_TABLES_DDL: ReadonlyArray<string>;
|
|
166
179
|
/**
|
|
167
180
|
* The runtime's own tables beside the catalog. They are created with it, after the entity tables
|
|
168
181
|
* because they refer to the built-in user table, and every statement is idempotent.
|
package/dist/migrate/storage.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isValueMatched, } from "../manifest.js";
|
|
1
2
|
/**
|
|
2
3
|
* The physical mapping of an ontology onto Postgres. The planner creates exactly these objects and the
|
|
3
4
|
* runtime reads and writes them, so every name here is part of the storage contract.
|
|
@@ -102,7 +103,8 @@ export function indexName(table, columns) {
|
|
|
102
103
|
}
|
|
103
104
|
/**
|
|
104
105
|
* The column of an index field: a system property (`created_at` → `created_at`), a property, or a
|
|
105
|
-
* `rel.one` (`owner` → `owner_id`). Returns undefined for anything else
|
|
106
|
+
* stored `rel.one` (`owner` → `owner_id`). Returns undefined for anything else, value-matched
|
|
107
|
+
* relationships included: they have no column.
|
|
106
108
|
*/
|
|
107
109
|
export function fieldColumnName(entity, field) {
|
|
108
110
|
// Applied manifests may still name system fields in camelCase.
|
|
@@ -121,7 +123,9 @@ export function fieldColumnName(entity, field) {
|
|
|
121
123
|
if (entity.properties.some((property) => property.name === field)) {
|
|
122
124
|
return columnName(field);
|
|
123
125
|
}
|
|
124
|
-
if (entity.relationships.some((relationship) => relationship.name === field &&
|
|
126
|
+
if (entity.relationships.some((relationship) => relationship.name === field &&
|
|
127
|
+
relationship.cardinality === "one" &&
|
|
128
|
+
!isValueMatched(relationship))) {
|
|
125
129
|
return relationshipColumnName(field);
|
|
126
130
|
}
|
|
127
131
|
return undefined;
|
|
@@ -364,8 +368,7 @@ function entityIndexes(entity) {
|
|
|
364
368
|
unique: true,
|
|
365
369
|
origin: `${entity.name}.${property.name} is unique`,
|
|
366
370
|
}));
|
|
367
|
-
const keys = entity
|
|
368
|
-
.filter((relationship) => relationship.cardinality === "one")
|
|
371
|
+
const keys = keyedRelationships(entity)
|
|
369
372
|
.map((relationship) => ({
|
|
370
373
|
name: indexName(table, [relationshipColumnName(relationship.name)]),
|
|
371
374
|
table,
|
|
@@ -376,10 +379,17 @@ function entityIndexes(entity) {
|
|
|
376
379
|
.filter((index) => !declaredNames.has(index.name));
|
|
377
380
|
return [...unique, ...keys, ...declared];
|
|
378
381
|
}
|
|
382
|
+
/**
|
|
383
|
+
* The `rel.one` relationships that have a key column and a foreign key: those between stored
|
|
384
|
+
* entities. A value-matched relationship is stored as the ordinary property its `on` names.
|
|
385
|
+
*/
|
|
386
|
+
function keyedRelationships(entity) {
|
|
387
|
+
return entity.relationships.filter((relationship) => relationship.cardinality === "one" && !isValueMatched(relationship));
|
|
388
|
+
}
|
|
379
389
|
/** The table, history table, enum types and join tables of an entity. */
|
|
380
390
|
export function entityStorage(entity) {
|
|
381
391
|
const table = tableName(entity.name);
|
|
382
|
-
const ones = entity
|
|
392
|
+
const ones = keyedRelationships(entity);
|
|
383
393
|
const manys = entity.relationships.filter((relationship) => relationship.cardinality === "many");
|
|
384
394
|
const checks = entity.properties.flatMap((property) => {
|
|
385
395
|
const check = propertyCheck(entity.name, property);
|
|
@@ -514,6 +524,53 @@ export const OAUTH_TABLES_DDL = [
|
|
|
514
524
|
`CREATE INDEX IF NOT EXISTS "${OAUTH_TOKENS_TABLE}_grant_id_idx" ` +
|
|
515
525
|
`ON ${quoteIdentifier(OAUTH_TOKENS_TABLE)} ("grant_id")`,
|
|
516
526
|
];
|
|
527
|
+
/** Channel accounts waiting, for ten minutes, for a member to claim them with a short code. */
|
|
528
|
+
export const AGENT_PAIRINGS_TABLE = "_agent_pairings";
|
|
529
|
+
/** Channel accounts paired with members: the app's agent acts as the member for their messages. */
|
|
530
|
+
export const AGENT_IDENTITIES_TABLE = "_agent_identities";
|
|
531
|
+
/** The agent's tables, in the order they are created. */
|
|
532
|
+
export const AGENT_TABLES = [AGENT_PAIRINGS_TABLE, AGENT_IDENTITIES_TABLE];
|
|
533
|
+
/**
|
|
534
|
+
* The tables of the app's agent: pairing codes, stored only as SHA-256 hashes, and the identities
|
|
535
|
+
* they create. An account and a member each have at most one live identity per provider. Every
|
|
536
|
+
* statement is idempotent.
|
|
537
|
+
*/
|
|
538
|
+
export const AGENT_TABLES_DDL = [
|
|
539
|
+
[
|
|
540
|
+
`CREATE TABLE IF NOT EXISTS ${quoteIdentifier(AGENT_PAIRINGS_TABLE)} (`,
|
|
541
|
+
` "id" uuid NOT NULL,`,
|
|
542
|
+
` "code_hash" text NOT NULL,`,
|
|
543
|
+
` "provider" text NOT NULL,`,
|
|
544
|
+
` "external_id" text NOT NULL,`,
|
|
545
|
+
` "display" text,`,
|
|
546
|
+
` "created_at" timestamptz NOT NULL DEFAULT now(),`,
|
|
547
|
+
` "expires_at" timestamptz NOT NULL,`,
|
|
548
|
+
` "consumed_at" timestamptz,`,
|
|
549
|
+
` CONSTRAINT "${AGENT_PAIRINGS_TABLE}_pkey" PRIMARY KEY ("id"),`,
|
|
550
|
+
` CONSTRAINT "${AGENT_PAIRINGS_TABLE}_code_hash_key" UNIQUE ("code_hash")`,
|
|
551
|
+
`)`,
|
|
552
|
+
].join("\n"),
|
|
553
|
+
[
|
|
554
|
+
`CREATE TABLE IF NOT EXISTS ${quoteIdentifier(AGENT_IDENTITIES_TABLE)} (`,
|
|
555
|
+
` "id" uuid NOT NULL,`,
|
|
556
|
+
` "provider" text NOT NULL,`,
|
|
557
|
+
` "external_id" text NOT NULL,`,
|
|
558
|
+
` "user_id" uuid NOT NULL,`,
|
|
559
|
+
` "display" text,`,
|
|
560
|
+
` "created_at" timestamptz NOT NULL DEFAULT now(),`,
|
|
561
|
+
` "revoked_at" timestamptz,`,
|
|
562
|
+
` CONSTRAINT "${AGENT_IDENTITIES_TABLE}_pkey" PRIMARY KEY ("id"),`,
|
|
563
|
+
` CONSTRAINT "${AGENT_IDENTITIES_TABLE}_user_id_fkey" FOREIGN KEY ("user_id") ` +
|
|
564
|
+
`REFERENCES ${quoteIdentifier(USER_TABLE)} ("id") ON DELETE CASCADE`,
|
|
565
|
+
`)`,
|
|
566
|
+
].join("\n"),
|
|
567
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS "${AGENT_IDENTITIES_TABLE}_account_key" ` +
|
|
568
|
+
`ON ${quoteIdentifier(AGENT_IDENTITIES_TABLE)} ("provider", "external_id") ` +
|
|
569
|
+
`WHERE "revoked_at" IS NULL`,
|
|
570
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS "${AGENT_IDENTITIES_TABLE}_member_key" ` +
|
|
571
|
+
`ON ${quoteIdentifier(AGENT_IDENTITIES_TABLE)} ("user_id", "provider") ` +
|
|
572
|
+
`WHERE "revoked_at" IS NULL`,
|
|
573
|
+
];
|
|
517
574
|
/**
|
|
518
575
|
* The runtime's own tables beside the catalog. They are created with it, after the entity tables
|
|
519
576
|
* because they refer to the built-in user table, and every statement is idempotent.
|
|
@@ -546,6 +603,7 @@ export const RUNTIME_TABLES_DDL = [
|
|
|
546
603
|
].join("\n"),
|
|
547
604
|
`CREATE INDEX IF NOT EXISTS "${API_KEYS_TABLE}_user_id_idx" ON ${quoteIdentifier(API_KEYS_TABLE)} ("user_id")`,
|
|
548
605
|
...OAUTH_TABLES_DDL,
|
|
606
|
+
...AGENT_TABLES_DDL,
|
|
549
607
|
];
|
|
550
608
|
export function columnDefinition(column) {
|
|
551
609
|
const parts = [quoteIdentifier(column.name), column.type];
|
package/dist/names.d.ts
CHANGED
|
@@ -20,12 +20,14 @@ export declare const MEMBER_ROLE = "member";
|
|
|
20
20
|
export declare const HISTORY_SUFFIX = "_history";
|
|
21
21
|
/** The combinators of the query language, which take the place of field names in a `where`. */
|
|
22
22
|
export declare const RESERVED_MEMBER_NAMES: readonly ["and", "or", "not"];
|
|
23
|
+
/** The ontology service's route for aggregates across entities: `/api/ontology/aggregate`. */
|
|
24
|
+
export declare const AGGREGATE_ROUTE = "aggregate";
|
|
23
25
|
/** Routes of the ontology service under `/api/ontology/`, which an entity's routes would shadow. */
|
|
24
|
-
export declare const RESERVED_ENTITY_NAMES: readonly ["graph", "status", "describe", "openapi", "docs", "mcp", "me", "oauth"];
|
|
26
|
+
export declare const RESERVED_ENTITY_NAMES: readonly ["aggregate", "graph", "status", "describe", "openapi", "docs", "mcp", "me", "oauth", "agent"];
|
|
25
27
|
/** The MCP tools of every stored entity's reads: `lead_find`, `lead_get` and `lead_history`. */
|
|
26
28
|
export declare const MCP_READ_TOOLS: readonly ["find", "get", "history"];
|
|
27
29
|
/** The MCP tools of the ontology as a whole. No entity tool can take them: they have no underscore. */
|
|
28
|
-
export declare const MCP_GLOBAL_TOOLS: readonly ["describe", "me"];
|
|
30
|
+
export declare const MCP_GLOBAL_TOOLS: readonly ["describe", "me", "aggregate"];
|
|
29
31
|
/** The longest tool name that Claude and OpenAI accept. */
|
|
30
32
|
export declare const MCP_TOOL_NAME_MAX_LENGTH = 64;
|
|
31
33
|
/** The MCP tool of an entity's read or action: `lead_find`, `lead_change_status`. */
|
package/dist/names.js
CHANGED
|
@@ -48,8 +48,11 @@ export const MEMBER_ROLE = "member";
|
|
|
48
48
|
export const HISTORY_SUFFIX = "_history";
|
|
49
49
|
/** The combinators of the query language, which take the place of field names in a `where`. */
|
|
50
50
|
export const RESERVED_MEMBER_NAMES = ["and", "or", "not"];
|
|
51
|
+
/** The ontology service's route for aggregates across entities: `/api/ontology/aggregate`. */
|
|
52
|
+
export const AGGREGATE_ROUTE = "aggregate";
|
|
51
53
|
/** Routes of the ontology service under `/api/ontology/`, which an entity's routes would shadow. */
|
|
52
54
|
export const RESERVED_ENTITY_NAMES = [
|
|
55
|
+
AGGREGATE_ROUTE,
|
|
53
56
|
"graph",
|
|
54
57
|
"status",
|
|
55
58
|
"describe",
|
|
@@ -58,11 +61,12 @@ export const RESERVED_ENTITY_NAMES = [
|
|
|
58
61
|
"mcp",
|
|
59
62
|
"me",
|
|
60
63
|
"oauth",
|
|
64
|
+
"agent",
|
|
61
65
|
];
|
|
62
66
|
/** The MCP tools of every stored entity's reads: `lead_find`, `lead_get` and `lead_history`. */
|
|
63
67
|
export const MCP_READ_TOOLS = ["find", "get", "history"];
|
|
64
68
|
/** The MCP tools of the ontology as a whole. No entity tool can take them: they have no underscore. */
|
|
65
|
-
export const MCP_GLOBAL_TOOLS = ["describe", "me"];
|
|
69
|
+
export const MCP_GLOBAL_TOOLS = ["describe", "me", "aggregate"];
|
|
66
70
|
/** The longest tool name that Claude and OpenAI accept. */
|
|
67
71
|
export const MCP_TOOL_NAME_MAX_LENGTH = 64;
|
|
68
72
|
/** The MCP tool of an entity's read or action: `lead_find`, `lead_change_status`. */
|
package/dist/query.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
2
|
import type { JsonSchemaObject } from "./analyze.js";
|
|
3
|
-
import type
|
|
3
|
+
import { type EntityManifest, type OntologyManifest } from "./manifest.js";
|
|
4
4
|
import type { PropertyType } from "./property.js";
|
|
5
5
|
/** How a record field is stored: a property type, or a UUID for ids and `<relationship>_id` keys. */
|
|
6
6
|
export type StorageType = PropertyType | "uuid";
|
|
@@ -139,7 +139,10 @@ export interface QueryLink {
|
|
|
139
139
|
readonly declared: "one" | "many";
|
|
140
140
|
readonly inverse: boolean;
|
|
141
141
|
}
|
|
142
|
-
/**
|
|
142
|
+
/**
|
|
143
|
+
* The fields of an entity's records, in record order. A value-matched relationship has no
|
|
144
|
+
* `<relationship>_id`: its reference is the property named in `on`.
|
|
145
|
+
*/
|
|
143
146
|
export declare function queryFields(entity: EntityManifest): ReadonlyArray<QueryField>;
|
|
144
147
|
/** The relationships of an entity, declared ones first, then the inverses landing on it. */
|
|
145
148
|
export declare function queryLinks(manifest: OntologyManifest, entityName: string): ReadonlyArray<QueryLink>;
|