typion 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +4 -2
  3. package/bin/typion.js +7 -0
  4. package/dist/auth/better-auth.d.ts +13 -0
  5. package/dist/auth/better-auth.js +11 -0
  6. package/dist/auth/better-auth.js.map +1 -0
  7. package/dist/cli/index.d.ts +14 -0
  8. package/dist/cli/index.js +883 -0
  9. package/dist/cli/index.js.map +1 -0
  10. package/dist/column-kinds-Bkm4NJI2.js +47 -0
  11. package/dist/column-kinds-Bkm4NJI2.js.map +1 -0
  12. package/dist/context-B4e2eREP.d.ts +2100 -0
  13. package/dist/contract/index.d.ts +137 -0
  14. package/dist/contract/index.js +42 -0
  15. package/dist/contract/index.js.map +1 -0
  16. package/dist/db/pglite.d.ts +12 -0
  17. package/dist/db/pglite.js +27 -0
  18. package/dist/db/pglite.js.map +1 -0
  19. package/dist/descriptor-Cc3mGp73.js +12 -0
  20. package/dist/descriptor-Cc3mGp73.js.map +1 -0
  21. package/dist/errors-ChDBxeS-.js +17 -0
  22. package/dist/errors-ChDBxeS-.js.map +1 -0
  23. package/dist/errors-D-CKO-px.js +114 -0
  24. package/dist/errors-D-CKO-px.js.map +1 -0
  25. package/dist/handler-BWVU8TCR.d.ts +23 -0
  26. package/dist/handler-_87bFkbP.js +565 -0
  27. package/dist/handler-_87bFkbP.js.map +1 -0
  28. package/dist/hono/index.d.ts +17 -0
  29. package/dist/hono/index.js +26 -0
  30. package/dist/hono/index.js.map +1 -0
  31. package/dist/http/index.d.ts +13 -0
  32. package/dist/http/index.js +2 -0
  33. package/dist/index.d.ts +2 -0
  34. package/dist/index.js +1324 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/issues-DLqCBQT1.js +42 -0
  37. package/dist/issues-DLqCBQT1.js.map +1 -0
  38. package/dist/next/index.d.ts +8 -0
  39. package/dist/next/index.js +18 -0
  40. package/dist/next/index.js.map +1 -0
  41. package/dist/operations-D3rOacLX.js +1811 -0
  42. package/dist/operations-D3rOacLX.js.map +1 -0
  43. package/dist/schema/index.d.ts +18 -0
  44. package/dist/schema/index.js +3 -0
  45. package/dist/schema-Rzzq97cK.js +68 -0
  46. package/dist/schema-Rzzq97cK.js.map +1 -0
  47. package/dist/seed/index.d.ts +19 -0
  48. package/dist/seed/index.js +85 -0
  49. package/dist/seed/index.js.map +1 -0
  50. package/dist/structural-BZnjFmCs.js +247 -0
  51. package/dist/structural-BZnjFmCs.js.map +1 -0
  52. package/dist/testing/index.d.ts +26 -0
  53. package/dist/testing/index.js +92 -0
  54. package/dist/testing/index.js.map +1 -0
  55. package/dist/typion-Dh2BCSRz.js +1187 -0
  56. package/dist/typion-Dh2BCSRz.js.map +1 -0
  57. package/dist/validation/zod.d.ts +21 -0
  58. package/dist/validation/zod.js +84 -0
  59. package/dist/validation/zod.js.map +1 -0
  60. package/docs/README.md +145 -0
  61. package/docs/access.md +114 -0
  62. package/docs/auth.md +195 -0
  63. package/docs/collections.md +172 -0
  64. package/docs/database.md +76 -0
  65. package/docs/features/localization.md +112 -0
  66. package/docs/features/multi-tenant.md +166 -0
  67. package/docs/features/soft-delete.md +85 -0
  68. package/docs/features/versions.md +106 -0
  69. package/docs/hooks.md +106 -0
  70. package/docs/http.md +188 -0
  71. package/docs/queries.md +200 -0
  72. package/docs/testing.md +85 -0
  73. package/package.json +125 -3
@@ -0,0 +1,1811 @@
1
+ import { a as InternalError, d as ValidationError, i as ForbiddenError, n as ConfigError, p as isTypionError, r as ConflictError, s as NotFoundError, t as BadRequestError, u as UnsupportedError } from "./errors-D-CKO-px.js";
2
+ import { r as readColumnKind } from "./column-kinds-Bkm4NJI2.js";
3
+ import { i as normalizeIssues } from "./issues-DLqCBQT1.js";
4
+ import { PgTable, getTableConfig } from "drizzle-orm/pg-core";
5
+ import { DrizzleError, SQL, TransactionRollbackError, and, eq, getTableColumns, getTableName, gt, gte, ilike, inArray, is, isNotNull, isNull, like, lt, lte, ne, not, notIlike, notInArray, notLike, or, relationsFilterToSQL, sql } from "drizzle-orm";
6
+ //#region src/define/plugin.ts
7
+ const PLUGIN_ID = /^[a-z][a-zA-Z0-9_]*$/;
8
+ const SETUP_FEATURES = /* @__PURE__ */ new Set(["localization", "multiTenant"]);
9
+ function defineFeaturePlugin(spec) {
10
+ if (typeof spec.id !== "string" || !PLUGIN_ID.test(spec.id)) throw new ConfigError(`definePlugin: '${String(spec.id)}' is not a valid plugin id (letters, digits, underscore; lowercase first letter)`);
11
+ for (const [key, table] of Object.entries(spec.tables ?? {})) if (!is(table, PgTable)) throw new ConfigError(`Plugin '${spec.id}': tables.${key} is not a pg table`);
12
+ for (const [feature, tables] of Object.entries(spec.featureTables ?? {})) {
13
+ if (!SETUP_FEATURES.has(feature)) throw new ConfigError(`Plugin '${spec.id}': featureTables.${feature} is not a feature a setup configures (${[...SETUP_FEATURES].join(", ")})`);
14
+ for (const [key, table] of Object.entries(tables)) if (!(key in (spec.tables ?? {})) || !is(table, PgTable)) throw new ConfigError(`Plugin '${spec.id}': featureTables.${feature}.${key} is not a pg table standing for one of its tables`);
15
+ }
16
+ return spec;
17
+ }
18
+ function definePlugin(spec) {
19
+ if (spec.traits !== void 0) throw new ConfigError(`Plugin '${String(spec.id)}': entity traits are the core's; a plugin adds and reacts (ADR-077)`);
20
+ return defineFeaturePlugin(spec);
21
+ }
22
+ function assertRequirements(spec, hostTables) {
23
+ for (const [key, shape] of Object.entries(spec.requires ?? {})) {
24
+ const host = hostTables[key];
25
+ if (!is(host, PgTable)) throw new ConfigError(`Plugin '${spec.id}' requires a table '${key}' that the engine's tables do not provide`);
26
+ if (!is(shape, PgTable)) continue;
27
+ const hostColumns = getTableColumns(host);
28
+ for (const column of Object.keys(getTableColumns(shape))) if (!(column in hostColumns)) throw new ConfigError(`Plugin '${spec.id}' requires column '${column}' on table '${key}', which the engine's table lacks`);
29
+ }
30
+ }
31
+ function assertDependencies(spec, before, installed) {
32
+ for (const needed of spec.dependsOn ?? []) {
33
+ if (needed === spec.id) throw new ConfigError(`Plugin '${spec.id}' cannot depend on itself`);
34
+ if (before.has(needed)) continue;
35
+ throw new ConfigError(installed.has(needed) ? `Plugin '${spec.id}' needs plugin '${needed}' installed before it` : `Plugin '${spec.id}' needs plugin '${needed}', which is not installed`);
36
+ }
37
+ }
38
+ function checkDependencies(plugins) {
39
+ const installed = new Set(plugins.map((plugin) => plugin.id));
40
+ const before = /* @__PURE__ */ new Set();
41
+ for (const plugin of plugins) {
42
+ assertDependencies(plugin, before, installed);
43
+ before.add(plugin.id);
44
+ }
45
+ }
46
+ function collectEntities(spec) {
47
+ if (spec.entities === void 0) return {};
48
+ const tables = spec.tables ?? {};
49
+ const define = (name, options) => {
50
+ if (!(name in tables)) throw new ConfigError(`Plugin '${spec.id}' defines entity '${name}' but owns no table with that key`);
51
+ return {
52
+ name,
53
+ options: options ?? {}
54
+ };
55
+ };
56
+ const entities = spec.entities(define);
57
+ for (const [key, def] of Object.entries(entities)) if (def.name !== key) throw new ConfigError(`Plugin '${spec.id}': entity registered as '${key}' is defined as '${def.name}'`);
58
+ return entities;
59
+ }
60
+ function resolvePlugins(plugins, hostTables) {
61
+ const seen = /* @__PURE__ */ new Set();
62
+ const installed = new Set(plugins.map((plugin) => plugin.id));
63
+ const claimedTables = /* @__PURE__ */ new Map();
64
+ const runtimes = [];
65
+ for (const plugin of plugins) {
66
+ const spec = plugin;
67
+ if (seen.has(spec.id)) throw new ConfigError(`Duplicate plugin id '${spec.id}'`);
68
+ assertDependencies(spec, seen, installed);
69
+ seen.add(spec.id);
70
+ assertRequirements(spec, hostTables);
71
+ const tables = { ...spec.tables };
72
+ for (const [feature, variants] of Object.entries(spec.featureTables ?? {})) if (installed.has(feature)) Object.assign(tables, variants);
73
+ for (const key of Object.keys(tables)) {
74
+ if (key in hostTables) throw new ConfigError(`Plugin '${spec.id}' table '${key}' collides with a table of the engine`);
75
+ const owner = claimedTables.get(key);
76
+ if (owner !== void 0) throw new ConfigError(`Plugin '${spec.id}' table '${key}' collides with a table of plugin '${owner}'`);
77
+ claimedTables.set(key, spec.id);
78
+ }
79
+ runtimes.push({
80
+ id: spec.id,
81
+ spec,
82
+ columnKinds: spec.columnKinds ?? null,
83
+ traits: spec.traits ?? null,
84
+ tables,
85
+ entities: collectEntities(spec),
86
+ hooks: spec.hooks ?? null,
87
+ routes: spec.routes ?? [],
88
+ errors: spec.errors ?? {}
89
+ });
90
+ }
91
+ columnKindsOf(runtimes);
92
+ traitOperations(runtimes);
93
+ traitRules(runtimes);
94
+ return runtimes;
95
+ }
96
+ const CORE_OPS = /* @__PURE__ */ new Set([
97
+ "find",
98
+ "findOne",
99
+ "findById",
100
+ "count",
101
+ "page",
102
+ "create",
103
+ "update",
104
+ "updateMany",
105
+ "delete",
106
+ "deleteMany",
107
+ "query",
108
+ "queryOne",
109
+ "run",
110
+ "schemas"
111
+ ]);
112
+ const byPlugins = /* @__PURE__ */ new WeakMap();
113
+ function traitOperations(plugins) {
114
+ let operations = byPlugins.get(plugins);
115
+ if (operations !== void 0) return operations;
116
+ const out = /* @__PURE__ */ new Map();
117
+ for (const plugin of plugins) for (const [name, spec] of Object.entries(plugin.traits?.operations ?? {})) {
118
+ const path = name.split(".");
119
+ if (path.some((part) => part === "") || CORE_OPS.has(path[0])) throw new ConfigError(`Plugin '${plugin.id}': '${name}' cannot name an operation`);
120
+ const owner = out.get(name);
121
+ if (owner !== void 0) throw new ConfigError(`Operation '${name}' is added by both plugin '${owner.plugin.id}' and plugin '${plugin.id}'`);
122
+ for (const other of out.keys()) if (other.startsWith(`${name}.`) || name.startsWith(`${other}.`)) throw new ConfigError(`Operation '${name}' and operation '${other}' cannot both exist: one would hide the other`);
123
+ out.set(name, {
124
+ name,
125
+ plugin,
126
+ spec
127
+ });
128
+ }
129
+ operations = out;
130
+ byPlugins.set(plugins, operations);
131
+ return operations;
132
+ }
133
+ const CORE_RULES = /* @__PURE__ */ new Map([
134
+ ["read", "filter"],
135
+ ["create", "boolean"],
136
+ ["update", "filter"],
137
+ ["delete", "filter"]
138
+ ]);
139
+ const rulesByPlugins = /* @__PURE__ */ new WeakMap();
140
+ function traitRules(plugins) {
141
+ let rules = rulesByPlugins.get(plugins);
142
+ if (rules !== void 0) return rules;
143
+ const out = /* @__PURE__ */ new Map();
144
+ for (const plugin of plugins) for (const [name, kind] of Object.entries(plugin.traits?.rules ?? {})) {
145
+ if (CORE_RULES.has(name)) throw new ConfigError(`Plugin '${plugin.id}': rule '${name}' is one of the engine's`);
146
+ if (kind !== "filter" && kind !== "boolean") throw new ConfigError(`Plugin '${plugin.id}': rule '${name}' must be 'filter' or 'boolean'`);
147
+ const owner = out.get(name);
148
+ if (owner !== void 0) throw new ConfigError(`Rule '${name}' is added by both plugin '${owner.plugin}' and plugin '${plugin.id}'`);
149
+ out.set(name, {
150
+ name,
151
+ plugin: plugin.id,
152
+ kind
153
+ });
154
+ }
155
+ rules = out;
156
+ rulesByPlugins.set(plugins, rules);
157
+ return rules;
158
+ }
159
+ const kindsByPlugins = /* @__PURE__ */ new WeakMap();
160
+ function columnKindsOf(runtimes) {
161
+ let kinds = kindsByPlugins.get(runtimes);
162
+ if (kinds !== void 0) return kinds;
163
+ kinds = /* @__PURE__ */ new Map();
164
+ for (const runtime of runtimes) for (const [id, spec] of Object.entries(runtime.columnKinds?.kinds ?? {})) {
165
+ const owner = kinds.get(id);
166
+ if (owner !== void 0) throw new ConfigError(`Column kind '${id}' is handled by both plugin '${owner.plugin}' and plugin '${runtime.id}'`);
167
+ kinds.set(id, {
168
+ ...spec,
169
+ id,
170
+ plugin: runtime.id
171
+ });
172
+ }
173
+ kindsByPlugins.set(runtimes, kinds);
174
+ return kinds;
175
+ }
176
+ function isPromise(value) {
177
+ return typeof value?.then === "function";
178
+ }
179
+ function validatePluginEntityOptions(runtimes, entity, pluginOptions) {
180
+ for (const runtime of runtimes) {
181
+ const schema = runtime.spec.entityOptions?.schema;
182
+ if (schema === void 0) continue;
183
+ const result = schema["~standard"].validate(pluginOptions);
184
+ if (isPromise(result)) throw new ConfigError(`Plugin '${runtime.id}': entityOptions.schema must validate synchronously (entity '${entity}')`);
185
+ if (result.issues !== void 0 && result.issues.length > 0) throw new ConfigError(`Entity '${entity}': invalid options for plugin '${runtime.id}': ${result.issues.map((issue) => issue.message).join("; ")}`, "config", {
186
+ entity,
187
+ details: {
188
+ plugin: runtime.id,
189
+ issues: result.issues
190
+ }
191
+ });
192
+ }
193
+ }
194
+ //#endregion
195
+ //#region src/define/entity.ts
196
+ const ALLOW_ALL = Symbol.for("typion.allowAll");
197
+ function allowAll() {
198
+ return {
199
+ read: true,
200
+ create: true,
201
+ update: true,
202
+ delete: true,
203
+ [ALLOW_ALL]: true
204
+ };
205
+ }
206
+ function defineEntity(name, options) {
207
+ return {
208
+ name,
209
+ options: options ?? {}
210
+ };
211
+ }
212
+ const CORE_OPTION_KEYS = /* @__PURE__ */ new Set([
213
+ "idColumn",
214
+ "fields",
215
+ "access",
216
+ "hooks",
217
+ "validate",
218
+ "defaultOrder",
219
+ "maxLimit",
220
+ "links"
221
+ ]);
222
+ function fail(entity, message) {
223
+ throw new ConfigError(`Entity '${entity}': ${message}`, "config", { entity });
224
+ }
225
+ function resolvePrimaryKey(entity, table, columns, idColumn) {
226
+ const tableLevel = getTableConfig(table).primaryKeys.flatMap((pk) => pk.columns);
227
+ const columnLevel = Object.entries(columns).filter(([, c]) => c.primary);
228
+ if (tableLevel.length + columnLevel.length > 1 || tableLevel.length > 1) fail(entity, "composite primary keys are not supported (v1 requires a single-column pk)");
229
+ const pkColumn = columnLevel[0]?.[1] ?? tableLevel[0];
230
+ if (pkColumn === void 0) fail(entity, `table '${getTableName(table)}' has no primary key`);
231
+ const pkKey = Object.entries(columns).find(([, c]) => c === pkColumn)?.[0];
232
+ if (pkKey !== idColumn) fail(entity, `the primary key is '${pkKey}', not '${idColumn}'; set idColumn: '${pkKey}' on the entity`);
233
+ return idColumn;
234
+ }
235
+ const ALL_GROUPS = [
236
+ "read",
237
+ "create",
238
+ "update",
239
+ "delete"
240
+ ];
241
+ const NO_TABLES = {};
242
+ function resolveTraits(name, table, options, columns, plugins, derived) {
243
+ const traits = {};
244
+ const managed = [];
245
+ for (const plugin of plugins) {
246
+ const spec = plugin.traits;
247
+ if (spec === null) continue;
248
+ const tables = derived.get(plugin.id) ?? NO_TABLES;
249
+ const resolved = spec.resolve === void 0 ? {} : spec.resolve({
250
+ entity: name,
251
+ table,
252
+ columns,
253
+ options,
254
+ derived: tables
255
+ });
256
+ if (resolved === null) {
257
+ if (tables !== NO_TABLES) fail(name, `plugin '${plugin.id}' derived tables for it but does not act on it`);
258
+ continue;
259
+ }
260
+ for (const key of resolved.managed ?? []) {
261
+ if (!(key in columns)) fail(name, `plugin '${plugin.id}' manages '${key}', which is not a column`);
262
+ if (!managed.includes(key)) managed.push(key);
263
+ }
264
+ const own = Object.keys(spec.rules ?? {});
265
+ for (const rule of resolved.rules ?? []) if (!own.includes(rule)) fail(name, `plugin '${plugin.id}' has no rule '${rule}'`);
266
+ traits[plugin.id] = {
267
+ plugin: plugin.id,
268
+ state: resolved.state,
269
+ options: resolved.options ?? ALL_GROUPS,
270
+ rules: resolved.rules ?? own
271
+ };
272
+ }
273
+ return {
274
+ traits,
275
+ managed
276
+ };
277
+ }
278
+ function checkAccess(name, access, traits, plugins) {
279
+ const rules = traitRules(plugins);
280
+ for (const key of Object.keys(access)) {
281
+ if (CORE_RULES.has(key)) continue;
282
+ const rule = rules.get(key);
283
+ if (rule === void 0) fail(name, `access.${key} is not a rule`);
284
+ if (!traits[rule.plugin]?.rules.includes(key)) fail(name, `access.${key} is a rule of plugin '${rule.plugin}', which does not give it to this entity`);
285
+ }
286
+ }
287
+ function relationMeta(relations, entity) {
288
+ const out = {};
289
+ for (const [key, rel] of Object.entries(relations[entity]?.relations ?? {})) out[key] = {
290
+ key,
291
+ target: rel.targetTableName,
292
+ type: rel.relationType,
293
+ optional: rel.relationType === "one" ? rel.optional ?? true : false,
294
+ through: rel.through !== void 0
295
+ };
296
+ return out;
297
+ }
298
+ function buildEntityMeta(input) {
299
+ const { name, table } = input;
300
+ if (typeof input.options !== "object" || input.options === null) fail(name, "options must be an object");
301
+ const options = input.options;
302
+ const columns = getTableColumns(table);
303
+ const pkKey = resolvePrimaryKey(name, table, columns, options.idColumn ?? "id");
304
+ const { traits, managed: managedKeys } = resolveTraits(name, table, options, columns, input.plugins, input.derived);
305
+ const managed = new Set(managedKeys);
306
+ const access = options.access ?? {};
307
+ checkAccess(name, access, traits, input.plugins);
308
+ const fields = options.fields ?? {};
309
+ for (const key of Object.keys(fields)) if (!(key in columns)) fail(name, `fields.${key} does not name a column`);
310
+ const kinds = columnKindsOf(input.plugins);
311
+ const columnMeta = {};
312
+ for (const [key, column] of Object.entries(columns)) {
313
+ const field = fields[key] ?? {};
314
+ const read = field.read === void 0 ? true : field.read;
315
+ const write = field.write !== void 0 ? field.write : managed.has(key) ? "system" : "user";
316
+ columnMeta[key] = {
317
+ key,
318
+ name: column.name,
319
+ column,
320
+ dataType: column.dataType,
321
+ columnType: column.columnType,
322
+ notNull: column.notNull,
323
+ hasDefault: column.hasDefault,
324
+ generated: column.generated !== void 0 || column.generatedIdentity !== void 0,
325
+ enumValues: column.enumValues,
326
+ kind: field.kind ?? readColumnKind(column),
327
+ columnKind: kinds.get(readColumnKind(column) ?? "") ?? null,
328
+ read,
329
+ write
330
+ };
331
+ }
332
+ const pk = columnMeta[pkKey];
333
+ if (pk.read === false) fail(name, `the primary key '${pkKey}' cannot be hidden`);
334
+ const defaultOrder = options.defaultOrder ?? null;
335
+ if (defaultOrder !== null) {
336
+ for (const key of Object.keys(defaultOrder)) if (!(key in columns)) fail(name, `defaultOrder.${key} does not name a column`);
337
+ }
338
+ if (options.maxLimit !== void 0 && (!Number.isInteger(options.maxLimit) || options.maxLimit < 1)) fail(name, "maxLimit must be a positive integer");
339
+ const relations = relationMeta(input.relations, name);
340
+ for (const key of options.links ?? []) if (relations[key]?.through !== true) fail(name, `links: '${key}' is not a many-to-many relation through a table`);
341
+ const pluginOptions = {};
342
+ for (const [key, value] of Object.entries(options)) if (!CORE_OPTION_KEYS.has(key)) pluginOptions[key] = value;
343
+ validatePluginEntityOptions(input.plugins, name, pluginOptions);
344
+ const config = getTableConfig(table);
345
+ return {
346
+ name,
347
+ table,
348
+ tableName: config.name,
349
+ pgSchema: config.schema,
350
+ pk,
351
+ columns: columnMeta,
352
+ visibleKeys: Object.keys(columnMeta).filter((k) => columnMeta[k]?.read !== false),
353
+ kindKeys: Object.keys(columnMeta).filter((k) => columnMeta[k]?.columnKind !== null),
354
+ dynamicReadKeys: Object.keys(columnMeta).filter((k) => typeof columnMeta[k]?.read === "function"),
355
+ managedKeys,
356
+ relations,
357
+ links: new Set(options.links ?? []),
358
+ traits,
359
+ access,
360
+ hooks: options.hooks ?? {},
361
+ validate: options.validate ?? null,
362
+ defaultOrder,
363
+ maxLimit: options.maxLimit ?? input.maxLimit,
364
+ pluginOptions,
365
+ owner: input.owner
366
+ };
367
+ }
368
+ //#endregion
369
+ //#region src/runtime/context.ts
370
+ var CommitQueue = class {
371
+ onError;
372
+ items = [];
373
+ constructor(onError) {
374
+ this.onError = onError;
375
+ }
376
+ get size() {
377
+ return this.items.length;
378
+ }
379
+ push(fn) {
380
+ this.items.push(fn);
381
+ }
382
+ mark() {
383
+ return this.items.length;
384
+ }
385
+ truncate(mark) {
386
+ this.items.length = Math.min(this.items.length, mark);
387
+ }
388
+ async flush() {
389
+ const items = this.items;
390
+ this.items = [];
391
+ for (const fn of items) try {
392
+ await fn();
393
+ } catch (error) {
394
+ this.onError(error);
395
+ }
396
+ }
397
+ discard() {
398
+ this.items = [];
399
+ }
400
+ };
401
+ function buildContext(parts) {
402
+ const user = parts.user;
403
+ const actorId = typeof user.actorId === "string" ? user.actorId : null;
404
+ const userMeta = user.meta;
405
+ const meta = parts.meta ?? (typeof userMeta === "object" && userMeta !== null ? userMeta : {});
406
+ return {
407
+ ...user,
408
+ mode: parts.mode,
409
+ now: parts.now,
410
+ requestId: crypto.randomUUID(),
411
+ request: parts.request,
412
+ actorId,
413
+ systemReason: parts.systemReason,
414
+ meta
415
+ };
416
+ }
417
+ function operationMeta(ctx, entity, operation, now) {
418
+ return {
419
+ entity,
420
+ operation,
421
+ mode: ctx.mode,
422
+ requestId: ctx.requestId,
423
+ now,
424
+ systemReason: ctx.systemReason
425
+ };
426
+ }
427
+ //#endregion
428
+ //#region src/runtime/errors.ts
429
+ function normalizeError(runtime, error) {
430
+ if (isTypionError(error) || error instanceof TransactionRollbackError) return error;
431
+ if (error instanceof DrizzleError && !error.query) {
432
+ if (/relational filter field|not enough data provided|relation/i.test(error.message)) return new BadRequestError(error.message, { cause: error });
433
+ }
434
+ const mapped = runtime.dialect.mapError(error);
435
+ return mapped instanceof ConflictError ? described(runtime, mapped) : mapped ?? error;
436
+ }
437
+ function constraintKeys(entity, constraint) {
438
+ const columns = Object.entries(getTableColumns(entity.table));
439
+ const keyOf = (name) => columns.find(([, column]) => column.name === name)?.[0];
440
+ const keys = (names) => names.map(keyOf).filter((key) => key !== void 0);
441
+ const config = getTableConfig(entity.table);
442
+ const index = config.indexes.find((i) => i.config.name === constraint);
443
+ if (index !== void 0) return keys(index.config.columns.map((c) => c?.name));
444
+ const unique = config.uniqueConstraints.find((u) => u.getName() === constraint);
445
+ if (unique !== void 0) return keys(unique.columns.map((c) => c.name));
446
+ const foreign = config.foreignKeys.find((f) => f.getName() === constraint);
447
+ if (foreign !== void 0) return keys(foreign.reference().columns.map((c) => c.name));
448
+ const own = columns.find(([, column]) => column.isUnique && (column.uniqueName === constraint || column.uniqueName === void 0 && (constraint === `${config.name}_${column.name}_unique` || constraint === `${config.name}_${column.name}_key`)));
449
+ return own === void 0 ? [] : [own[0]];
450
+ }
451
+ function described(runtime, error) {
452
+ if (error.kind !== "unique" && error.kind !== "foreign_key" || error.constraint === void 0) return error;
453
+ const table = error.details?.table;
454
+ const entity = Object.values(runtime.def.entities).find((e) => e.tableName === table);
455
+ if (entity === void 0) return error;
456
+ const covered = constraintKeys(entity, error.constraint);
457
+ const written = covered.filter((key) => !entity.managedKeys.includes(key));
458
+ const keys = written.length > 0 ? written : covered;
459
+ const what = keys.length === 0 ? `'${error.constraint}'` : keys.join(", ");
460
+ const unique = error.kind === "unique";
461
+ return new ConflictError(unique ? `Entity '${entity.name}': a row with this ${what} exists already` : `Entity '${entity.name}': a reference does not hold (${what})`, error.kind, {
462
+ entity: entity.name,
463
+ constraint: error.constraint,
464
+ details: {
465
+ sqlstate: error.details?.sqlstate,
466
+ constraint: error.constraint
467
+ },
468
+ cause: error.cause,
469
+ ...keys.length === 0 ? {} : { issues: [{
470
+ path: [keys[0]],
471
+ message: unique ? "already exists" : "does not hold"
472
+ }] }
473
+ });
474
+ }
475
+ async function guard(runtime, fn) {
476
+ try {
477
+ return await fn();
478
+ } catch (error) {
479
+ throw normalizeError(runtime, error);
480
+ }
481
+ }
482
+ //#endregion
483
+ //#region src/runtime/filters.ts
484
+ const FALSE_FILTER = { RAW: sql`false` };
485
+ function isPlainObject(value) {
486
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
487
+ const proto = Object.getPrototypeOf(value);
488
+ return proto === Object.prototype || proto === null;
489
+ }
490
+ function isEmptyFilter(filter) {
491
+ return filter === void 0 || Object.values(filter).every((v) => v === void 0);
492
+ }
493
+ function andFilters(parts) {
494
+ const present = parts.filter((p) => !isEmptyFilter(p));
495
+ if (present.length === 0) return void 0;
496
+ if (present.length === 1) return present[0];
497
+ return { AND: present };
498
+ }
499
+ function asRelationsFilter(filter) {
500
+ return filter;
501
+ }
502
+ //#endregion
503
+ //#region src/runtime/read/kinds.ts
504
+ const optionsByPlugins = /* @__PURE__ */ new WeakMap();
505
+ function pluginOptions(plugins) {
506
+ let options = optionsByPlugins.get(plugins);
507
+ if (options === void 0) {
508
+ const read = /* @__PURE__ */ new Map();
509
+ const write = /* @__PURE__ */ new Map();
510
+ const create = /* @__PURE__ */ new Map();
511
+ const update = /* @__PURE__ */ new Map();
512
+ const del = /* @__PURE__ */ new Map();
513
+ for (const plugin of plugins) {
514
+ for (const [key, type] of Object.entries(plugin.columnKinds?.readOptions ?? {})) read.set(key, type);
515
+ for (const [key, type] of Object.entries(plugin.columnKinds?.writeOptions ?? {})) write.set(key, type);
516
+ const traits = plugin.traits?.options;
517
+ for (const [group, target] of [
518
+ ["read", read],
519
+ ["create", create],
520
+ ["update", update],
521
+ ["delete", del]
522
+ ]) for (const [key, type] of Object.entries(traits?.[group] ?? {})) target.set(key, type);
523
+ }
524
+ options = {
525
+ read,
526
+ write,
527
+ create,
528
+ update,
529
+ delete: del
530
+ };
531
+ optionsByPlugins.set(plugins, options);
532
+ }
533
+ return options;
534
+ }
535
+ function checkOptionType(key, type, value) {
536
+ if (value !== void 0 && typeof value !== type) throw new BadRequestError(`${key} must be a ${type}`);
537
+ }
538
+ function traitOptions(entity, plugins, group, given) {
539
+ const out = /* @__PURE__ */ new Map();
540
+ for (const plugin of plugins) {
541
+ const keys = plugin.traits?.options?.[group];
542
+ if (keys === void 0) continue;
543
+ const values = {};
544
+ for (const [key, type] of Object.entries(keys)) {
545
+ checkOptionType(key, type, given[key]);
546
+ if (given[key] !== void 0) values[key] = given[key];
547
+ }
548
+ const names = Object.keys(values);
549
+ if (names.length === 0) continue;
550
+ const trait = entity.traits[plugin.id];
551
+ if (trait === void 0 || !trait.options.includes(group)) throw new UnsupportedError(`Entity '${entity.name}' does not take ${names.map((k) => `'${k}'`).join(", ")}: plugin '${plugin.id}' does not act on it`, { entity: entity.name });
552
+ out.set(plugin.id, values);
553
+ }
554
+ return out;
555
+ }
556
+ function kindPlugins(entity) {
557
+ const out = /* @__PURE__ */ new Set();
558
+ for (const key of entity.kindKeys) {
559
+ const kind = entity.columns[key]?.columnKind;
560
+ if (kind !== null && kind !== void 0) out.add(kind.plugin);
561
+ }
562
+ return out;
563
+ }
564
+ function writeStates(entity, plugins, ctx, given) {
565
+ const present = kindPlugins(entity);
566
+ const states = /* @__PURE__ */ new Map();
567
+ for (const plugin of plugins) {
568
+ const spec = plugin.columnKinds;
569
+ if (spec === void 0 || spec === null) continue;
570
+ const options = {};
571
+ for (const [key, type] of Object.entries(spec.writeOptions ?? {})) {
572
+ checkOptionType(key, type, given[key]);
573
+ if (given[key] !== void 0) options[key] = given[key];
574
+ }
575
+ if (!present.has(plugin.id)) {
576
+ const names = Object.keys(options);
577
+ if (names.length > 0) throw new UnsupportedError(`Entity '${entity.name}' does not take ${names.map((k) => `'${k}'`).join(", ")}: it has no columns of plugin '${plugin.id}'`, { entity: entity.name });
578
+ continue;
579
+ }
580
+ if (spec.write !== void 0) states.set(plugin.id, spec.write({
581
+ options,
582
+ ctx
583
+ }));
584
+ }
585
+ return states;
586
+ }
587
+ function readStates(plugins, ctx, config) {
588
+ const states = /* @__PURE__ */ new Map();
589
+ for (const plugin of plugins) {
590
+ const spec = plugin.columnKinds;
591
+ if (spec?.read === void 0) continue;
592
+ const options = {};
593
+ for (const [key, type] of Object.entries(spec.readOptions ?? {})) {
594
+ checkOptionType(key, type, config[key]);
595
+ if (config[key] !== void 0) options[key] = config[key];
596
+ }
597
+ states.set(plugin.id, spec.read({
598
+ options,
599
+ ctx
600
+ }));
601
+ }
602
+ return states;
603
+ }
604
+ function kindExpression(kind, column, states, dialect, sample) {
605
+ if (kind?.expression === void 0) return void 0;
606
+ return kind.expression({
607
+ column,
608
+ state: states.get(kind.plugin),
609
+ sample,
610
+ dialect
611
+ });
612
+ }
613
+ async function projectCells(cells, states) {
614
+ const byKind = /* @__PURE__ */ new Map();
615
+ for (const cell of cells) {
616
+ let list = byKind.get(cell.kind);
617
+ if (list === void 0) {
618
+ list = [];
619
+ byKind.set(cell.kind, list);
620
+ }
621
+ list.push(cell);
622
+ }
623
+ for (const [kind, list] of byKind) {
624
+ const project = kind.project;
625
+ if (project === void 0) continue;
626
+ const values = await project(list.map(({ row, key }) => row[key]), states.get(kind.plugin));
627
+ list.forEach(({ row, key }, i) => {
628
+ row[key] = values[i];
629
+ });
630
+ }
631
+ }
632
+ function assertScalar(key, value) {
633
+ if (isPlainObject(value) || Array.isArray(value)) throw new BadRequestError(`Filter on column '${key}': only primitive values can be compared through its kind`);
634
+ }
635
+ function asArray(key, op, value) {
636
+ if (!Array.isArray(value)) throw new BadRequestError(`Filter '${key}.${op}' expects an array`);
637
+ for (const item of value) assertScalar(key, item);
638
+ return value;
639
+ }
640
+ function asPattern(key, op, value) {
641
+ if (typeof value !== "string") throw new BadRequestError(`Filter '${key}.${op}' expects a string`);
642
+ return value;
643
+ }
644
+ function kindFilterSQL(expr, key, filter) {
645
+ const build = (f) => {
646
+ if (!isPlainObject(f)) {
647
+ if (f === null) throw new BadRequestError(`Filter '${key}': use { isNull: true } instead of null`);
648
+ assertScalar(key, f);
649
+ return eq(expr(f), f);
650
+ }
651
+ const parts = [];
652
+ for (const [op, value] of Object.entries(f)) {
653
+ if (value === void 0) continue;
654
+ switch (op) {
655
+ case "eq":
656
+ case "ne":
657
+ case "gt":
658
+ case "gte":
659
+ case "lt":
660
+ case "lte": {
661
+ assertScalar(key, value);
662
+ const fn = {
663
+ eq,
664
+ ne,
665
+ gt,
666
+ gte,
667
+ lt,
668
+ lte
669
+ }[op];
670
+ parts.push(fn(expr(value), value));
671
+ break;
672
+ }
673
+ case "in": {
674
+ const items = asArray(key, op, value);
675
+ parts.push(items.length === 0 ? sql`false` : inArray(expr(items[0]), items));
676
+ break;
677
+ }
678
+ case "notIn": {
679
+ const items = asArray(key, op, value);
680
+ if (items.length > 0) parts.push(notInArray(expr(items[0]), items));
681
+ break;
682
+ }
683
+ case "like":
684
+ parts.push(like(expr(null), asPattern(key, op, value)));
685
+ break;
686
+ case "ilike":
687
+ parts.push(ilike(expr(null), asPattern(key, op, value)));
688
+ break;
689
+ case "notLike":
690
+ parts.push(notLike(expr(null), asPattern(key, op, value)));
691
+ break;
692
+ case "notIlike":
693
+ parts.push(notIlike(expr(null), asPattern(key, op, value)));
694
+ break;
695
+ case "isNull":
696
+ if (value === true) parts.push(isNull(expr(null)));
697
+ break;
698
+ case "isNotNull":
699
+ if (value === true) parts.push(isNotNull(expr(null)));
700
+ break;
701
+ case "NOT": {
702
+ const inner = build(value);
703
+ if (inner !== void 0) parts.push(not(inner));
704
+ break;
705
+ }
706
+ case "OR":
707
+ case "AND": {
708
+ if (!Array.isArray(value)) throw new BadRequestError(`Filter '${key}.${op}' expects an array`);
709
+ const inner = value.map(build);
710
+ parts.push(op === "OR" ? or(...inner) : and(...inner));
711
+ break;
712
+ }
713
+ default: throw new BadRequestError(`Unknown filter operator '${op}' on '${key}'`);
714
+ }
715
+ }
716
+ return and(...parts);
717
+ };
718
+ return build(filter) ?? sql`true`;
719
+ }
720
+ //#endregion
721
+ //#region src/runtime/access.ts
722
+ const ALLOW = { kind: "allow" };
723
+ const DENY = { kind: "deny" };
724
+ function forbidden(entity, rule, meta, why) {
725
+ return new ForbiddenError(`Entity '${entity}': ${why}`, {
726
+ entity,
727
+ operation: meta.operation,
728
+ rule
729
+ });
730
+ }
731
+ function ruleKind(entity, rule, bound) {
732
+ const core = CORE_RULES.get(rule);
733
+ if (core !== void 0) return core;
734
+ const declared = traitRules(bound.runtime.plugins).get(rule);
735
+ if (declared === void 0 || !entity.traits[declared.plugin]?.rules.includes(rule)) throw new InternalError(`Entity '${entity.name}' has no rule '${rule}'`, { entity: entity.name });
736
+ return declared.kind;
737
+ }
738
+ async function evaluateRule(entity, rule, bound, meta, extra = {}) {
739
+ const kind = ruleKind(entity, rule, bound);
740
+ if (bound.$mode === "system") return ALLOW;
741
+ const access = entity.access;
742
+ const value = access[rule];
743
+ if (value === void 0) {
744
+ if (access[ALLOW_ALL] === true) return ALLOW;
745
+ throw forbidden(entity.name, rule, meta, `no access.${rule} rule, so '${meta.operation}' is denied in user mode (declare the rule or spread allowAll())`);
746
+ }
747
+ const result = typeof value === "function" ? await value({
748
+ ctx: bound.ctx,
749
+ meta,
750
+ typion: bound.systemTwin(`access.${rule} of '${entity.name}'`),
751
+ ...extra
752
+ }) : value;
753
+ if (result === true) return ALLOW;
754
+ if (result === false) return DENY;
755
+ if (isPlainObject(result) && kind === "filter") {
756
+ const states = readStates(bound.runtime.plugins, bound.ctx, {});
757
+ return {
758
+ kind: "filter",
759
+ filter: ruleFilter(bound, meta.operation, entity.name, result, states)
760
+ };
761
+ }
762
+ throw new InternalError(`Entity '${entity.name}': access.${rule} must return a boolean${kind === "filter" ? " or a filter object" : ""}`, { entity: entity.name });
763
+ }
764
+ function ruleFilter(bound, operation, tableKey, filter, states) {
765
+ const runtime = bound.runtime;
766
+ const entity = runtime.entityOrNull(tableKey);
767
+ const relations = runtime.relations[tableKey]?.relations ?? {};
768
+ const plain = {};
769
+ const extra = [];
770
+ for (const [key, value] of Object.entries(filter)) {
771
+ if (value === void 0) continue;
772
+ if ((key === "AND" || key === "OR") && Array.isArray(value)) {
773
+ plain[key] = value.map((part) => isPlainObject(part) ? ruleFilter(bound, operation, tableKey, part, states) : part);
774
+ continue;
775
+ }
776
+ if (key === "NOT" && isPlainObject(value)) {
777
+ plain.NOT = ruleFilter(bound, operation, tableKey, value, states);
778
+ continue;
779
+ }
780
+ const relation = relations[key];
781
+ if (relation !== void 0) {
782
+ const target = runtime.entityOrNull(relation.targetTableName);
783
+ const kept = target === null ? void 0 : pluginFilter(target, bound, operation, NO_TRAIT_OPTIONS, READING);
784
+ if (isPlainObject(value)) {
785
+ plain[key] = andFilters([kept, ruleFilter(bound, operation, relation.targetTableName, value, states)]) ?? {};
786
+ continue;
787
+ }
788
+ if (value === true) {
789
+ plain[key] = kept ?? true;
790
+ continue;
791
+ }
792
+ if (value === false) {
793
+ if (kept === void 0) plain[key] = false;
794
+ else extra.push({ NOT: { [key]: kept } });
795
+ continue;
796
+ }
797
+ }
798
+ const kind = entity?.columns[key]?.columnKind;
799
+ if (kind?.expression === void 0) {
800
+ plain[key] = value;
801
+ continue;
802
+ }
803
+ extra.push({ RAW: (t) => kindFilterSQL((sample) => kindExpression(kind, t[key], states, runtime.dialect, sample), key, value) });
804
+ }
805
+ return extra.length === 0 ? plain : { AND: [plain, ...extra] };
806
+ }
807
+ const UNPARTITIONED = /* @__PURE__ */ new Map();
808
+ function partitionOf(entity, bound, operation) {
809
+ let out;
810
+ for (const plugin of bound.runtime.plugins) {
811
+ const partition = plugin.traits?.partition;
812
+ const trait = entity.traits[plugin.id];
813
+ if (partition === void 0 || trait === void 0) continue;
814
+ const values = partition({
815
+ entity,
816
+ state: trait.state,
817
+ operation,
818
+ ctx: bound.ctx
819
+ });
820
+ if (values === void 0) continue;
821
+ for (const [column, value] of Object.entries(values)) {
822
+ if (!(column in entity.columns)) throw new InternalError(`Plugin '${plugin.id}' partitions '${entity.name}' by '${column}', which is not a column`, { entity: entity.name });
823
+ if (value === null || value === void 0) throw new ForbiddenError(`Entity '${entity.name}' is partitioned by '${column}', and the call has no value for it`, {
824
+ entity: entity.name,
825
+ operation,
826
+ rule: `partition:${plugin.id}`
827
+ });
828
+ out ??= /* @__PURE__ */ new Map();
829
+ out.set(column, {
830
+ value,
831
+ plugin: plugin.id
832
+ });
833
+ }
834
+ }
835
+ return out ?? UNPARTITIONED;
836
+ }
837
+ function partitionFilter(partition) {
838
+ if (partition.size === 0) return void 0;
839
+ const filter = {};
840
+ for (const [column, { value }] of partition) filter[column] = { eq: value };
841
+ return filter;
842
+ }
843
+ const READING = { writing: false };
844
+ const WRITING = { writing: true };
845
+ function pluginFilter(entity, bound, operation, options, use) {
846
+ return andFilters([partitionFilter(partitionOf(entity, bound, operation)), traitFilter(entity, bound, operation, options, use)]);
847
+ }
848
+ const NO_TRAIT_OPTIONS = /* @__PURE__ */ new Map();
849
+ const NO_OPTIONS = {};
850
+ function traitFilter(entity, bound, operation, options, use) {
851
+ const parts = [];
852
+ for (const plugin of bound.runtime.plugins) {
853
+ const filter = plugin.traits?.filter;
854
+ const trait = entity.traits[plugin.id];
855
+ if (filter === void 0 || trait === void 0 || plugin.id === use.skip) continue;
856
+ parts.push(filter({
857
+ entity,
858
+ state: trait.state,
859
+ operation,
860
+ writing: use.writing,
861
+ options: options.get(plugin.id) ?? NO_OPTIONS,
862
+ ctx: bound.ctx
863
+ }));
864
+ }
865
+ return andFilters(parts);
866
+ }
867
+ function filterToSQL(relations, tableKey, table, filter) {
868
+ if (filter === void 0) return void 0;
869
+ const tableRelations = relations[tableKey]?.relations ?? {};
870
+ return relationsFilterToSQL(table, asRelationsFilter(filter), tableRelations, relations);
871
+ }
872
+ function fieldReadDenied(entity, bound, meta) {
873
+ if (bound.$mode === "system") return [];
874
+ const denied = [];
875
+ for (const key of entity.dynamicReadKeys) {
876
+ const rule = entity.columns[key].read;
877
+ if (typeof rule === "function" && !rule({
878
+ ctx: bound.ctx,
879
+ meta
880
+ })) denied.push(key);
881
+ }
882
+ return denied;
883
+ }
884
+ function fieldWriteCheck(entity, bound, meta, data, allowed = /* @__PURE__ */ new Set()) {
885
+ for (const key of Object.keys(data)) {
886
+ if (data[key] === void 0 || allowed.has(key)) continue;
887
+ const column = entity.columns[key];
888
+ if (column === void 0) continue;
889
+ const write = column.write;
890
+ if (write === false || write === "system" && bound.$mode === "user" || typeof write === "function" && bound.$mode === "user" && !write({
891
+ ctx: bound.ctx,
892
+ meta
893
+ })) throw new ForbiddenError(`Entity '${entity.name}': column '${key}' is not writable here`, {
894
+ entity: entity.name,
895
+ operation: meta.operation,
896
+ rule: `fields.${key}.write`
897
+ });
898
+ }
899
+ }
900
+ //#endregion
901
+ //#region src/runtime/equal.ts
902
+ function deepEqual(a, b) {
903
+ if (Object.is(a, b)) return true;
904
+ if (a instanceof Date || b instanceof Date) return a instanceof Date && b instanceof Date && a.getTime() === b.getTime();
905
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
906
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
907
+ if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((item, i) => deepEqual(item, b[i]));
908
+ const left = a;
909
+ const right = b;
910
+ const keys = /* @__PURE__ */ new Set([...Object.keys(left), ...Object.keys(right)]);
911
+ for (const key of keys) if (!deepEqual(left[key], right[key])) return false;
912
+ return true;
913
+ }
914
+ //#endregion
915
+ //#region src/runtime/write/pipeline.ts
916
+ async function runOperation(bound, entity, operation, fn) {
917
+ const { runtime } = bound;
918
+ await runtime.ready();
919
+ const meta = operationMeta(bound.ctx, entity.name, operation, runtime.now());
920
+ const bag = /* @__PURE__ */ new Map();
921
+ try {
922
+ if (bound.$tx !== null && bound.queue !== null) {
923
+ const queue = bound.queue;
924
+ const mark = queue.mark();
925
+ try {
926
+ return await bound.$tx.transaction((tx) => fn({
927
+ bound: bound.joined(tx, queue),
928
+ tx,
929
+ meta,
930
+ bag,
931
+ queue,
932
+ entity
933
+ }));
934
+ } catch (error) {
935
+ queue.truncate(mark);
936
+ throw error;
937
+ }
938
+ }
939
+ const queue = new CommitQueue((error) => runtime.afterCommitError(error, {
940
+ entity: entity.name,
941
+ operation,
942
+ requestId: bound.ctx.requestId
943
+ }));
944
+ let result;
945
+ try {
946
+ result = await bound.$executor.transaction((tx) => fn({
947
+ bound: bound.joined(tx, queue),
948
+ tx,
949
+ meta,
950
+ bag,
951
+ queue,
952
+ entity
953
+ }));
954
+ } catch (error) {
955
+ queue.discard();
956
+ throw error;
957
+ }
958
+ await queue.flush();
959
+ return result;
960
+ } catch (error) {
961
+ throw normalizeError(runtime, error);
962
+ }
963
+ }
964
+ function hookBase(scope) {
965
+ return {
966
+ ctx: scope.bound.ctx,
967
+ meta: scope.meta,
968
+ typion: scope.bound,
969
+ tx: scope.tx,
970
+ bag: scope.bag
971
+ };
972
+ }
973
+ function globalBase(scope) {
974
+ return {
975
+ ...hookBase(scope),
976
+ entityMeta: scope.entity
977
+ };
978
+ }
979
+ function globalHooks(scope) {
980
+ const out = [];
981
+ for (const plugin of scope.bound.runtime.plugins) {
982
+ const hooks = plugin.hooks;
983
+ if (hooks === null) continue;
984
+ if (hooks.entities !== void 0 && !hooks.entities.includes(scope.entity.name)) continue;
985
+ out.push(hooks);
986
+ }
987
+ return out;
988
+ }
989
+ async function beforeOperation(scope, args) {
990
+ for (const hooks of globalHooks(scope)) await hooks.beforeOperation?.({
991
+ entity: scope.entity.name,
992
+ op: scope.meta.operation,
993
+ args,
994
+ ...globalBase(scope)
995
+ });
996
+ }
997
+ async function afterOperation(scope, result) {
998
+ for (const hooks of globalHooks(scope)) await hooks.afterOperation?.({
999
+ entity: scope.entity.name,
1000
+ op: scope.meta.operation,
1001
+ result,
1002
+ ...globalBase(scope)
1003
+ });
1004
+ }
1005
+ async function afterLink(scope, outcome) {
1006
+ for (const hooks of globalHooks(scope)) await hooks.afterLink?.({
1007
+ entity: scope.entity.name,
1008
+ ...outcome,
1009
+ ...globalBase(scope)
1010
+ });
1011
+ }
1012
+ const argsByPlugins = /* @__PURE__ */ new WeakMap();
1013
+ function defaultHookArgs(plugins) {
1014
+ let args = argsByPlugins.get(plugins);
1015
+ if (args === void 0) {
1016
+ const out = {
1017
+ change: {},
1018
+ afterChange: {},
1019
+ afterDelete: {}
1020
+ };
1021
+ for (const plugin of plugins) {
1022
+ const defaults = plugin.traits?.hookArgs;
1023
+ Object.assign(out.change, defaults?.change);
1024
+ Object.assign(out.afterChange, defaults?.afterChange);
1025
+ Object.assign(out.afterDelete, defaults?.afterDelete);
1026
+ }
1027
+ args = out;
1028
+ argsByPlugins.set(plugins, args);
1029
+ }
1030
+ return args;
1031
+ }
1032
+ async function entityChangeHook(scope, name, input) {
1033
+ const hook = scope.entity.hooks[name];
1034
+ if (hook === void 0) return input.data;
1035
+ const { args, ...rest } = input;
1036
+ return await hook({
1037
+ ...args,
1038
+ ...rest,
1039
+ ...hookBase(scope)
1040
+ }) ?? input.data;
1041
+ }
1042
+ function beforeValidate(scope, input) {
1043
+ return entityChangeHook(scope, "beforeValidate", input);
1044
+ }
1045
+ async function beforeChange(scope, input) {
1046
+ let data = await entityChangeHook(scope, "beforeChange", input);
1047
+ const { args, ...rest } = input;
1048
+ for (const hooks of globalHooks(scope)) {
1049
+ if (hooks.beforeChange === void 0) continue;
1050
+ data = await hooks.beforeChange({
1051
+ ...args,
1052
+ entity: scope.entity.name,
1053
+ ...rest,
1054
+ data,
1055
+ ...globalBase(scope)
1056
+ }) ?? data;
1057
+ }
1058
+ return data;
1059
+ }
1060
+ async function afterChange(scope, outcome) {
1061
+ const { args, ...rest } = outcome;
1062
+ const hook = scope.entity.hooks.afterChange;
1063
+ await hook?.({
1064
+ ...args,
1065
+ ...rest,
1066
+ ...hookBase(scope)
1067
+ });
1068
+ for (const hooks of globalHooks(scope)) await hooks.afterChange?.({
1069
+ ...args,
1070
+ entity: scope.entity.name,
1071
+ ...rest,
1072
+ ...globalBase(scope)
1073
+ });
1074
+ }
1075
+ async function beforeDelete(scope, doc, options) {
1076
+ const hook = scope.entity.hooks.beforeDelete;
1077
+ await hook?.({
1078
+ doc,
1079
+ options,
1080
+ ...hookBase(scope)
1081
+ });
1082
+ }
1083
+ async function afterDelete(scope, outcome) {
1084
+ const { doc, previous, removed, options, args } = outcome;
1085
+ const hook = scope.entity.hooks.afterDelete;
1086
+ await hook?.({
1087
+ ...args,
1088
+ doc,
1089
+ options,
1090
+ ...hookBase(scope)
1091
+ });
1092
+ for (const hooks of globalHooks(scope)) await hooks.afterDelete?.({
1093
+ ...args,
1094
+ entity: scope.entity.name,
1095
+ doc,
1096
+ previous,
1097
+ removed,
1098
+ options,
1099
+ ...globalBase(scope)
1100
+ });
1101
+ }
1102
+ function queueAfterCommit(scope, doc, previous) {
1103
+ const { entity, meta, queue } = scope;
1104
+ const ctx = scope.bound.ctx;
1105
+ const hook = entity.hooks.afterCommit;
1106
+ if (hook !== void 0) queue.push(() => hook({
1107
+ op: meta.operation,
1108
+ doc,
1109
+ previous,
1110
+ ctx,
1111
+ meta
1112
+ }));
1113
+ for (const hooks of globalHooks(scope)) {
1114
+ const global = hooks.afterCommit;
1115
+ if (global === void 0) continue;
1116
+ queue.push(() => global({
1117
+ entity: entity.name,
1118
+ op: meta.operation,
1119
+ doc,
1120
+ previous,
1121
+ ctx,
1122
+ meta
1123
+ }));
1124
+ }
1125
+ }
1126
+ //#endregion
1127
+ //#region src/runtime/write/validate.ts
1128
+ function runtimeColumn(entity, key) {
1129
+ return entity.columns[key]?.column;
1130
+ }
1131
+ function isGeneratedAlways(entity, key) {
1132
+ const column = runtimeColumn(entity, key);
1133
+ return column.generated?.type === "always" || column.generatedIdentity?.type === "always";
1134
+ }
1135
+ function assertData(data, what = "data") {
1136
+ if (!isPlainObject(data)) throw new BadRequestError(`${what} must be an object`);
1137
+ return data;
1138
+ }
1139
+ function assertScalars(entity, data) {
1140
+ const issues = [];
1141
+ for (const [key, value] of Object.entries(data)) {
1142
+ if (!isPlainObject(value) && !Array.isArray(value)) continue;
1143
+ const column = entity.columns[key];
1144
+ if (column === void 0 || column.columnKind !== null) continue;
1145
+ const type = column.dataType.split(" ")[0];
1146
+ if (type === "string" || type === "number" || type === "boolean" || type === "bigint") issues.push({
1147
+ path: [key],
1148
+ message: `Expected a ${type}, not ${Array.isArray(value) ? "an array" : "an object"}`
1149
+ });
1150
+ }
1151
+ if (issues.length > 0) throw new ValidationError(`Entity '${entity.name}': invalid data`, issues, { entity: entity.name });
1152
+ }
1153
+ function applyDefaults(entity, data) {
1154
+ const out = { ...data };
1155
+ for (const key of Object.keys(entity.columns)) {
1156
+ if (out[key] !== void 0) continue;
1157
+ const column = runtimeColumn(entity, key);
1158
+ if (typeof column.defaultFn === "function") out[key] = column.defaultFn();
1159
+ else if (column.default !== void 0 && !is(column.default, SQL)) out[key] = typeof column.default === "object" ? structuredClone(column.default) : column.default;
1160
+ }
1161
+ return out;
1162
+ }
1163
+ function applyKindWrites(entity, base, data, states) {
1164
+ const out = { ...data };
1165
+ if (states === null) return out;
1166
+ for (const key of entity.kindKeys) {
1167
+ const kind = entity.columns[key]?.columnKind;
1168
+ if (kind?.write === void 0 || data[key] === void 0) continue;
1169
+ out[key] = kind.write({
1170
+ value: data[key],
1171
+ stored: base?.[key],
1172
+ state: states.get(kind.plugin)
1173
+ });
1174
+ }
1175
+ return out;
1176
+ }
1177
+ async function runSchema(schema, value, entity) {
1178
+ const result = await schema["~standard"].validate(value);
1179
+ if (result.issues !== void 0) {
1180
+ const issues = normalizeIssues(result.issues);
1181
+ const summary = issues.slice(0, 3).map((i) => `${i.path.map(String).join(".") || "(root)"}: ${i.message}`).join("; ");
1182
+ throw new ValidationError(`Invalid ${entity.name}: ${summary}`, issues, { entity: entity.name });
1183
+ }
1184
+ return result.value;
1185
+ }
1186
+ function insertable(entity, row) {
1187
+ const out = {};
1188
+ for (const key of Object.keys(entity.columns)) if (row[key] !== void 0 && !isGeneratedAlways(entity, key)) out[key] = row[key];
1189
+ return out;
1190
+ }
1191
+ async function validateCreate(runtime, entity, data) {
1192
+ const unknown = Object.keys(data).filter((k) => data[k] !== void 0 && !(k in entity.columns));
1193
+ if (unknown.length > 0) throw new ValidationError(`Invalid ${entity.name}: unknown key${unknown.length > 1 ? "s" : ""} ${unknown.join(", ")}`, unknown.map((k) => ({
1194
+ path: [k],
1195
+ message: `Unknown key '${k}'`
1196
+ })), { entity: entity.name });
1197
+ const base = await runSchema(runtime.schema(entity, "insert"), insertable(entity, data), entity);
1198
+ const user = entity.validate?.insert;
1199
+ return user === void 0 ? base : runSchema(user, base, entity);
1200
+ }
1201
+ async function validateUpdate(runtime, entity, base, patch) {
1202
+ const unknown = Object.keys(patch).filter((k) => patch[k] !== void 0 && !(k in entity.columns));
1203
+ if (unknown.length > 0) throw new ValidationError(`Invalid ${entity.name}: unknown key${unknown.length > 1 ? "s" : ""} ${unknown.join(", ")}`, unknown.map((k) => ({
1204
+ path: [k],
1205
+ message: `Unknown key '${k}'`
1206
+ })), { entity: entity.name });
1207
+ const merged = await runSchema(runtime.schema(entity, "insert"), insertable(entity, {
1208
+ ...base,
1209
+ ...patch
1210
+ }), entity);
1211
+ const validated = {};
1212
+ for (const key of Object.keys(patch)) {
1213
+ if (patch[key] === void 0) continue;
1214
+ validated[key] = key in merged ? merged[key] : patch[key];
1215
+ }
1216
+ const user = entity.validate?.update;
1217
+ return user === void 0 ? validated : runSchema(user, validated, entity);
1218
+ }
1219
+ function stripHidden(entity, row) {
1220
+ const out = {};
1221
+ for (const [key, value] of Object.entries(row)) if (entity.columns[key]?.read !== false) out[key] = value;
1222
+ return out;
1223
+ }
1224
+ //#endregion
1225
+ //#region src/runtime/write/references.ts
1226
+ function keysOf(table, columns) {
1227
+ const entries = Object.entries(getTableColumns(table));
1228
+ return columns.map((column) => entries.find(([, c]) => c === column)?.[0]);
1229
+ }
1230
+ function* oneTargets(entity, bound) {
1231
+ for (const relation of Object.values(entity.relations)) {
1232
+ if (relation.type !== "one" || relation.through) continue;
1233
+ const target = bound.runtime.entityOrNull(relation.target);
1234
+ if (target !== null) yield {
1235
+ relation,
1236
+ target
1237
+ };
1238
+ }
1239
+ }
1240
+ function referencesDeletable(entity, bound) {
1241
+ for (const { target } of oneTargets(entity, bound)) if (target.traits.softDelete !== void 0) return true;
1242
+ return false;
1243
+ }
1244
+ const NO_PARTITION = /* @__PURE__ */ new Map();
1245
+ async function checkReferences(scope, row, written, options) {
1246
+ const { entity, bound, tx, meta } = scope;
1247
+ const partitioned = partitionOf(entity, bound, meta.operation).size > 0;
1248
+ const issues = [];
1249
+ for (const { relation, target } of oneTargets(entity, bound)) {
1250
+ const partition = partitioned ? partitionOf(target, bound, "findById") : NO_PARTITION;
1251
+ const kept = traitFilter(target, bound, "findById", traitOptions(target, bound.runtime.plugins, "read", options.withDeleted === true && target.traits.softDelete !== void 0 ? { withDeleted: true } : {}), WRITING);
1252
+ if (partition.size === 0 && kept === void 0) continue;
1253
+ const rel = bound.runtime.relations[entity.name]?.relations[relation.key];
1254
+ const sources = keysOf(entity.table, rel.sourceColumns);
1255
+ const targets = keysOf(target.table, rel.targetColumns);
1256
+ if (sources.some((key) => key === void 0 || key === entity.pk.key)) continue;
1257
+ if (!sources.some((key) => written.has(key))) continue;
1258
+ const values = sources.map((key) => row[key]);
1259
+ if (values.some((value) => value === null || value === void 0)) continue;
1260
+ const filter = andFilters([
1261
+ ...targets.map((key, i) => ({ [key]: { eq: values[i] } })),
1262
+ partitionFilter(partition),
1263
+ kept
1264
+ ]);
1265
+ const where = filterToSQL(bound.runtime.relations, target.name, target.table, filter);
1266
+ if (Number(await tx.$count(target.table, where)) === 0) issues.push({
1267
+ path: [sources[0]],
1268
+ message: `No ${target.name} row ${values.map(String).join(", ")}`
1269
+ });
1270
+ }
1271
+ if (issues.length > 0) throw new ValidationError(`Entity '${entity.name}': a reference points at no row`, issues, { entity: entity.name });
1272
+ }
1273
+ //#endregion
1274
+ //#region src/runtime/write/mutations.ts
1275
+ function referenceOptionKeys(entity, bound) {
1276
+ return referencesDeletable(entity, bound) ? ["withDeleted"] : [];
1277
+ }
1278
+ const NO_KEYS = /* @__PURE__ */ new Set();
1279
+ const NO_VALUES = {};
1280
+ function column(table, key) {
1281
+ return table[key];
1282
+ }
1283
+ function assertId(entity, id) {
1284
+ if (id === void 0 || id === null || typeof id === "object") throw new BadRequestError(`${entity.name}: a scalar id is required`, { entity: entity.name });
1285
+ }
1286
+ function writeOptionKeys(bound) {
1287
+ return [...pluginOptions(bound.runtime.plugins).write.keys()];
1288
+ }
1289
+ async function resultOf(scope, doc, options) {
1290
+ const { entity, bound } = scope;
1291
+ const out = stripHidden(entity, doc);
1292
+ for (const key of fieldReadDenied(entity, bound, scope.meta)) if (key in out) out[key] = null;
1293
+ const cells = entity.kindKeys.flatMap((key) => {
1294
+ const kind = entity.columns[key]?.columnKind;
1295
+ return kind === null || kind === void 0 || !(key in out) ? [] : [{
1296
+ row: out,
1297
+ key,
1298
+ kind
1299
+ }];
1300
+ });
1301
+ if (cells.length > 0) await projectCells(cells, readStates(bound.runtime.plugins, bound.ctx, options));
1302
+ return out;
1303
+ }
1304
+ function assertOptions(options, keys) {
1305
+ if (options === void 0) return {};
1306
+ if (!isPlainObject(options)) throw new BadRequestError("options must be an object");
1307
+ for (const key of Object.keys(options)) if (!keys.includes(key)) throw new BadRequestError(`Unknown option '${key}'`);
1308
+ return options;
1309
+ }
1310
+ async function writeRule(scope, rule, extra) {
1311
+ const outcome = await evaluateRule(scope.entity, rule, scope.bound, scope.meta, extra);
1312
+ if (outcome.kind === "deny") throw forbidden(scope.entity.name, rule, scope.meta, `access.${rule} denies '${scope.meta.operation}'`);
1313
+ return outcome.kind === "filter" ? outcome.filter : void 0;
1314
+ }
1315
+ function partitionAllowance(scope, data) {
1316
+ const { entity, bound, meta } = scope;
1317
+ const partition = partitionOf(entity, bound, meta.operation);
1318
+ let allowed;
1319
+ for (const [key, { value, plugin }] of partition) {
1320
+ const given = data[key];
1321
+ if (given === void 0) continue;
1322
+ if (!deepEqual(given, value)) throw new ForbiddenError(`Entity '${entity.name}': '${key}' cannot be set outside the call's partition`, {
1323
+ entity: entity.name,
1324
+ operation: meta.operation,
1325
+ rule: `partition:${plugin}`
1326
+ });
1327
+ (allowed ??= /* @__PURE__ */ new Set()).add(key);
1328
+ }
1329
+ return {
1330
+ allowed: allowed ?? NO_KEYS,
1331
+ partition
1332
+ };
1333
+ }
1334
+ async function lockRow(scope, id, parts) {
1335
+ const { entity, bound } = scope;
1336
+ const filter = andFilters([{ [entity.pk.key]: { eq: id } }, ...parts]);
1337
+ const where = filterToSQL(bound.runtime.relations, entity.name, entity.table, filter);
1338
+ const row = (await bound.runtime.dialect.lockSelect(scope.tx, entity.table, where))[0];
1339
+ if (row === void 0) throw new NotFoundError(`${entity.name} '${String(id)}' not found`, { entity: entity.name });
1340
+ return row;
1341
+ }
1342
+ function changedKeys(entity, before, after) {
1343
+ return Object.keys(entity.columns).filter((key) => before === null ? after[key] !== void 0 && after[key] !== null : !deepEqual(before[key], after[key]));
1344
+ }
1345
+ function hookArgs(scope) {
1346
+ const defaults = defaultHookArgs(scope.bound.runtime.plugins);
1347
+ return {
1348
+ change: { ...defaults.change },
1349
+ afterChange: { ...defaults.afterChange },
1350
+ afterDelete: { ...defaults.afterDelete }
1351
+ };
1352
+ }
1353
+ function addArgs(args, given) {
1354
+ if (given === void 0) return;
1355
+ Object.assign(args.change, given.change);
1356
+ Object.assign(args.afterChange, given.afterChange);
1357
+ Object.assign(args.afterDelete, given.afterDelete);
1358
+ }
1359
+ function* actingPlugins(scope) {
1360
+ for (const plugin of scope.bound.runtime.plugins) {
1361
+ const trait = scope.entity.traits[plugin.id];
1362
+ if (plugin.traits !== null && trait !== void 0) yield {
1363
+ plugin,
1364
+ trait,
1365
+ traits: plugin.traits
1366
+ };
1367
+ }
1368
+ }
1369
+ async function record(scope, input, into) {
1370
+ for (const { plugin, trait, traits } of actingPlugins(scope)) {
1371
+ if (traits.record === void 0) continue;
1372
+ const recorded = await traits.record({
1373
+ entity: scope.entity,
1374
+ state: trait.state,
1375
+ operation: input.operation,
1376
+ step: input.step,
1377
+ row: input.row,
1378
+ previous: input.previous,
1379
+ written: input.written,
1380
+ options: input.options.get(plugin.id) ?? NO_VALUES,
1381
+ now: scope.meta.now,
1382
+ tx: scope.tx,
1383
+ ctx: scope.bound.ctx
1384
+ });
1385
+ if (recorded !== void 0 && recorded !== null) Object.assign(into, recorded);
1386
+ }
1387
+ }
1388
+ async function planUpdate(scope, row, op, options) {
1389
+ const args = hookArgs(scope);
1390
+ let base;
1391
+ let keep = false;
1392
+ for (const { plugin, trait, traits } of actingPlugins(scope)) {
1393
+ if (traits.update === void 0) continue;
1394
+ const plan = await traits.update({
1395
+ entity: scope.entity,
1396
+ state: trait.state,
1397
+ operation: op,
1398
+ options: options.get(plugin.id) ?? NO_VALUES,
1399
+ row,
1400
+ tx: scope.tx,
1401
+ ctx: scope.bound.ctx
1402
+ });
1403
+ if (plan === void 0) continue;
1404
+ if (base === void 0 && !keep) {
1405
+ base = plan.base;
1406
+ keep = plan.keep === true;
1407
+ }
1408
+ addArgs(args, plan.args);
1409
+ }
1410
+ return {
1411
+ base: base ?? row,
1412
+ keep,
1413
+ args
1414
+ };
1415
+ }
1416
+ async function updateCore(scope, args) {
1417
+ const { entity, tx, bound } = scope;
1418
+ const { original, op, plan } = args;
1419
+ const { base, keep } = plan;
1420
+ const change = plan.args.change;
1421
+ let patch = await beforeValidate(scope, {
1422
+ op,
1423
+ data: args.patch,
1424
+ original,
1425
+ args: change
1426
+ });
1427
+ patch = await validateUpdate(bound.runtime, entity, base, patch);
1428
+ patch = await beforeChange(scope, {
1429
+ op,
1430
+ data: patch,
1431
+ original,
1432
+ args: change
1433
+ });
1434
+ await checkReferences(scope, {
1435
+ ...base,
1436
+ ...patch
1437
+ }, new Set(Object.keys(patch).filter((key) => patch[key] !== void 0)), { withDeleted: args.withDeleted });
1438
+ let doc;
1439
+ if (keep) doc = {
1440
+ ...base,
1441
+ ...patch
1442
+ };
1443
+ else {
1444
+ if (Object.keys(patch).filter((k) => patch[k] !== void 0).length === 0) return { doc: original };
1445
+ const pk = column(entity.table, entity.pk.key);
1446
+ const updated = (await tx.update(entity.table).set(patch).where(eq(pk, original[entity.pk.key])).returning())[0];
1447
+ if (updated === void 0) throw new InternalError(`${entity.name}: locked row vanished`);
1448
+ doc = updated;
1449
+ }
1450
+ const after = {
1451
+ ...change,
1452
+ ...plan.args.afterChange
1453
+ };
1454
+ await record(scope, {
1455
+ operation: op,
1456
+ step: "update",
1457
+ row: doc,
1458
+ previous: base,
1459
+ written: !keep,
1460
+ options: args.options
1461
+ }, after);
1462
+ await afterChange(scope, {
1463
+ op,
1464
+ doc,
1465
+ previous: base,
1466
+ changes: changedKeys(entity, base, doc),
1467
+ args: after
1468
+ });
1469
+ return { doc };
1470
+ }
1471
+ async function updateDocument(scope, original, data, opts) {
1472
+ const plan = await planUpdate(scope, original, opts.op, opts.options);
1473
+ return updateCore(scope, {
1474
+ original,
1475
+ patch: applyKindWrites(scope.entity, plan.base, data, opts.states),
1476
+ op: opts.op,
1477
+ plan,
1478
+ options: opts.options,
1479
+ withDeleted: opts.withDeleted
1480
+ });
1481
+ }
1482
+ function planCreate(scope, row, options) {
1483
+ const args = hookArgs(scope);
1484
+ const set = {};
1485
+ const rules = [];
1486
+ for (const { plugin, trait, traits } of actingPlugins(scope)) {
1487
+ if (traits.create === void 0) continue;
1488
+ const plan = traits.create({
1489
+ entity: scope.entity,
1490
+ state: trait.state,
1491
+ options: options.get(plugin.id) ?? NO_VALUES,
1492
+ row,
1493
+ now: scope.meta.now,
1494
+ ctx: scope.bound.ctx
1495
+ });
1496
+ if (plan === void 0) continue;
1497
+ for (const [key, value] of Object.entries(plan.set ?? {})) {
1498
+ if (!(key in scope.entity.columns)) throw new InternalError(`Plugin '${plugin.id}' sets '${key}' on '${scope.entity.name}', which is not a column`, { entity: scope.entity.name });
1499
+ set[key] = value;
1500
+ }
1501
+ for (const rule of plan.rules ?? []) if (!rules.includes(rule)) rules.push(rule);
1502
+ addArgs(args, plan.args);
1503
+ }
1504
+ return {
1505
+ set,
1506
+ rules,
1507
+ args
1508
+ };
1509
+ }
1510
+ function createOptionKeys(bound) {
1511
+ return [...pluginOptions(bound.runtime.plugins).create.keys()];
1512
+ }
1513
+ function updateOptionKeys(bound) {
1514
+ return [...pluginOptions(bound.runtime.plugins).update.keys()];
1515
+ }
1516
+ async function create(entity, bound, input, rawOptions) {
1517
+ const data = assertData(input);
1518
+ assertScalars(entity, data);
1519
+ const options = assertOptions(rawOptions, [
1520
+ ...createOptionKeys(bound),
1521
+ ...writeOptionKeys(bound),
1522
+ ...referenceOptionKeys(entity, bound)
1523
+ ]);
1524
+ const values = traitOptions(entity, bound.runtime.plugins, "create", options);
1525
+ return runOperation(bound, entity, "create", async (scope) => {
1526
+ await beforeOperation(scope, {
1527
+ data,
1528
+ options
1529
+ });
1530
+ await writeRule(scope, "create", { data });
1531
+ const { allowed, partition } = partitionAllowance(scope, data);
1532
+ fieldWriteCheck(entity, scope.bound, scope.meta, data, allowed);
1533
+ let row = applyKindWrites(entity, null, data, writeStates(entity, bound.runtime.plugins, bound.ctx, options));
1534
+ for (const [key, { value }] of partition) row[key] = value;
1535
+ const plan = planCreate(scope, row, values);
1536
+ const checks = [];
1537
+ for (const rule of plan.rules) {
1538
+ const filter = await writeRule(scope, rule, {});
1539
+ if (filter !== void 0) checks.push({
1540
+ rule,
1541
+ filter
1542
+ });
1543
+ }
1544
+ Object.assign(row, plan.set);
1545
+ const change = plan.args.change;
1546
+ row = await beforeValidate(scope, {
1547
+ op: "create",
1548
+ data: row,
1549
+ original: null,
1550
+ args: change
1551
+ });
1552
+ row = applyDefaults(entity, row);
1553
+ row = await validateCreate(bound.runtime, entity, row);
1554
+ row = await beforeChange(scope, {
1555
+ op: "create",
1556
+ data: row,
1557
+ original: null,
1558
+ args: change
1559
+ });
1560
+ await checkReferences(scope, row, new Set(Object.keys(row)), options);
1561
+ const doc = (await scope.tx.insert(entity.table).values(row).returning())[0];
1562
+ if (doc === void 0) throw new InternalError(`${entity.name}: insert returned no row`);
1563
+ for (const { rule, filter } of checks) {
1564
+ const where = filterToSQL(bound.runtime.relations, entity.name, entity.table, andFilters([{ [entity.pk.key]: { eq: doc[entity.pk.key] } }, filter]));
1565
+ if (Number(await scope.tx.$count(entity.table, where)) === 0) throw forbidden(entity.name, rule, scope.meta, `access.${rule} denies this row`);
1566
+ }
1567
+ const after = {
1568
+ ...change,
1569
+ ...plan.args.afterChange
1570
+ };
1571
+ await record(scope, {
1572
+ operation: "create",
1573
+ step: "create",
1574
+ row: doc,
1575
+ previous: null,
1576
+ written: true,
1577
+ options: values
1578
+ }, after);
1579
+ await afterChange(scope, {
1580
+ op: "create",
1581
+ doc,
1582
+ previous: null,
1583
+ changes: changedKeys(entity, null, doc),
1584
+ args: after
1585
+ });
1586
+ await afterOperation(scope, doc);
1587
+ queueAfterCommit(scope, doc, null);
1588
+ return resultOf(scope, doc, options);
1589
+ });
1590
+ }
1591
+ async function update(entity, bound, id, input, rawOptions) {
1592
+ assertId(entity, id);
1593
+ const data = assertData(input);
1594
+ assertScalars(entity, data);
1595
+ const options = assertOptions(rawOptions, [
1596
+ ...updateOptionKeys(bound),
1597
+ ...writeOptionKeys(bound),
1598
+ ...referenceOptionKeys(entity, bound)
1599
+ ]);
1600
+ const values = traitOptions(entity, bound.runtime.plugins, "update", options);
1601
+ return runOperation(bound, entity, "update", async (scope) => {
1602
+ await beforeOperation(scope, {
1603
+ id,
1604
+ data,
1605
+ options
1606
+ });
1607
+ const rule = await writeRule(scope, "update", { data });
1608
+ const { allowed, partition } = partitionAllowance(scope, data);
1609
+ fieldWriteCheck(entity, scope.bound, scope.meta, data, allowed);
1610
+ const original = await lockRow(scope, id, [
1611
+ partitionFilter(partition),
1612
+ traitFilter(entity, scope.bound, scope.meta.operation, values, WRITING),
1613
+ rule
1614
+ ]);
1615
+ const result = await updateDocument(scope, original, data, {
1616
+ states: writeStates(entity, bound.runtime.plugins, bound.ctx, options),
1617
+ op: "update",
1618
+ options: values,
1619
+ withDeleted: options.withDeleted === true
1620
+ });
1621
+ await afterOperation(scope, result.doc);
1622
+ queueAfterCommit(scope, result.doc, original);
1623
+ return resultOf(scope, result.doc, options);
1624
+ });
1625
+ }
1626
+ function mergedOptions(values) {
1627
+ return Object.assign({}, ...values.values());
1628
+ }
1629
+ async function keepRow(scope, original, values) {
1630
+ const { entity, tx } = scope;
1631
+ const pk = column(entity.table, entity.pk.key);
1632
+ for (const { plugin, trait, traits } of actingPlugins(scope)) {
1633
+ const keep = traits.remove;
1634
+ if (keep === void 0) continue;
1635
+ const kept = keep({
1636
+ entity,
1637
+ state: trait.state,
1638
+ options: values.get(plugin.id) ?? NO_VALUES,
1639
+ row: original,
1640
+ now: scope.meta.now,
1641
+ update: async (patch) => {
1642
+ const row = (await tx.update(entity.table).set(patch).where(eq(pk, original[entity.pk.key])).returning())[0];
1643
+ if (row === void 0) throw new InternalError(`${entity.name}: locked row vanished`);
1644
+ return row;
1645
+ }
1646
+ });
1647
+ if (kept !== void 0) return kept;
1648
+ }
1649
+ }
1650
+ async function deleteDocument(scope, original, values) {
1651
+ const { entity, tx } = scope;
1652
+ const options = mergedOptions(values);
1653
+ await beforeDelete(scope, original, options);
1654
+ const kept = await keepRow(scope, original, values);
1655
+ let doc;
1656
+ if (kept !== void 0) doc = kept;
1657
+ else {
1658
+ const pk = column(entity.table, entity.pk.key);
1659
+ const gone = (await tx.delete(entity.table).where(eq(pk, original[entity.pk.key])).returning())[0];
1660
+ if (gone === void 0) throw new InternalError(`${entity.name}: locked row vanished`);
1661
+ doc = gone;
1662
+ }
1663
+ const args = hookArgs(scope).afterDelete;
1664
+ const removed = kept === void 0;
1665
+ await record(scope, {
1666
+ operation: "delete",
1667
+ step: "delete",
1668
+ row: doc,
1669
+ previous: original,
1670
+ written: removed,
1671
+ options: values
1672
+ }, args);
1673
+ const outcome = {
1674
+ doc,
1675
+ previous: original,
1676
+ removed,
1677
+ options,
1678
+ args
1679
+ };
1680
+ await afterDelete(scope, outcome);
1681
+ return outcome.doc;
1682
+ }
1683
+ function deleteOptionKeys(bound) {
1684
+ return [...pluginOptions(bound.runtime.plugins).delete.keys()];
1685
+ }
1686
+ async function remove(entity, bound, id, rawOptions) {
1687
+ assertId(entity, id);
1688
+ const options = assertOptions(rawOptions, deleteOptionKeys(bound));
1689
+ const values = traitOptions(entity, bound.runtime.plugins, "delete", options);
1690
+ return runOperation(bound, entity, "delete", async (scope) => {
1691
+ await beforeOperation(scope, {
1692
+ id,
1693
+ options
1694
+ });
1695
+ const rule = await writeRule(scope, "delete", { options: mergedOptions(values) });
1696
+ const original = await lockRow(scope, id, [pluginFilter(entity, scope.bound, scope.meta.operation, values, WRITING), rule]);
1697
+ const doc = await deleteDocument(scope, original, values);
1698
+ await afterOperation(scope, doc);
1699
+ queueAfterCommit(scope, doc, original);
1700
+ return resultOf(scope, doc, {});
1701
+ });
1702
+ }
1703
+ //#endregion
1704
+ //#region src/runtime/write/operations.ts
1705
+ function entityOperations(entity, plugins) {
1706
+ return [...traitOperations(plugins).values()].filter((op) => entity.traits[op.plugin.id] !== void 0);
1707
+ }
1708
+ function entityOperation(entity, plugins, name) {
1709
+ const op = traitOperations(plugins).get(name);
1710
+ return op !== void 0 && entity.traits[op.plugin.id] !== void 0 ? op : null;
1711
+ }
1712
+ function operationInput(op, args) {
1713
+ const input = {};
1714
+ op.spec.args.forEach((key, i) => {
1715
+ if (args[i] !== void 0) input[key] = args[i];
1716
+ });
1717
+ const trailing = args[op.spec.args.length];
1718
+ if (op.spec.options !== void 0 && trailing !== void 0) {
1719
+ if (!isPlainObject(trailing)) throw new BadRequestError(`${op.name}: options must be an object`);
1720
+ for (const [key, value] of Object.entries(trailing)) if (value !== void 0) input[key] = value;
1721
+ }
1722
+ return input;
1723
+ }
1724
+ function operationMethods(entity, plugins, run) {
1725
+ const out = {};
1726
+ for (const op of entityOperations(entity, plugins)) {
1727
+ const path = op.name.split(".");
1728
+ let node = out;
1729
+ for (const part of path.slice(0, -1)) node = node[part] ??= {};
1730
+ node[path[path.length - 1]] = (...args) => run(op, args);
1731
+ }
1732
+ return out;
1733
+ }
1734
+ function kit(entity, bound, op, input) {
1735
+ const name = op.name;
1736
+ return {
1737
+ update: (id, patch, options) => {
1738
+ assertId(entity, id);
1739
+ return runOperation(bound, entity, name, async (scope) => {
1740
+ await beforeOperation(scope, input);
1741
+ const rule = options?.rule ?? "update";
1742
+ const filter = await writeRule(scope, rule, rule === "update" ? { data: {} } : {});
1743
+ const original = await lockRow(scope, id, [pluginFilter(entity, scope.bound, name, NO_TRAIT_OPTIONS, WRITING), filter]);
1744
+ const given = typeof patch === "function" ? await patch({
1745
+ row: original,
1746
+ tx: scope.tx,
1747
+ now: scope.meta.now,
1748
+ ctx: scope.bound.ctx
1749
+ }) : patch;
1750
+ if (given === void 0) return resultOf(scope, original, {});
1751
+ const values = options?.options === void 0 ? NO_TRAIT_OPTIONS : /* @__PURE__ */ new Map([[op.plugin.id, options.options]]);
1752
+ const plan = await planUpdate(scope, original, name, values);
1753
+ const result = await updateCore(scope, {
1754
+ original,
1755
+ patch: { ...given },
1756
+ op: name,
1757
+ plan,
1758
+ options: values
1759
+ });
1760
+ await afterOperation(scope, result.doc);
1761
+ queueAfterCommit(scope, result.doc, original);
1762
+ return resultOf(scope, result.doc, {});
1763
+ });
1764
+ },
1765
+ read: async (fn, options) => {
1766
+ await bound.runtime.ready();
1767
+ return guard(bound.runtime, async () => {
1768
+ const meta = operationMeta(bound.ctx, entity.name, name, bound.runtime.now());
1769
+ const rule = options?.rule ?? "read";
1770
+ const outcome = await evaluateRule(entity, rule, bound, meta);
1771
+ if (outcome.kind === "deny") throw forbidden(entity.name, rule, meta, `access.${rule} denies '${name}'`);
1772
+ if (outcome.kind === "filter") throw new ForbiddenError(`Entity '${entity.name}': access.${rule} must allow '${name}' as a whole, not return a filter`, {
1773
+ entity: entity.name,
1774
+ operation: name,
1775
+ rule
1776
+ });
1777
+ const partition = {};
1778
+ for (const [key, { value }] of partitionOf(entity, bound, name)) partition[key] = value;
1779
+ return fn({
1780
+ executor: bound.$executor,
1781
+ partition,
1782
+ now: meta.now,
1783
+ ctx: bound.ctx
1784
+ });
1785
+ });
1786
+ }
1787
+ };
1788
+ }
1789
+ async function runEntityOperation(entity, bound, name, input) {
1790
+ const op = entityOperation(entity, bound.runtime.plugins, name);
1791
+ if (op === null) throw new UnsupportedError(`Entity '${entity.name}' has no operation '${name}'`, { entity: entity.name });
1792
+ const given = input ?? {};
1793
+ if (!isPlainObject(given)) throw new BadRequestError(`${name}: the input must be an object`);
1794
+ const options = op.spec.options ?? {};
1795
+ for (const [key, value] of Object.entries(given)) {
1796
+ const type = options[key];
1797
+ if (type !== void 0) checkOptionType(key, type, value);
1798
+ else if (!op.spec.args.includes(key)) throw new BadRequestError(`${name}: unknown input '${key}'`);
1799
+ }
1800
+ const trait = entity.traits[op.plugin.id];
1801
+ return op.spec.run({
1802
+ entity,
1803
+ state: trait?.state,
1804
+ input: given,
1805
+ kit: kit(entity, bound, op, given)
1806
+ });
1807
+ }
1808
+ //#endregion
1809
+ export { allowAll as $, evaluateRule as A, pluginOptions as B, afterOperation as C, NO_TRAIT_OPTIONS as D, runOperation as E, partitionFilter as F, FALSE_FILTER as G, readStates as H, pluginFilter as I, guard as J, andFilters as K, traitFilter as L, fieldWriteCheck as M, filterToSQL as N, READING as O, forbidden as P, operationMeta as Q, kindExpression as R, afterLink as S, queueAfterCommit as T, traitOptions as U, projectCells as V, writeStates as W, CommitQueue as X, normalizeError as Y, buildContext as Z, updateDocument as _, runEntityOperation as a, resolvePlugins as at, assertData as b, create as c, lockRow as d, buildEntityMeta as et, mergedOptions as f, update as g, remove as h, operationMethods as i, definePlugin as it, fieldReadDenied as j, WRITING as k, deleteDocument as l, referenceOptionKeys as m, entityOperations as n, checkDependencies as nt, assertId as o, traitOperations as ot, partitionAllowance as p, isPlainObject as q, operationInput as r, defineFeaturePlugin as rt, assertOptions as s, entityOperation as t, defineEntity as tt, deleteOptionKeys as u, writeOptionKeys as v, beforeOperation as w, assertScalars as x, writeRule as y, kindFilterSQL as z };
1810
+
1811
+ //# sourceMappingURL=operations-D3rOacLX.js.map