@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/dist/query.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
|
+
import { wireStruct } from "./wire.js";
|
|
2
3
|
/**
|
|
3
4
|
* The query language of `find` and of the HTTP API's list and query endpoints.
|
|
4
5
|
*
|
|
@@ -10,7 +11,7 @@ import { Schema } from "effect";
|
|
|
10
11
|
* "tags": { "some": { "name": { "eq": "priority" } } },
|
|
11
12
|
* "or": [{ "fitScore": { "gte": 80 } }, { "source": "pilot_request" }]
|
|
12
13
|
* },
|
|
13
|
-
* "orderBy": [{ "field": "
|
|
14
|
+
* "orderBy": [{ "field": "created_at", "direction": "desc" }],
|
|
14
15
|
* "include": { "owner": true },
|
|
15
16
|
* "limit": 50
|
|
16
17
|
* }
|
|
@@ -72,10 +73,10 @@ export function queryResultSchema(record) {
|
|
|
72
73
|
}
|
|
73
74
|
const SYSTEM_FIELDS = [
|
|
74
75
|
{ name: "id", storage: "uuid", nullable: false },
|
|
75
|
-
{ name: "
|
|
76
|
-
{ name: "
|
|
77
|
-
{ name: "
|
|
78
|
-
{ name: "
|
|
76
|
+
{ name: "created_at", storage: "timestamp", nullable: false },
|
|
77
|
+
{ name: "updated_at", storage: "timestamp", nullable: false },
|
|
78
|
+
{ name: "created_by", storage: "uuid", nullable: true },
|
|
79
|
+
{ name: "updated_by", storage: "uuid", nullable: true },
|
|
79
80
|
];
|
|
80
81
|
/** The fields of an entity's records, in record order. */
|
|
81
82
|
export function queryFields(entity) {
|
|
@@ -90,7 +91,7 @@ export function queryFields(entity) {
|
|
|
90
91
|
const keys = entity.relationships
|
|
91
92
|
.filter((relationship) => relationship.cardinality === "one")
|
|
92
93
|
.map((relationship) => ({
|
|
93
|
-
name: `${relationship.name}
|
|
94
|
+
name: `${relationship.name}_id`,
|
|
94
95
|
storage: "uuid",
|
|
95
96
|
nullable: relationship.optional === true,
|
|
96
97
|
}));
|
|
@@ -224,7 +225,7 @@ function isOperatorObject(value) {
|
|
|
224
225
|
return (typeof value === "object" &&
|
|
225
226
|
value !== null &&
|
|
226
227
|
!Array.isArray(value) &&
|
|
227
|
-
Object.keys(value).every((key) => FILTER_OPERATORS.includes(key)));
|
|
228
|
+
Object.keys(value).every((key) => [...FILTER_OPERATORS, "is_null", "starts_with"].includes(key)));
|
|
228
229
|
}
|
|
229
230
|
/** The bare-value form of `eq`. For JSON, an object of operators is a filter, never a value. */
|
|
230
231
|
function shorthandSchema(field, value) {
|
|
@@ -233,7 +234,7 @@ function shorthandSchema(field, value) {
|
|
|
233
234
|
}
|
|
234
235
|
return JsonValue.check(Schema.makeFilter((candidate) => !isOperatorObject(candidate) || "Expected a filter"));
|
|
235
236
|
}
|
|
236
|
-
function fieldFilterSchema(field, identifier) {
|
|
237
|
+
function fieldFilterSchema(field, identifier, wire) {
|
|
237
238
|
const value = fieldValueSchema(field);
|
|
238
239
|
const operators = {
|
|
239
240
|
eq: Schema.optionalKey(value),
|
|
@@ -251,24 +252,28 @@ function fieldFilterSchema(field, identifier) {
|
|
|
251
252
|
}
|
|
252
253
|
operators.between = Schema.optionalKey(Schema.Tuple([value, value]));
|
|
253
254
|
}
|
|
254
|
-
const filter =
|
|
255
|
-
|
|
255
|
+
const filter = wire
|
|
256
|
+
? wireStruct(operators, { isNull: "is_null", startsWith: "starts_with" })
|
|
257
|
+
: closedStruct(operators, "Unknown operator");
|
|
258
|
+
const note = wire
|
|
259
|
+
? FILTER_DESCRIPTIONS[identifier]?.replaceAll("startsWith", "starts_with")
|
|
260
|
+
: FILTER_DESCRIPTIONS[identifier];
|
|
256
261
|
return Schema.Union([filter, shorthandSchema(field, value), Schema.Null]).annotate({
|
|
257
262
|
identifier,
|
|
258
263
|
description: "Conditions on the field, all of which must hold. A bare value means eq, null means " +
|
|
259
|
-
|
|
264
|
+
`${wire ? "is_null" : "isNull"}: true, and ne also matches empty values.${note === undefined ? "" : ` ${note}`}`,
|
|
260
265
|
});
|
|
261
266
|
}
|
|
262
|
-
function filterFor(entity, field, schemas) {
|
|
267
|
+
function filterFor(entity, field, schemas, wire) {
|
|
263
268
|
if (field.storage === "select") {
|
|
264
269
|
const identifier = `${pascalCase(entity.name)}${pascalCase(field.name)}Filter`;
|
|
265
|
-
const existing = schemas.get(identifier) ?? fieldFilterSchema(field, identifier);
|
|
270
|
+
const existing = schemas.get(identifier) ?? fieldFilterSchema(field, identifier, wire);
|
|
266
271
|
schemas.set(identifier, existing);
|
|
267
272
|
return existing;
|
|
268
273
|
}
|
|
269
274
|
const identifier = FILTER_IDENTIFIERS[field.storage];
|
|
270
|
-
const existing = sharedFilters.get(identifier) ?? fieldFilterSchema(field, identifier);
|
|
271
|
-
sharedFilters.set(identifier
|
|
275
|
+
const existing = sharedFilters.get(`${identifier}:${wire}`) ?? fieldFilterSchema(field, identifier, wire);
|
|
276
|
+
sharedFilters.set(`${identifier}:${wire}`, existing);
|
|
272
277
|
return existing;
|
|
273
278
|
}
|
|
274
279
|
/** `pilot_request` → `PilotRequest`, `fitScore` → `FitScore`. */
|
|
@@ -282,7 +287,7 @@ function pascalCase(name) {
|
|
|
282
287
|
* The `where` of an entity. With `links`, it includes the entity's relationships, whose targets are
|
|
283
288
|
* filtered by their own fields only: relationship paths are one level deep.
|
|
284
289
|
*/
|
|
285
|
-
function whereSchema(manifest, entity, links, schemas) {
|
|
290
|
+
function whereSchema(manifest, entity, links, schemas, wire) {
|
|
286
291
|
const identifier = `${pascalCase(entity.name)}${links ? "Where" : "FieldsWhere"}`;
|
|
287
292
|
const existing = schemas.get(identifier);
|
|
288
293
|
if (existing !== undefined) {
|
|
@@ -299,11 +304,11 @@ function whereSchema(manifest, entity, links, schemas) {
|
|
|
299
304
|
const fields = {};
|
|
300
305
|
schemas.set(identifier, self);
|
|
301
306
|
for (const field of queryFields(entity)) {
|
|
302
|
-
fields[field.name] = Schema.optionalKey(filterFor(entity, field, schemas));
|
|
307
|
+
fields[field.name] = Schema.optionalKey(filterFor(entity, field, schemas, wire));
|
|
303
308
|
}
|
|
304
309
|
if (links) {
|
|
305
310
|
for (const link of queryLinks(manifest, entity.name)) {
|
|
306
|
-
const target = whereSchema(manifest, requireEntity(manifest, link.target), false, schemas);
|
|
311
|
+
const target = whereSchema(manifest, requireEntity(manifest, link.target), false, schemas, wire);
|
|
307
312
|
fields[link.name] = Schema.optionalKey(link.cardinality === "one" ? target : manyFilterSchema(link, target));
|
|
308
313
|
}
|
|
309
314
|
}
|
|
@@ -322,6 +327,7 @@ function manyFilterSchema(link, target) {
|
|
|
322
327
|
description: `Whether some, none or every one of the related ${link.target} records match.`,
|
|
323
328
|
});
|
|
324
329
|
}
|
|
330
|
+
const wireQuerySchemas = new WeakMap();
|
|
325
331
|
const querySchemas = new WeakMap();
|
|
326
332
|
/**
|
|
327
333
|
* The precise query schemas of one entity: its fields with the operators of their types, its
|
|
@@ -329,8 +335,9 @@ const querySchemas = new WeakMap();
|
|
|
329
335
|
* include. Every struct is closed: an unknown field, operator or key is an issue whatever the parse
|
|
330
336
|
* options, never a silently ignored filter.
|
|
331
337
|
*/
|
|
332
|
-
export function entityQuerySchemas(manifest, entityName) {
|
|
333
|
-
const
|
|
338
|
+
export function entityQuerySchemas(manifest, entityName, wire = false) {
|
|
339
|
+
const cache = wire ? wireQuerySchemas : querySchemas;
|
|
340
|
+
const cached = cache.get(manifest)?.get(entityName);
|
|
334
341
|
if (cached !== undefined) {
|
|
335
342
|
return cached;
|
|
336
343
|
}
|
|
@@ -341,7 +348,7 @@ export function entityQuerySchemas(manifest, entityName) {
|
|
|
341
348
|
for (const link of queryLinks(manifest, entityName)) {
|
|
342
349
|
include[link.name] = Schema.optionalKey(Schema.Literal(true));
|
|
343
350
|
}
|
|
344
|
-
const where = whereSchema(manifest, entity, true, new Map());
|
|
351
|
+
const where = whereSchema(manifest, entity, true, new Map(), wire);
|
|
345
352
|
const orderBy = Schema.Array(closedStruct({
|
|
346
353
|
field: Schema.Literals(fields.map((field) => field.name)),
|
|
347
354
|
direction: Schema.optionalKey(SortDirection),
|
|
@@ -349,13 +356,16 @@ export function entityQuerySchemas(manifest, entityName) {
|
|
|
349
356
|
const included = closedStruct(include, "Unknown relationship").annotate({
|
|
350
357
|
description: "Related records to add to each record, by relationship name.",
|
|
351
358
|
});
|
|
352
|
-
const
|
|
359
|
+
const queryMembers = {
|
|
353
360
|
where: Schema.optionalKey(where),
|
|
354
361
|
orderBy: Schema.optionalKey(orderBy),
|
|
355
362
|
include: Schema.optionalKey(included),
|
|
356
363
|
after: Schema.optionalKey(After),
|
|
357
364
|
limit: Schema.optionalKey(Limit),
|
|
358
|
-
}
|
|
365
|
+
};
|
|
366
|
+
const query = (wire
|
|
367
|
+
? wireStruct(queryMembers, { orderBy: "order_by" })
|
|
368
|
+
: closedStruct(queryMembers, "Unknown key; use where, orderBy, include, after or limit")).annotate({ identifier: `${pascal}Query`, description: `A query on ${entity.plural}.` });
|
|
359
369
|
// The structs are assembled from the manifest, so their static types are too wide to name; they
|
|
360
370
|
// accept exactly the Query shape and decode it unchanged.
|
|
361
371
|
const schemas = {
|
|
@@ -366,9 +376,9 @@ export function entityQuerySchemas(manifest, entityName) {
|
|
|
366
376
|
after: After,
|
|
367
377
|
query,
|
|
368
378
|
};
|
|
369
|
-
const byEntity =
|
|
379
|
+
const byEntity = cache.get(manifest) ?? new Map();
|
|
370
380
|
byEntity.set(entityName, schemas);
|
|
371
|
-
|
|
381
|
+
cache.set(manifest, byEntity);
|
|
372
382
|
return schemas;
|
|
373
383
|
}
|
|
374
384
|
/** The query schema of one entity; see {@link entityQuerySchemas}. */
|
|
@@ -411,7 +421,7 @@ function recordValueSchema(field) {
|
|
|
411
421
|
}
|
|
412
422
|
/**
|
|
413
423
|
* The record of an entity as the runtime returns it: system fields, properties (`null` when empty;
|
|
414
|
-
* dates as `YYYY-MM-DD`, timestamps as ISO 8601 in UTC) and a `<relationship>
|
|
424
|
+
* dates as `YYYY-MM-DD`, timestamps as ISO 8601 in UTC) and a `<relationship>_id` per `rel.one`. It
|
|
415
425
|
* checks the shape of stored values, not the refinements of the definition (`maxLength`, `min`,
|
|
416
426
|
* patterns), so that tightening a refinement never makes existing records unreadable.
|
|
417
427
|
*/
|
|
@@ -430,7 +440,7 @@ function recordFields(entity) {
|
|
|
430
440
|
return fields;
|
|
431
441
|
}
|
|
432
442
|
/** A page of an entity's records, with the relationships a query may include. */
|
|
433
|
-
export function entityQueryResultSchema(manifest, entityName) {
|
|
443
|
+
export function entityQueryResultSchema(manifest, entityName, wire = false) {
|
|
434
444
|
const entity = requireEntity(manifest, entityName);
|
|
435
445
|
const fields = recordFields(entity);
|
|
436
446
|
for (const link of queryLinks(manifest, entityName)) {
|
|
@@ -438,7 +448,8 @@ export function entityQueryResultSchema(manifest, entityName) {
|
|
|
438
448
|
fields[link.name] = Schema.optionalKey(link.cardinality === "one" ? Schema.NullOr(target) : Schema.Array(target));
|
|
439
449
|
}
|
|
440
450
|
const record = Schema.Struct(fields);
|
|
441
|
-
|
|
451
|
+
const page = queryResultSchema(record);
|
|
452
|
+
return wire ? page.pipe(Schema.encodeKeys({ nextCursor: "next_cursor" })) : page;
|
|
442
453
|
}
|
|
443
454
|
/** A JSON Schema whose root is the schema itself, not a `$ref`, as MCP tool inputs need it. */
|
|
444
455
|
function jsonSchema(schema) {
|
|
@@ -453,9 +464,9 @@ function jsonSchema(schema) {
|
|
|
453
464
|
}
|
|
454
465
|
/** The JSON Schema (draft 2020-12) of an entity's queries, for MCP tools, OpenAPI and the console. */
|
|
455
466
|
export function queryJsonSchema(manifest, entityName) {
|
|
456
|
-
return jsonSchema(
|
|
467
|
+
return jsonSchema(entityQuerySchemas(manifest, entityName, true).query);
|
|
457
468
|
}
|
|
458
469
|
/** The JSON Schema (draft 2020-12) of a page of an entity's records. */
|
|
459
470
|
export function queryResultJsonSchema(manifest, entityName) {
|
|
460
|
-
return jsonSchema(entityQueryResultSchema(manifest, entityName));
|
|
471
|
+
return jsonSchema(entityQueryResultSchema(manifest, entityName, true));
|
|
461
472
|
}
|
package/dist/runtime/cursor.js
CHANGED
|
@@ -3,7 +3,7 @@ import { fieldValueSchema } from "../query.js";
|
|
|
3
3
|
import { bindValue } from "./records.js";
|
|
4
4
|
import { columnOf } from "./statement.js";
|
|
5
5
|
/** The default order: newest first. */
|
|
6
|
-
const DEFAULT_ORDER = [{ field: "
|
|
6
|
+
const DEFAULT_ORDER = [{ field: "created_at", direction: "desc" }];
|
|
7
7
|
export function sortKeys(entity, orderBy) {
|
|
8
8
|
const requested = orderBy === undefined || orderBy.length === 0 ? DEFAULT_ORDER : orderBy;
|
|
9
9
|
const keys = [];
|
|
@@ -3,13 +3,13 @@ import type { JsonSchemaObject } from "../analyze.js";
|
|
|
3
3
|
import type { Ontology } from "../ontology.js";
|
|
4
4
|
/** An entity's generated actions by name. */
|
|
5
5
|
export type GeneratedActions = Readonly<Record<string, AnyAction>>;
|
|
6
|
-
/** `
|
|
6
|
+
/** `upsert_by_email`. */
|
|
7
7
|
export declare function upsertActionName(property: string): string;
|
|
8
8
|
/**
|
|
9
9
|
* The actions generated for an entity: `create`, `update` (without `managed` properties), `delete`,
|
|
10
|
-
* `
|
|
10
|
+
* `upsert_by_<property>` per unique property, and `link`/`unlink` when it declares a `rel.many`. They
|
|
11
11
|
* are ordinary actions and run through the same pipeline. Their `permission` is unset: each requires
|
|
12
|
-
* the permission named after it (`lead:create`), and `
|
|
12
|
+
* the permission named after it (`lead:create`), and `upsert_by_<property>` both `create` and `update`,
|
|
13
13
|
* as `generatedActionPermissions` says. The built-in User has none: members are created by sign-in
|
|
14
14
|
* and services by the app or the CLI.
|
|
15
15
|
*/
|
|
@@ -18,7 +18,7 @@ export declare function generatedActions(ontology: Ontology, entityName: string)
|
|
|
18
18
|
export interface GeneratedActionManifest {
|
|
19
19
|
readonly name: string;
|
|
20
20
|
readonly description: string;
|
|
21
|
-
/** Every permission the action requires: one, or `create` and `update` for `
|
|
21
|
+
/** Every permission the action requires: one, or `create` and `update` for `upsert_by_<property>`. */
|
|
22
22
|
readonly permissions: ReadonlyArray<string>;
|
|
23
23
|
readonly input: JsonSchemaObject;
|
|
24
24
|
}
|
|
@@ -12,12 +12,9 @@ const builtActions = new WeakMap();
|
|
|
12
12
|
function article(word) {
|
|
13
13
|
return /^[aeiou]/i.test(word) ? `an ${word}` : `a ${word}`;
|
|
14
14
|
}
|
|
15
|
-
|
|
16
|
-
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
17
|
-
}
|
|
18
|
-
/** `upsertByEmail`. */
|
|
15
|
+
/** `upsert_by_email`. */
|
|
19
16
|
export function upsertActionName(property) {
|
|
20
|
-
return `${GENERATED_UPSERT_PREFIX}${
|
|
17
|
+
return `${GENERATED_UPSERT_PREFIX}${property}`;
|
|
21
18
|
}
|
|
22
19
|
/** A property's value schema, refusing days that do not exist, which PostgreSQL would reject. */
|
|
23
20
|
function propertyInput(field) {
|
|
@@ -83,9 +80,9 @@ function valuesOf(input, omit) {
|
|
|
83
80
|
}
|
|
84
81
|
/**
|
|
85
82
|
* The actions generated for an entity: `create`, `update` (without `managed` properties), `delete`,
|
|
86
|
-
* `
|
|
83
|
+
* `upsert_by_<property>` per unique property, and `link`/`unlink` when it declares a `rel.many`. They
|
|
87
84
|
* are ordinary actions and run through the same pipeline. Their `permission` is unset: each requires
|
|
88
|
-
* the permission named after it (`lead:create`), and `
|
|
85
|
+
* the permission named after it (`lead:create`), and `upsert_by_<property>` both `create` and `update`,
|
|
89
86
|
* as `generatedActionPermissions` says. The built-in User has none: members are created by sign-in
|
|
90
87
|
* and services by the app or the CLI.
|
|
91
88
|
*/
|
package/dist/runtime/history.js
CHANGED
|
@@ -33,7 +33,7 @@ export const HistoryEntry = Schema.Struct({
|
|
|
33
33
|
at: Schema.String,
|
|
34
34
|
changes: Changes,
|
|
35
35
|
});
|
|
36
|
-
/** The fields history records: declared properties and `<relationship>
|
|
36
|
+
/** The fields history records: declared properties and `<relationship>_id` keys. */
|
|
37
37
|
function changedFields(entity) {
|
|
38
38
|
return entity.fields.filter((field) => field.source !== "system");
|
|
39
39
|
}
|
package/dist/runtime/model.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export interface FieldModel extends QueryField {
|
|
|
12
12
|
readonly source: "system" | "property" | "relationship";
|
|
13
13
|
/** The declared property, for properties. */
|
|
14
14
|
readonly property?: ResolvedProperty;
|
|
15
|
-
/** The relationship whose key the field holds, for `<relationship>
|
|
15
|
+
/** The relationship whose key the field holds, for `<relationship>_id`. */
|
|
16
16
|
readonly relationship?: string;
|
|
17
17
|
}
|
|
18
18
|
/** A declared `rel.one`: this entity's key column points at the target's id. */
|
|
@@ -45,7 +45,7 @@ export interface EntityModel {
|
|
|
45
45
|
readonly historyTable: string;
|
|
46
46
|
readonly resolved: ResolvedEntity;
|
|
47
47
|
readonly manifest: EntityManifest;
|
|
48
|
-
/** System fields, properties and `<relationship>
|
|
48
|
+
/** System fields, properties and `<relationship>_id` keys, in record order. */
|
|
49
49
|
readonly fields: ReadonlyArray<FieldModel>;
|
|
50
50
|
readonly fieldsByName: ReadonlyMap<string, FieldModel>;
|
|
51
51
|
readonly links: ReadonlyMap<string, LinkModel>;
|
package/dist/runtime/model.js
CHANGED
|
@@ -80,7 +80,7 @@ function buildField(entity, field, properties) {
|
|
|
80
80
|
property,
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
|
-
const relationship = field.name.slice(0, -"
|
|
83
|
+
const relationship = field.name.slice(0, -"_id".length);
|
|
84
84
|
return {
|
|
85
85
|
...field,
|
|
86
86
|
column: relationshipColumnName(relationship),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { propertyDefinition } from "../property.js";
|
|
1
2
|
import { pbkdf2, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
2
3
|
import { promisify } from "node:util";
|
|
3
4
|
import { Effect, Schema } from "effect";
|
|
@@ -110,19 +111,19 @@ function userRow(record) {
|
|
|
110
111
|
* the ontology's sign-in policy decides: `invited` turns the person away, `open` creates a member.
|
|
111
112
|
*/
|
|
112
113
|
function signIn(ontology, sql, claims) {
|
|
113
|
-
const runtime = platformRuntime(ontology, sql, "user.
|
|
114
|
+
const runtime = platformRuntime(ontology, sql, "user.sign_in");
|
|
114
115
|
const claimedEmail = claims.email?.trim().toLowerCase();
|
|
115
116
|
const email = claimedEmail !== undefined && EMAIL_PATTERN.test(claimedEmail) ? claimedEmail : undefined;
|
|
116
117
|
return sql
|
|
117
118
|
.withTransaction(Effect.gen(function* () {
|
|
118
119
|
const now = new Date();
|
|
119
|
-
const signedIn = {
|
|
120
|
+
const signedIn = { last_sign_in_at: now.toISOString() };
|
|
120
121
|
const [linked] = yield* runtime.find({
|
|
121
122
|
entity: "user",
|
|
122
123
|
options: {
|
|
123
124
|
where: {
|
|
124
|
-
|
|
125
|
-
|
|
125
|
+
identity_provider: claims.identityProvider,
|
|
126
|
+
identity_subject: claims.identitySubject,
|
|
126
127
|
},
|
|
127
128
|
limit: 1,
|
|
128
129
|
forUpdate: true,
|
|
@@ -130,7 +131,7 @@ function signIn(ontology, sql, claims) {
|
|
|
130
131
|
});
|
|
131
132
|
if (linked !== undefined) {
|
|
132
133
|
// An inactive user is turned away, which is not a sign-in to record.
|
|
133
|
-
if (linked.active !== true || !signInDue(linked.
|
|
134
|
+
if (linked.active !== true || !signInDue(linked.last_sign_in_at, now)) {
|
|
134
135
|
return userRow(linked);
|
|
135
136
|
}
|
|
136
137
|
const returning = yield* runtime.update({
|
|
@@ -147,7 +148,7 @@ function signIn(ontology, sql, claims) {
|
|
|
147
148
|
options: { where: { email }, limit: 1, forUpdate: true },
|
|
148
149
|
});
|
|
149
150
|
if (invited !== undefined) {
|
|
150
|
-
if (invited.kind !== "member" || invited.
|
|
151
|
+
if (invited.kind !== "member" || invited.identity_subject !== null) {
|
|
151
152
|
return yield* new PrincipalRejected({
|
|
152
153
|
reason: "identity_conflict",
|
|
153
154
|
message: "Another user already has this email.",
|
|
@@ -157,8 +158,8 @@ function signIn(ontology, sql, claims) {
|
|
|
157
158
|
entity: "user",
|
|
158
159
|
id: invited.id,
|
|
159
160
|
values: {
|
|
160
|
-
|
|
161
|
-
|
|
161
|
+
identity_provider: claims.identityProvider,
|
|
162
|
+
identity_subject: claims.identitySubject,
|
|
162
163
|
...(invited.active === true ? signedIn : {}),
|
|
163
164
|
},
|
|
164
165
|
});
|
|
@@ -176,8 +177,8 @@ function signIn(ontology, sql, claims) {
|
|
|
176
177
|
name: claims.name?.trim() || email || claims.identitySubject,
|
|
177
178
|
email: email ?? null,
|
|
178
179
|
kind: "member",
|
|
179
|
-
|
|
180
|
-
|
|
180
|
+
identity_provider: claims.identityProvider,
|
|
181
|
+
identity_subject: claims.identitySubject,
|
|
181
182
|
...signedIn,
|
|
182
183
|
},
|
|
183
184
|
});
|
|
@@ -295,7 +296,7 @@ export function setUserRoles(ontology, input) {
|
|
|
295
296
|
export function createServiceUser(ontology, input) {
|
|
296
297
|
return Effect.gen(function* () {
|
|
297
298
|
const sql = yield* SqlClient.SqlClient;
|
|
298
|
-
const runtime = platformRuntime(ontology, sql, "user.
|
|
299
|
+
const runtime = platformRuntime(ontology, sql, "user.create_service");
|
|
299
300
|
return yield* sql.withTransaction(Effect.gen(function* () {
|
|
300
301
|
const user = yield* runtime
|
|
301
302
|
.create({ entity: "user", values: { name: input.name, kind: "service" } })
|
|
@@ -317,7 +318,9 @@ function deployedUser(manifest) {
|
|
|
317
318
|
return User;
|
|
318
319
|
}
|
|
319
320
|
const names = new Set(deployed.properties.map((property) => property.name));
|
|
320
|
-
const kept = Object.entries(User.definition.properties).filter(([name]) => names.has(name)
|
|
321
|
+
const kept = Object.entries(User.definition.properties).filter(([name, property]) => names.has(name) ||
|
|
322
|
+
(propertyDefinition(property)?.renamedFrom !== undefined &&
|
|
323
|
+
names.has(propertyDefinition(property).renamedFrom)));
|
|
321
324
|
if (kept.length === Object.keys(User.definition.properties).length) {
|
|
322
325
|
return User;
|
|
323
326
|
}
|
package/dist/runtime/reads.js
CHANGED
|
@@ -139,7 +139,7 @@ function loadRelated(context, link, records) {
|
|
|
139
139
|
const target = entityModel(context.model, link.target);
|
|
140
140
|
const params = new Params();
|
|
141
141
|
if (link.via === "key") {
|
|
142
|
-
const keyField = `${link.name}
|
|
142
|
+
const keyField = `${link.name}_id`;
|
|
143
143
|
const ids = [...new Set(records.map((record) => record[keyField]).filter(isString))];
|
|
144
144
|
const byId = new Map();
|
|
145
145
|
if (ids.length > 0) {
|
package/dist/runtime/records.js
CHANGED
|
@@ -130,7 +130,7 @@ export function prepareWrite(entity, values, mode) {
|
|
|
130
130
|
columns.push({ field, value });
|
|
131
131
|
}
|
|
132
132
|
else if (link?.via === "key") {
|
|
133
|
-
const key = entity.fieldsByName.get(`${name}
|
|
133
|
+
const key = entity.fieldsByName.get(`${name}_id`);
|
|
134
134
|
if (value === null && !link.optional) {
|
|
135
135
|
issues.push({ path: [name], message: `${name} is required` });
|
|
136
136
|
}
|
|
@@ -117,7 +117,7 @@ export function makeOntologyRuntime(ontology, sql, scope) {
|
|
|
117
117
|
}
|
|
118
118
|
const target = entityModel(model, link.target);
|
|
119
119
|
const validated = yield* validateQuery(target, input.query ?? {});
|
|
120
|
-
const keyValue = link.via === "key" ? (source[`${link.name}
|
|
120
|
+
const keyValue = link.via === "key" ? (source[`${link.name}_id`] ?? null) : null;
|
|
121
121
|
return yield* orDieOnSql(runQuery(context, target, validated, {
|
|
122
122
|
condition: relatedCondition(link, source.id, keyValue),
|
|
123
123
|
}));
|
package/dist/service/errors.d.ts
CHANGED
|
@@ -42,6 +42,7 @@ export interface ErrorResponse {
|
|
|
42
42
|
readonly headers?: Readonly<Record<string, string>>;
|
|
43
43
|
}
|
|
44
44
|
export declare function toServerResponse(response: ErrorResponse): HttpServerResponse.HttpServerResponse;
|
|
45
|
+
export declare function errorBodyForWire(body: ErrorBody): Readonly<Record<string, unknown>>;
|
|
45
46
|
/** Issues of a schema failure, as `[{ path, message }]`. */
|
|
46
47
|
export declare function schemaIssues(issue: SchemaIssue.Issue): Array<ValidationIssue>;
|
|
47
48
|
export declare function unauthorized(requestId: string, detail: string): ErrorResponse;
|
package/dist/service/errors.js
CHANGED
|
@@ -21,11 +21,15 @@ export class InvalidRequestBody extends Schema.TaggedError()("InvalidRequestBody
|
|
|
21
21
|
export class SignInCheckLimited extends Schema.TaggedError()("SignInCheckLimited", { retryAfterSeconds: Schema.Number }) {
|
|
22
22
|
}
|
|
23
23
|
export function toServerResponse(response) {
|
|
24
|
-
return HttpServerResponse.jsonUnsafe(response.body, {
|
|
24
|
+
return HttpServerResponse.jsonUnsafe(errorBodyForWire(response.body), {
|
|
25
25
|
status: response.status,
|
|
26
26
|
...(response.headers !== undefined ? { headers: response.headers } : {}),
|
|
27
27
|
});
|
|
28
28
|
}
|
|
29
|
+
export function errorBodyForWire(body) {
|
|
30
|
+
const { requestId, ...rest } = body;
|
|
31
|
+
return { ...rest, request_id: requestId };
|
|
32
|
+
}
|
|
29
33
|
/** Issues of a schema failure, as `[{ path, message }]`. */
|
|
30
34
|
export function schemaIssues(issue) {
|
|
31
35
|
const formatted = SchemaIssue.makeFormatterStandardSchemaV1()(issue);
|
package/dist/service/handlers.js
CHANGED
|
@@ -84,7 +84,7 @@ function requireRead(entities) {
|
|
|
84
84
|
}
|
|
85
85
|
/**
|
|
86
86
|
* Follows a relationship from one record with the query language: the record's own
|
|
87
|
-
* `<relationship>
|
|
87
|
+
* `<relationship>_id` for a `rel.one`, and for everything to-many a relationship path from the
|
|
88
88
|
* target back to the record (`lead.notes` is notes whose `lead` is the lead, `lead.tags` is tags
|
|
89
89
|
* some of whose `leads` is the lead), combined with the caller's query and paged like any find.
|
|
90
90
|
*/
|
|
@@ -96,7 +96,7 @@ function traverse(planned, traversal, request, targets) {
|
|
|
96
96
|
yield* requireRead([planned.name, ...queryReads(traversal.target, query, targets)]);
|
|
97
97
|
const source = yield* backend.get({ entity: planned.name, id });
|
|
98
98
|
if (traversal.kind === "one") {
|
|
99
|
-
const targetId = source[`${traversal.name}
|
|
99
|
+
const targetId = source[`${traversal.name}_id`];
|
|
100
100
|
if (typeof targetId !== "string") {
|
|
101
101
|
return { items: [], nextCursor: null };
|
|
102
102
|
}
|
|
@@ -35,7 +35,7 @@ export interface RunActionInput {
|
|
|
35
35
|
readonly connections?: ConnectionTransport["Service"];
|
|
36
36
|
}
|
|
37
37
|
/**
|
|
38
|
-
* `generatedActions(entity, ontology)`: `create`, `update`, `delete`, `
|
|
38
|
+
* `generatedActions(entity, ontology)`: `create`, `update`, `delete`, `upsert_by_<property>`, `link`
|
|
39
39
|
* and `unlink`, as `src/runtime/generated-actions.ts` derives them.
|
|
40
40
|
*/
|
|
41
41
|
export type GeneratedActionsProvider = (entity: ResolvedEntity, ontology: Ontology) => Readonly<Record<string, AnyAction>>;
|