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,1187 @@
1
+ import { a as InternalError, c as PluginError, d as ValidationError, i as ForbiddenError, n as ConfigError, r as ConflictError, s as NotFoundError, t as BadRequestError, u as UnsupportedError } from "./errors-D-CKO-px.js";
2
+ import { A as evaluateRule, B as pluginOptions, C as afterOperation, D as NO_TRAIT_OPTIONS, E as runOperation, F as partitionFilter, G as FALSE_FILTER, H as readStates, I as pluginFilter, J as guard, K as andFilters, L as traitFilter, M as fieldWriteCheck, N as filterToSQL, O as READING, P as forbidden, Q as operationMeta, R as kindExpression, S as afterLink, T as queueAfterCommit, U as traitOptions, V as projectCells, W as writeStates, X as CommitQueue, Y as normalizeError, Z as buildContext, _ as updateDocument, a as runEntityOperation, b as assertData, c as create, d as lockRow, f as mergedOptions, g as update, h as remove, i as operationMethods, j as fieldReadDenied, k as WRITING, l as deleteDocument, m as referenceOptionKeys, o as assertId, ot as traitOperations, p as partitionAllowance, q as isPlainObject, r as operationInput, s as assertOptions, u as deleteOptionKeys, v as writeOptionKeys, w as beforeOperation, x as assertScalars, y as writeRule, z as kindFilterSQL } from "./operations-D3rOacLX.js";
3
+ import { t as descriptor } from "./descriptor-Cc3mGp73.js";
4
+ import { n as structuralValidation } from "./structural-BZnjFmCs.js";
5
+ import { PgTable, getTableConfig } from "drizzle-orm/pg-core";
6
+ import { and, asc, eq, getTableColumns, inArray, is, sql } from "drizzle-orm";
7
+ //#region \0rolldown/runtime.js
8
+ var __defProp = Object.defineProperty;
9
+ var __exportAll = (all, no_symbols) => {
10
+ let target = {};
11
+ for (var name in all) __defProp(target, name, {
12
+ get: all[name],
13
+ enumerable: true
14
+ });
15
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
16
+ return target;
17
+ };
18
+ //#endregion
19
+ //#region src/runtime/dialect/pg.ts
20
+ function keyLiteral(key) {
21
+ return sql.raw(`'${key.replace(/'/g, "''")}'`);
22
+ }
23
+ const CAST_NAME = /^[a-z_][a-z0-9_ ]*(\([0-9, ]*\))?$/i;
24
+ const SQLSTATE = /^[0-9A-Z]{5}$/;
25
+ function findPgError(error) {
26
+ let current = error;
27
+ for (let depth = 0; depth < 8 && typeof current === "object" && current !== null; depth++) {
28
+ const code = current.code;
29
+ if (typeof code === "string" && SQLSTATE.test(code)) return current;
30
+ current = current.cause;
31
+ }
32
+ return null;
33
+ }
34
+ function isQueryError(error) {
35
+ return error instanceof Error && typeof error.query === "string" && Array.isArray(error.params);
36
+ }
37
+ function mapSqlState(pg, cause) {
38
+ const message = pg.message ?? "Database error";
39
+ const details = {
40
+ sqlstate: pg.code,
41
+ table: pg.table,
42
+ constraint: pg.constraint
43
+ };
44
+ switch (pg.code) {
45
+ case "23505": return new ConflictError(message, "unique", {
46
+ constraint: pg.constraint,
47
+ details,
48
+ cause
49
+ });
50
+ case "23503": return new ConflictError(message, "foreign_key", {
51
+ constraint: pg.constraint,
52
+ details,
53
+ cause
54
+ });
55
+ case "23514": return new ConflictError(message, "check", {
56
+ constraint: pg.constraint,
57
+ details,
58
+ cause
59
+ });
60
+ case "23502": return new ValidationError(message, [{
61
+ path: pg.column === void 0 ? [] : [pg.column],
62
+ message: "must not be null"
63
+ }], {
64
+ details,
65
+ cause
66
+ });
67
+ case "40001": return new ConflictError(message, "serialization", {
68
+ details,
69
+ cause
70
+ });
71
+ case "40P01": return new ConflictError(message, "deadlock", {
72
+ details,
73
+ cause
74
+ });
75
+ case "22P02":
76
+ case "22007": return new BadRequestError(message, {
77
+ details,
78
+ cause
79
+ });
80
+ default: return new InternalError("Database error", {
81
+ details,
82
+ cause
83
+ });
84
+ }
85
+ }
86
+ function qualifiedName(table) {
87
+ const config = getTableConfig(table);
88
+ const schema = config.schema ?? "public";
89
+ return {
90
+ schema,
91
+ name: config.name,
92
+ label: schema === "public" ? config.name : `${schema}.${config.name}`
93
+ };
94
+ }
95
+ function createPgDialect() {
96
+ return {
97
+ id: "pg",
98
+ jsonText(column, key) {
99
+ return sql`${column}->>${keyLiteral(key)}`;
100
+ },
101
+ cast(value, type) {
102
+ if (!CAST_NAME.test(type)) throw new InternalError(`cast: '${type}' is not a type name`);
103
+ return sql`(${value})::${sql.raw(type)}`;
104
+ },
105
+ jsonbMerge(column, patch) {
106
+ return sql`${column} || ${sql.param(patch, column)}`;
107
+ },
108
+ jsonParam(value, column) {
109
+ return sql`${sql.param(value, column)}`;
110
+ },
111
+ async lockSelect(tx, table, where) {
112
+ return await tx.select().from(table).where(where).for("update");
113
+ },
114
+ mapError(error) {
115
+ const pg = findPgError(error);
116
+ if (pg !== null) return mapSqlState(pg, error);
117
+ if (isQueryError(error)) return new InternalError("Database error", { cause: error });
118
+ return null;
119
+ },
120
+ async introspect(db, tables) {
121
+ const drift = {
122
+ missingTables: [],
123
+ missingColumns: []
124
+ };
125
+ if (tables.length === 0) return drift;
126
+ const names = tables.map(qualifiedName);
127
+ const matches = names.map((t) => sql`(table_schema = ${t.schema} and table_name = ${t.name})`);
128
+ const rows = await db.select({
129
+ schema: sql`table_schema`,
130
+ table: sql`table_name`,
131
+ column: sql`column_name`
132
+ }).from(sql`information_schema.columns`).where(sql.join(matches, sql` or `));
133
+ const present = /* @__PURE__ */ new Map();
134
+ for (const row of rows) {
135
+ const key = `${row.schema}.${row.table}`;
136
+ const columns = present.get(key) ?? /* @__PURE__ */ new Set();
137
+ columns.add(row.column);
138
+ present.set(key, columns);
139
+ }
140
+ tables.forEach((table, i) => {
141
+ const name = names[i];
142
+ const columns = present.get(`${name.schema}.${name.name}`);
143
+ if (columns === void 0) {
144
+ drift.missingTables.push(name.label);
145
+ return;
146
+ }
147
+ const missing = getTableConfig(table).columns.map((c) => c.name).filter((c) => !columns.has(c));
148
+ if (missing.length > 0) drift.missingColumns.push({
149
+ table: name.label,
150
+ columns: missing
151
+ });
152
+ });
153
+ return drift;
154
+ }
155
+ };
156
+ }
157
+ //#endregion
158
+ //#region src/runtime/constants.ts
159
+ const QUERY_CONFIG_KEYS = /* @__PURE__ */ new Set([
160
+ "columns",
161
+ "with",
162
+ "where",
163
+ "extras",
164
+ "orderBy",
165
+ "offset",
166
+ "limit",
167
+ "comment"
168
+ ]);
169
+ //#endregion
170
+ //#region src/runtime/read/virtual-table.ts
171
+ function virtualTable(table, entity, dialect, kinds) {
172
+ if (entity === null) return table;
173
+ const rewritten = new Set(entity.kindKeys.filter((k) => entity.columns[k]?.columnKind?.expression !== void 0));
174
+ const hidden = new Set(Object.keys(entity.columns).filter((k) => entity.columns[k]?.read === false));
175
+ if (rewritten.size === 0 && hidden.size === 0) return table;
176
+ return new Proxy(table, { get(target, key, receiver) {
177
+ if (typeof key === "string") {
178
+ if (hidden.has(key)) return void 0;
179
+ if (rewritten.has(key)) {
180
+ const column = Reflect.get(target, key, receiver);
181
+ return kindExpression(entity.columns[key]?.columnKind, column, kinds, dialect, null);
182
+ }
183
+ }
184
+ return Reflect.get(target, key, receiver);
185
+ } });
186
+ }
187
+ //#endregion
188
+ //#region src/runtime/read/compose.ts
189
+ function projectedKinds(entity) {
190
+ const out = [];
191
+ for (const key of entity?.kindKeys ?? []) {
192
+ const kind = entity?.columns[key]?.columnKind;
193
+ if (kind?.project !== void 0) out.push([key, kind]);
194
+ }
195
+ return out;
196
+ }
197
+ function swappedSource(entity, runtime, bound, meta, options) {
198
+ for (const plugin of runtime.plugins) {
199
+ const source = plugin.traits?.source;
200
+ const trait = entity.traits[plugin.id];
201
+ if (source === void 0 || trait === void 0) continue;
202
+ const swapped = source({
203
+ entity,
204
+ state: trait.state,
205
+ operation: meta.operation,
206
+ options: options.get(plugin.id) ?? {},
207
+ ctx: bound.ctx
208
+ });
209
+ if (swapped === void 0) continue;
210
+ if (runtime.relations[swapped.table] === void 0) throw new InternalError(`Plugin '${plugin.id}' reads '${entity.name}' from '${swapped.table}', which is not a table of the engine`, { entity: entity.name });
211
+ return {
212
+ plugin: plugin.id,
213
+ key: swapped.table,
214
+ filter: swapped.filter,
215
+ rule: swapped.rule ?? "read"
216
+ };
217
+ }
218
+ return null;
219
+ }
220
+ function sourceOf(entity, runtime, swapped) {
221
+ if (swapped === null) return {
222
+ key: entity.name,
223
+ table: entity.table
224
+ };
225
+ return {
226
+ key: swapped.key,
227
+ table: runtime.relations[swapped.key]?.table
228
+ };
229
+ }
230
+ var Composer = class {
231
+ req;
232
+ nested = /* @__PURE__ */ new Map();
233
+ readOptions;
234
+ constructor(req) {
235
+ this.req = req;
236
+ this.readOptions = new Set(pluginOptions(req.runtime.plugins).read.keys());
237
+ }
238
+ async rootFilter(entity) {
239
+ const { bound, meta, traitOptions, source } = this.req;
240
+ const rule = source?.rule ?? "read";
241
+ const outcome = await evaluateRule(entity, rule, bound, meta);
242
+ if (outcome.kind === "deny") throw forbidden(entity.name, rule, meta, `access.${rule} denies '${meta.operation}'`);
243
+ return andFilters([
244
+ pluginFilter(entity, bound, meta.operation, traitOptions, {
245
+ writing: false,
246
+ skip: source?.plugin
247
+ }),
248
+ source?.filter,
249
+ outcome.kind === "filter" ? outcome.filter : void 0
250
+ ]);
251
+ }
252
+ nestedFilter(tableName) {
253
+ let pending = this.nested.get(tableName);
254
+ if (pending === void 0) {
255
+ pending = this.computeNested(tableName);
256
+ this.nested.set(tableName, pending);
257
+ }
258
+ return pending;
259
+ }
260
+ async computeNested(tableName) {
261
+ const { runtime, bound, meta, traitOptions } = this.req;
262
+ const entity = runtime.entityOrNull(tableName);
263
+ if (entity === null) {
264
+ if (bound.$mode === "system") return void 0;
265
+ throw new ForbiddenError(`Table '${tableName}' is not an entity: user-mode reads cannot reach it through a relation`, {
266
+ operation: meta.operation,
267
+ rule: "entity"
268
+ });
269
+ }
270
+ const outcome = await evaluateRule(entity, "read", bound, meta);
271
+ if (outcome.kind === "deny") return FALSE_FILTER;
272
+ return andFilters([pluginFilter(entity, bound, meta.operation, traitOptions, READING), outcome.kind === "filter" ? outcome.filter : void 0]);
273
+ }
274
+ async compileRoot(entity, config, injected) {
275
+ const source = sourceOf(entity, this.req.runtime, this.req.source);
276
+ const parts = andFilters([await this.rootFilter(entity), injected]);
277
+ const level = await this.compileLevel(source.key, entity, config, parts, "root");
278
+ const where = level.config.where;
279
+ return {
280
+ ...level,
281
+ source,
282
+ where
283
+ };
284
+ }
285
+ async compileWhere(entity, where, injected) {
286
+ const source = sourceOf(entity, this.req.runtime, this.req.source);
287
+ const user = where === void 0 ? void 0 : await this.rewriteWhere(where, source.key, entity);
288
+ return {
289
+ source,
290
+ where: andFilters([
291
+ await this.rootFilter(entity),
292
+ injected,
293
+ user
294
+ ])
295
+ };
296
+ }
297
+ async compileLevel(tableKey, entity, raw, parts, level) {
298
+ const root = level === "root";
299
+ const cfg = raw === true ? {} : raw;
300
+ if (!isPlainObject(cfg)) throw new BadRequestError(`with.${level} must be true or a query config object`);
301
+ for (const key of Object.keys(cfg)) if (!QUERY_CONFIG_KEYS.has(key) && !(root && this.readOptions.has(key))) throw new BadRequestError(`Unknown query option '${key}'${root ? "" : ` in with.${level}`}`);
302
+ const { runtime, bound, meta } = this.req;
303
+ const out = {};
304
+ for (const key of [
305
+ "offset",
306
+ "limit",
307
+ "comment"
308
+ ]) if (cfg[key] !== void 0) out[key] = cfg[key];
309
+ if (root && entity !== null) this.checkLimit(entity, cfg.limit);
310
+ const swappedRoot = root && this.req.source !== null;
311
+ const columns = this.columnsFor(entity, cfg.columns, swappedRoot);
312
+ if (columns !== void 0) out.columns = columns;
313
+ const userWhere = cfg.where === void 0 ? void 0 : await this.rewriteWhere(cfg.where, tableKey, entity);
314
+ const where = andFilters([parts, userWhere]);
315
+ if (where !== void 0) out.where = where;
316
+ const orderBy = cfg.orderBy ?? (root ? entity?.defaultOrder ?? void 0 : void 0);
317
+ if (orderBy !== void 0) out.orderBy = this.rewriteOrderBy(orderBy, tableKey, entity);
318
+ if (cfg.extras !== void 0) out.extras = this.wrapExtras(cfg.extras, entity);
319
+ const withPlan = {};
320
+ if (cfg.with !== void 0) {
321
+ if (!isPlainObject(cfg.with)) throw new BadRequestError("with must be an object");
322
+ const relations = runtime.relations[tableKey]?.relations ?? {};
323
+ const outWith = {};
324
+ for (const [key, value] of Object.entries(cfg.with)) {
325
+ if (value === void 0 || value === false) continue;
326
+ const relation = relations[key];
327
+ if (relation === void 0) throw new BadRequestError(`Unknown relation '${key}' on '${tableKey}'`);
328
+ const target = relation.targetTableName;
329
+ const compiled = await this.compileLevel(target, runtime.entityOrNull(target), value, await this.nestedFilter(target), key);
330
+ outWith[key] = compiled.config;
331
+ withPlan[key] = compiled.plan;
332
+ }
333
+ out.with = outWith;
334
+ }
335
+ return {
336
+ config: out,
337
+ plan: {
338
+ kinds: projectedKinds(entity),
339
+ nullify: entity === null ? [] : fieldReadDenied(entity, bound, meta),
340
+ with: withPlan
341
+ }
342
+ };
343
+ }
344
+ checkLimit(entity, limit) {
345
+ if (limit === void 0) return;
346
+ if (typeof limit !== "number" || !Number.isInteger(limit) || limit < 0) throw new BadRequestError("limit must be a non-negative integer");
347
+ if (limit > entity.maxLimit) throw new BadRequestError(`limit ${limit} exceeds the maximum of ${entity.maxLimit} for '${entity.name}'`, { entity: entity.name });
348
+ }
349
+ columnKeys(tableKey, entity) {
350
+ if (entity !== null) return new Set(Object.keys(entity.columns));
351
+ const table = this.req.runtime.relations[tableKey]?.table;
352
+ return new Set(table === void 0 ? [] : Object.keys(getTableColumns(table)));
353
+ }
354
+ hiddenKeys(entity) {
355
+ if (entity === null) return [];
356
+ return Object.keys(entity.columns).filter((k) => entity.columns[k]?.read === false);
357
+ }
358
+ columnsFor(entity, columns, swappedRoot) {
359
+ if (entity === null) return columns;
360
+ const hidden = this.hiddenKeys(entity);
361
+ if (columns === void 0) {
362
+ if (!swappedRoot && hidden.length === 0) return void 0;
363
+ return Object.fromEntries(entity.visibleKeys.map((k) => [k, true]));
364
+ }
365
+ if (!isPlainObject(columns)) throw new BadRequestError("columns must be an object");
366
+ const entries = Object.entries(columns).filter(([, v]) => v !== void 0);
367
+ for (const [key, value] of entries) {
368
+ if (!(key in entity.columns)) throw new BadRequestError(`Unknown column '${key}' on '${entity.name}'`, { entity: entity.name });
369
+ if (typeof value !== "boolean") throw new BadRequestError(`columns.${key} must be a boolean`);
370
+ if (value && hidden.includes(key)) throw new BadRequestError(`Column '${key}' of '${entity.name}' is not readable`, { entity: entity.name });
371
+ }
372
+ if (entries.some(([, v]) => v === true)) return Object.fromEntries(entries.filter(([, v]) => v));
373
+ const omitted = /* @__PURE__ */ new Set([...entries.map(([k]) => k), ...hidden]);
374
+ return Object.fromEntries(entity.visibleKeys.filter((k) => !omitted.has(k)).map((k) => [k, true]));
375
+ }
376
+ async rewriteWhere(where, tableKey, entity) {
377
+ if (!isPlainObject(where)) throw new BadRequestError("where must be an object");
378
+ const { runtime } = this.req;
379
+ const columns = this.columnKeys(tableKey, entity);
380
+ const relations = runtime.relations[tableKey]?.relations ?? {};
381
+ const plain = {};
382
+ const extra = [];
383
+ for (const [key, value] of Object.entries(where)) {
384
+ if (value === void 0) continue;
385
+ if (key === "AND" || key === "OR") {
386
+ if (!Array.isArray(value)) throw new BadRequestError(`where.${key} must be an array`);
387
+ plain[key] = await Promise.all(value.map((v) => this.rewriteWhere(v, tableKey, entity)));
388
+ } else if (key === "NOT") plain.NOT = await this.rewriteWhere(value, tableKey, entity);
389
+ else if (key === "RAW") plain.RAW = typeof value === "function" ? (t, ops) => value(this.virtual(t, entity), ops) : value;
390
+ else if (columns.has(key)) {
391
+ if (entity?.columns[key]?.read === false) throw new BadRequestError(`Column '${key}' of '${entity.name}' is not readable`, { entity: entity.name });
392
+ const kind = entity?.columns[key]?.columnKind;
393
+ if (kind?.expression !== void 0) {
394
+ const { dialect } = runtime;
395
+ const { kinds } = this.req;
396
+ extra.push({ RAW: (t) => kindFilterSQL((sample) => kindExpression(kind, t[key], kinds, dialect, sample), key, value) });
397
+ } else plain[key] = value;
398
+ } else if (relations[key] !== void 0) {
399
+ const target = relations[key].targetTableName;
400
+ const parts = await this.nestedFilter(target);
401
+ if (value === true) plain[key] = parts ?? true;
402
+ else if (value === false) {
403
+ if (parts === void 0) plain[key] = false;
404
+ else extra.push({ NOT: { [key]: parts } });
405
+ } else if (isPlainObject(value)) {
406
+ const inner = await this.rewriteWhere(value, target, runtime.entityOrNull(target));
407
+ plain[key] = andFilters([parts, inner]) ?? {};
408
+ } else throw new BadRequestError(`where.${key} must be a boolean or a filter object`);
409
+ } else throw new BadRequestError(`Unknown filter field '${key}' on '${tableKey}'`);
410
+ }
411
+ return extra.length === 0 ? plain : { AND: [plain, ...extra] };
412
+ }
413
+ rewriteOrderBy(orderBy, tableKey, entity) {
414
+ if (typeof orderBy === "function") return (t, ops) => orderBy(this.virtual(t, entity), ops);
415
+ if (!isPlainObject(orderBy)) throw new BadRequestError("orderBy must be an object or a callback");
416
+ const columns = this.columnKeys(tableKey, entity);
417
+ const entries = Object.entries(orderBy).filter(([, v]) => v !== void 0);
418
+ for (const [key, dir] of entries) {
419
+ if (!columns.has(key) || entity?.columns[key]?.read === false) throw new BadRequestError(`Cannot order '${tableKey}' by '${key}'`);
420
+ if (dir !== "asc" && dir !== "desc") throw new BadRequestError(`orderBy.${key} must be 'asc' or 'desc'`);
421
+ }
422
+ const rewritten = new Set(entries.map(([key]) => key).filter((key) => entity?.columns[key]?.columnKind?.expression !== void 0));
423
+ if (rewritten.size === 0) return orderBy;
424
+ const { dialect } = this.req.runtime;
425
+ const { kinds } = this.req;
426
+ return (t, ops) => entries.map(([key, dir]) => {
427
+ const column = t[key];
428
+ const target = rewritten.has(key) ? kindExpression(entity?.columns[key]?.columnKind, column, kinds, dialect, null) : column;
429
+ return dir === "asc" ? ops.asc(target) : ops.desc(target);
430
+ });
431
+ }
432
+ wrapExtras(extras, entity) {
433
+ if (!isPlainObject(extras)) throw new BadRequestError("extras must be an object");
434
+ const out = {};
435
+ for (const [key, value] of Object.entries(extras)) out[key] = typeof value === "function" ? (t, ops) => value(this.virtual(t, entity), ops) : value;
436
+ return out;
437
+ }
438
+ virtual(table, entity) {
439
+ return virtualTable(table, entity, this.req.runtime.dialect, this.req.kinds);
440
+ }
441
+ };
442
+ //#endregion
443
+ //#region src/runtime/read/find.ts
444
+ function parseOptions(entity, bound, operation, config) {
445
+ const { runtime } = bound;
446
+ const traits = traitOptions(entity, runtime.plugins, "read", config);
447
+ const kinds = readStates(runtime.plugins, bound.ctx, config);
448
+ const meta = operationMeta(bound.ctx, entity.name, operation, runtime.now());
449
+ return {
450
+ composer: new Composer({
451
+ runtime,
452
+ bound,
453
+ meta,
454
+ kinds,
455
+ traitOptions: traits,
456
+ source: swappedSource(entity, runtime, bound, meta, traits)
457
+ }),
458
+ kinds
459
+ };
460
+ }
461
+ function collect(rows, plan, cells, hidden) {
462
+ for (const row of rows) {
463
+ for (const [key, kind] of plan.kinds) if (key in row) cells.push({
464
+ row,
465
+ key,
466
+ kind
467
+ });
468
+ for (const key of plan.nullify) if (key in row) hidden.push({
469
+ row,
470
+ key
471
+ });
472
+ for (const [key, sub] of Object.entries(plan.with)) {
473
+ const value = row[key];
474
+ if (Array.isArray(value)) collect(value, sub, cells, hidden);
475
+ else if (isPlainObject(value)) collect([value], sub, cells, hidden);
476
+ }
477
+ }
478
+ }
479
+ async function project(rows, plan, kinds) {
480
+ const cells = [];
481
+ const hidden = [];
482
+ collect(rows, plan, cells, hidden);
483
+ if (cells.length > 0) await projectCells(cells, kinds);
484
+ for (const { row, key } of hidden) row[key] = null;
485
+ return rows;
486
+ }
487
+ function builderFor(bound, key) {
488
+ return bound.$executor.query[key];
489
+ }
490
+ function assertConfig(config) {
491
+ if (config === void 0) return {};
492
+ if (!isPlainObject(config)) throw new BadRequestError("The query config must be an object");
493
+ return config;
494
+ }
495
+ async function runMany(entity, bound, operation, config, injected) {
496
+ const { composer, kinds } = parseOptions(entity, bound, operation, config);
497
+ const compiled = await composer.compileRoot(entity, config, injected);
498
+ return project(await builderFor(bound, compiled.source.key).findMany(compiled.config), compiled.plan, kinds);
499
+ }
500
+ async function find(entity, bound, config) {
501
+ await bound.runtime.ready();
502
+ return guard(bound.runtime, () => runMany(entity, bound, "find", assertConfig(config)));
503
+ }
504
+ async function findOne(entity, bound, config, operation = "findOne", injected) {
505
+ await bound.runtime.ready();
506
+ return guard(bound.runtime, async () => {
507
+ const cfg = assertConfig(config);
508
+ if (cfg.limit !== void 0) throw new BadRequestError(`'${operation}' does not accept a limit`);
509
+ const { composer, kinds } = parseOptions(entity, bound, operation, cfg);
510
+ const compiled = await composer.compileRoot(entity, cfg, injected);
511
+ const row = await builderFor(bound, compiled.source.key).findFirst(compiled.config);
512
+ if (row === void 0) return null;
513
+ return (await project([row], compiled.plan, kinds))[0] ?? null;
514
+ });
515
+ }
516
+ async function findById(entity, bound, id, config) {
517
+ if (id === void 0 || id === null) throw new BadRequestError("findById needs an id");
518
+ const cfg = assertConfig(config);
519
+ if (cfg.where !== void 0 || cfg.offset !== void 0) throw new BadRequestError("findById does not accept where or offset");
520
+ const row = await findOne(entity, bound, cfg, "findById", { [entity.pk.key]: { eq: id } });
521
+ if (row === null) throw new NotFoundError(`${entity.name} '${String(id)}' not found`, { entity: entity.name });
522
+ return row;
523
+ }
524
+ async function runCount(entity, bound, operation, config) {
525
+ const { composer } = parseOptions(entity, bound, operation, config);
526
+ const { source, where } = await composer.compileWhere(entity, config.where, void 0);
527
+ const sqlWhere = filterToSQL(bound.runtime.relations, source.key, source.table, where);
528
+ return Number(await bound.$executor.$count(source.table, sqlWhere));
529
+ }
530
+ async function count(entity, bound, config) {
531
+ await bound.runtime.ready();
532
+ return guard(bound.runtime, async () => {
533
+ const cfg = assertConfig(config);
534
+ const known = pluginOptions(bound.runtime.plugins).read;
535
+ for (const key of Object.keys(cfg)) if (key !== "where" && !known.has(key)) throw new BadRequestError(`Unknown count option '${key}'`);
536
+ return runCount(entity, bound, "count", cfg);
537
+ });
538
+ }
539
+ async function page(entity, bound, config) {
540
+ await bound.runtime.ready();
541
+ return guard(bound.runtime, async () => {
542
+ const cfg = assertConfig(config);
543
+ const limit = cfg.limit ?? entity.maxLimit;
544
+ const offset = cfg.offset ?? 0;
545
+ if (typeof offset !== "number" || !Number.isInteger(offset) || offset < 0) throw new BadRequestError("offset must be a non-negative integer");
546
+ const items = await runMany(entity, bound, "page", {
547
+ ...cfg,
548
+ limit,
549
+ offset
550
+ });
551
+ const counted = { where: cfg.where };
552
+ for (const key of pluginOptions(bound.runtime.plugins).read.keys()) counted[key] = cfg[key];
553
+ const total = await runCount(entity, bound, "page", counted);
554
+ return {
555
+ items,
556
+ total,
557
+ limit,
558
+ offset,
559
+ hasMore: offset + items.length < total
560
+ };
561
+ });
562
+ }
563
+ //#endregion
564
+ //#region src/runtime/write/bulk.ts
565
+ const DEFAULT_MAX = 1e3;
566
+ function maxOf(options) {
567
+ const max = options.max ?? DEFAULT_MAX;
568
+ if (typeof max !== "number" || !Number.isInteger(max) || max < 1) throw new BadRequestError("max must be a positive integer");
569
+ return max;
570
+ }
571
+ async function lockMatches(scope, where, rule, values, max) {
572
+ const { entity, bound, meta } = scope;
573
+ if (!isPlainObject(where)) throw new BadRequestError("where must be an object");
574
+ const composer = new Composer({
575
+ runtime: bound.runtime,
576
+ bound,
577
+ meta,
578
+ kinds: readStates(bound.runtime.plugins, bound.ctx, {}),
579
+ traitOptions: values,
580
+ source: null
581
+ });
582
+ const filter = andFilters([
583
+ pluginFilter(entity, bound, meta.operation, values, WRITING),
584
+ rule,
585
+ await composer.rewriteWhere(where, entity.name, entity)
586
+ ]);
587
+ const sqlWhere = filterToSQL(bound.runtime.relations, entity.name, entity.table, filter);
588
+ const pk = entity.table[entity.pk.key];
589
+ const rows = await scope.tx.select().from(entity.table).where(sqlWhere).orderBy(asc(pk)).limit(max + 1).for("update");
590
+ if (rows.length > max) throw new BadRequestError(`${meta.operation} matches more than ${max} ${entity.name} rows; narrow the filter or raise max`, { entity: entity.name });
591
+ return rows;
592
+ }
593
+ async function updateMany(entity, bound, where, input, rawOptions) {
594
+ const data = assertData(input);
595
+ assertScalars(entity, data);
596
+ const options = assertOptions(rawOptions, [
597
+ "max",
598
+ ...writeOptionKeys(bound),
599
+ ...referenceOptionKeys(entity, bound)
600
+ ]);
601
+ const max = maxOf(options);
602
+ const states = writeStates(entity, bound.runtime.plugins, bound.ctx, options);
603
+ return runOperation(bound, entity, "updateMany", async (scope) => {
604
+ await beforeOperation(scope, {
605
+ where,
606
+ data,
607
+ options
608
+ });
609
+ const rule = await writeRule(scope, "update", { data });
610
+ const { allowed } = partitionAllowance(scope, data);
611
+ fieldWriteCheck(entity, scope.bound, scope.meta, data, allowed);
612
+ const rows = await lockMatches(scope, where, rule, NO_TRAIT_OPTIONS, max);
613
+ for (const original of rows) {
614
+ const { doc } = await updateDocument(scope, original, data, {
615
+ states,
616
+ op: "update",
617
+ options: NO_TRAIT_OPTIONS,
618
+ withDeleted: options.withDeleted === true
619
+ });
620
+ queueAfterCommit(scope, doc, original);
621
+ }
622
+ const result = { count: rows.length };
623
+ await afterOperation(scope, result);
624
+ return result;
625
+ });
626
+ }
627
+ async function deleteMany(entity, bound, where, rawOptions) {
628
+ const options = assertOptions(rawOptions, ["max", ...deleteOptionKeys(bound)]);
629
+ const max = maxOf(options);
630
+ const values = traitOptions(entity, bound.runtime.plugins, "delete", options);
631
+ return runOperation(bound, entity, "deleteMany", async (scope) => {
632
+ await beforeOperation(scope, {
633
+ where,
634
+ options
635
+ });
636
+ const rows = await lockMatches(scope, where, await writeRule(scope, "delete", { options: mergedOptions(values) }), values, max);
637
+ for (const original of rows) {
638
+ const doc = await deleteDocument(scope, original, values);
639
+ queueAfterCommit(scope, doc, original);
640
+ }
641
+ const result = { count: rows.length };
642
+ await afterOperation(scope, result);
643
+ return result;
644
+ });
645
+ }
646
+ //#endregion
647
+ //#region src/runtime/write/links.ts
648
+ function keyOf(table, column) {
649
+ const found = Object.entries(getTableColumns(table)).find(([, c]) => c === column);
650
+ if (found === void 0) throw new InternalError("A many-to-many relation names a column it has not");
651
+ return found[0];
652
+ }
653
+ function joinOf(bound, entity, relation) {
654
+ if (typeof relation !== "string") throw new BadRequestError("relation must be the name of a relation", { entity: entity.name });
655
+ if (!entity.links.has(relation)) throw new UnsupportedError(`Entity '${entity.name}' does not write the links of '${relation}' (r.many(…, { through, writable: true }))`, { entity: entity.name });
656
+ const rel = bound.runtime.relations[entity.name]?.relations[relation];
657
+ const table = rel.throughTable;
658
+ const [joinOwnerKey, joinTargetKey] = [rel.through?.source[0]?._.key, rel.through?.target[0]?._.key];
659
+ if (table === void 0 || joinOwnerKey === void 0 || joinTargetKey === void 0) throw new InternalError(`Relation '${relation}' of '${entity.name}' goes through no table`);
660
+ const target = bound.runtime.entity(rel.targetTableName);
661
+ const columns = getTableColumns(table);
662
+ return {
663
+ table,
664
+ ownerKey: keyOf(entity.table, rel.sourceColumns[0]),
665
+ target,
666
+ targetKey: keyOf(target.table, rel.targetColumns[0]),
667
+ joinOwner: columns[joinOwnerKey],
668
+ joinTarget: columns[joinTargetKey],
669
+ joinOwnerKey,
670
+ joinTargetKey
671
+ };
672
+ }
673
+ async function writeLinks(entity, bound, op, id, relation, ids) {
674
+ assertId(entity, id);
675
+ const join = joinOf(bound, entity, relation);
676
+ const list = Array.isArray(ids) ? ids : [ids];
677
+ if (list.some((value) => value === void 0 || value === null || typeof value === "object")) throw new BadRequestError(`${op} takes the ids of the rows to ${op}`, { entity: entity.name });
678
+ const targets = [...new Set(list)];
679
+ return runOperation(bound, entity, op, async (scope) => {
680
+ const rule = await writeRule(scope, "update", { data: {} });
681
+ const { partition } = partitionAllowance(scope, {});
682
+ const owner = await lockRow(scope, id, [
683
+ partitionFilter(partition),
684
+ traitFilter(entity, scope.bound, scope.meta.operation, NO_TRAIT_OPTIONS, WRITING),
685
+ rule
686
+ ]);
687
+ if (targets.length === 0) return { count: 0 };
688
+ if ((await find(join.target, scope.bound, {
689
+ columns: { [join.targetKey]: true },
690
+ where: { [join.targetKey]: { in: targets } }
691
+ })).length !== targets.length) throw new NotFoundError(`Not every '${join.target.name}' row to ${op} was found`, { entity: join.target.name });
692
+ const ownerValue = owner[join.ownerKey];
693
+ const mine = and(eq(join.joinOwner, ownerValue), inArray(join.joinTarget, targets));
694
+ if (op === "unlink") {
695
+ const removed = await scope.tx.delete(join.table).where(mine).returning();
696
+ const gone = removed.map((row) => row[join.joinTargetKey]);
697
+ if (gone.length > 0) await afterLink(scope, {
698
+ op,
699
+ doc: owner,
700
+ relation,
701
+ ids: gone
702
+ });
703
+ return { count: removed.length };
704
+ }
705
+ const present = new Set((await scope.tx.select({ target: join.joinTarget }).from(join.table).where(mine)).map((row) => row.target));
706
+ const missing = targets.filter((target) => !present.has(target));
707
+ if (missing.length > 0) {
708
+ const rows = missing.map((target) => ({
709
+ [join.joinOwnerKey]: ownerValue,
710
+ [join.joinTargetKey]: target
711
+ }));
712
+ await scope.tx.insert(join.table).values(rows);
713
+ await afterLink(scope, {
714
+ op,
715
+ doc: owner,
716
+ relation,
717
+ ids: missing
718
+ });
719
+ }
720
+ return { count: missing.length };
721
+ });
722
+ }
723
+ //#endregion
724
+ //#region src/runtime/execute.ts
725
+ const EXTEND_KEYS = /* @__PURE__ */ new Set([
726
+ "where",
727
+ "orderBy",
728
+ "limit",
729
+ "offset"
730
+ ]);
731
+ function inputOf(descriptor) {
732
+ const input = descriptor.input ?? {};
733
+ if (!isPlainObject(input)) throw new BadRequestError("descriptor.input must be an object");
734
+ return input;
735
+ }
736
+ function extendConfig(config, extend) {
737
+ if (extend === void 0) return config;
738
+ if (!isPlainObject(extend)) throw new BadRequestError("extend must be an object");
739
+ for (const key of Object.keys(extend)) if (!EXTEND_KEYS.has(key)) throw new BadRequestError(`extend.${key} is not supported`);
740
+ const out = { ...config };
741
+ const where = andFilters([config.where, extend.where]);
742
+ if (where !== void 0) out.where = where;
743
+ for (const key of [
744
+ "orderBy",
745
+ "limit",
746
+ "offset"
747
+ ]) if (extend[key] !== void 0) out[key] = extend[key];
748
+ return out;
749
+ }
750
+ function entityOf(bound, descriptor) {
751
+ if (!isPlainObject(descriptor) || typeof descriptor.entity !== "string") throw new BadRequestError("A descriptor needs an entity name");
752
+ if (typeof descriptor.op !== "string") throw new BadRequestError("A descriptor needs an op");
753
+ return bound.runtime.entity(descriptor.entity);
754
+ }
755
+ async function execute(bound, descriptor, extend) {
756
+ const entity = entityOf(bound, descriptor);
757
+ const input = inputOf(descriptor);
758
+ if (extend !== void 0 && ![
759
+ "find",
760
+ "findOne",
761
+ "count",
762
+ "page"
763
+ ].includes(descriptor.op)) throw new BadRequestError(`extend is only supported on read descriptors, not '${descriptor.op}'`);
764
+ switch (descriptor.op) {
765
+ case "find": return find(entity, bound, extendConfig(input, extend));
766
+ case "findOne": return findOne(entity, bound, extendConfig(input, extend));
767
+ case "findById": return findById(entity, bound, input.id, input.config);
768
+ case "count": return count(entity, bound, extendConfig(input, extend));
769
+ case "page": return page(entity, bound, extendConfig(input, extend));
770
+ case "create": return create(entity, bound, input.data, input.options);
771
+ case "update": return update(entity, bound, input.id, input.data, input.options);
772
+ case "updateMany": return updateMany(entity, bound, input.where, input.data, input.options);
773
+ case "delete": return remove(entity, bound, input.id, input.options);
774
+ case "deleteMany": return deleteMany(entity, bound, input.where, input.options);
775
+ case "link":
776
+ case "unlink": return writeLinks(entity, bound, descriptor.op, input.id, input.relation, input.ids);
777
+ default:
778
+ if (!traitOperations(bound.runtime.plugins).has(descriptor.op)) throw new BadRequestError(`Unknown descriptor op '${String(descriptor.op)}'`);
779
+ return runEntityOperation(entity, bound, descriptor.op, input);
780
+ }
781
+ }
782
+ //#endregion
783
+ //#region src/runtime/engine.ts
784
+ const CALLER_LOOKUP = Symbol("typion.callerLookup");
785
+ const OWN_BIND_OPTIONS = /* @__PURE__ */ new Set([
786
+ "request",
787
+ "meta",
788
+ "reason",
789
+ "ctx"
790
+ ]);
791
+ function pluginBindOptions(opts) {
792
+ const out = {};
793
+ for (const [key, value] of Object.entries(opts)) if (!OWN_BIND_OPTIONS.has(key) && value !== void 0) out[key] = value;
794
+ return out;
795
+ }
796
+ function defaultAfterCommitError(error, info) {
797
+ console.error(`[typion] afterCommit failed (${info.entity}.${info.operation}, request ${info.requestId})`, error);
798
+ }
799
+ var EngineRuntime = class {
800
+ def;
801
+ db;
802
+ options;
803
+ relations;
804
+ dialect = createPgDialect();
805
+ validation;
806
+ plugins;
807
+ schemas = /* @__PURE__ */ new Map();
808
+ boot = null;
809
+ initializing = false;
810
+ engine;
811
+ constructor(def, db, options) {
812
+ this.def = def;
813
+ this.db = db;
814
+ this.options = options;
815
+ this.relations = def.relations;
816
+ this.validation = def.validation ?? structuralValidation();
817
+ this.plugins = def.plugins;
818
+ }
819
+ now() {
820
+ return this.options.now?.() ?? /* @__PURE__ */ new Date();
821
+ }
822
+ entity(name) {
823
+ const entity = this.entityOrNull(name);
824
+ if (entity === null) throw new BadRequestError(`Unknown entity '${name}'`);
825
+ return entity;
826
+ }
827
+ entityOrNull(name) {
828
+ return Object.hasOwn(this.def.entities, name) ? this.def.entities[name] ?? null : null;
829
+ }
830
+ schema(entity, mode) {
831
+ const key = `${entity.name}:${mode}`;
832
+ let schema = this.schemas.get(key);
833
+ if (schema === void 0) {
834
+ schema = this.validation.base(entity, mode, entity.validate?.fields);
835
+ this.schemas.set(key, schema);
836
+ }
837
+ return schema;
838
+ }
839
+ afterCommitError(error, info) {
840
+ (this.options.onAfterCommitError ?? defaultAfterCommitError)(error, info);
841
+ }
842
+ ready() {
843
+ if (this.initializing) return Promise.resolve();
844
+ if (this.boot === null) this.boot = this.runBoot().catch((error) => {
845
+ this.boot = null;
846
+ throw error;
847
+ });
848
+ return this.boot;
849
+ }
850
+ async runBoot() {
851
+ this.options.checkAdapter?.();
852
+ if (this.options.checkSchema !== false) await this.checkSchema();
853
+ this.initializing = true;
854
+ try {
855
+ for (const plugin of this.plugins) await plugin.spec.init?.(this.engine);
856
+ } finally {
857
+ this.initializing = false;
858
+ }
859
+ }
860
+ async checkSchema() {
861
+ const tables = Object.values(this.def.schema).filter((v) => is(v, PgTable));
862
+ let drift;
863
+ try {
864
+ drift = await this.dialect.introspect(this.db, tables);
865
+ } catch (error) {
866
+ throw normalizeError(this, error);
867
+ }
868
+ if (drift.missingTables.length === 0 && drift.missingColumns.length === 0) return;
869
+ const lines = [
870
+ "The database schema does not match the engine definition.",
871
+ ...drift.missingTables.length > 0 ? [` missing tables: ${drift.missingTables.join(", ")}`] : [],
872
+ ...drift.missingColumns.map((m) => ` missing columns on ${m.table}: ${m.columns.join(", ")}`),
873
+ "Run `typion db generate` and `typion db migrate` (or `typion db push` in development)."
874
+ ];
875
+ throw new ConfigError(lines.join("\n"), "schema_drift", { details: { ...drift } });
876
+ }
877
+ };
878
+ function entityApi(bound, entity) {
879
+ const name = entity.name;
880
+ const schemas = {};
881
+ for (const mode of [
882
+ "insert",
883
+ "update",
884
+ "select"
885
+ ]) Object.defineProperty(schemas, mode, {
886
+ enumerable: true,
887
+ get: () => bound.runtime.schema(entity, mode)
888
+ });
889
+ return {
890
+ find: (config) => find(entity, bound, config),
891
+ findOne: (config) => findOne(entity, bound, config),
892
+ findById: (id, config) => findById(entity, bound, id, config),
893
+ count: (config) => count(entity, bound, config),
894
+ page: (config) => page(entity, bound, config),
895
+ query: (config) => descriptor(name, "find", config ?? {}),
896
+ queryOne: (config) => descriptor(name, "findOne", config ?? {}),
897
+ run: async (d, extend) => {
898
+ if (d?.entity !== name) throw new BadRequestError(`A '${String(d?.entity)}' descriptor cannot run on '${name}'`);
899
+ return execute(bound, d, extend);
900
+ },
901
+ create: (data, options) => create(entity, bound, data, options),
902
+ update: (id, data, options) => update(entity, bound, id, data, options),
903
+ updateMany: (where, data, options) => updateMany(entity, bound, where, data, options),
904
+ delete: (id, options) => remove(entity, bound, id, options),
905
+ deleteMany: (where, options) => deleteMany(entity, bound, where, options),
906
+ ...entity.links.size === 0 ? {} : {
907
+ link: (id, relation, ids) => writeLinks(entity, bound, "link", id, relation, ids),
908
+ unlink: (id, relation, ids) => writeLinks(entity, bound, "unlink", id, relation, ids)
909
+ },
910
+ schemas,
911
+ ...operationMethods(entity, bound.runtime.plugins, async (op, args) => runEntityOperation(entity, bound, op.name, operationInput(op, args)))
912
+ };
913
+ }
914
+ const EMPTY_QUEUE = new CommitQueue(() => {});
915
+ var BoundImpl = class BoundImpl {
916
+ $engine;
917
+ ctx;
918
+ $tx;
919
+ queue;
920
+ apis = /* @__PURE__ */ new Map();
921
+ plugins;
922
+ constructor($engine, ctx, $tx, queue) {
923
+ this.$engine = $engine;
924
+ this.ctx = ctx;
925
+ this.$tx = $tx;
926
+ this.queue = queue;
927
+ for (const name of Object.keys($engine.runtime.def.entities)) Object.defineProperty(this, name, {
928
+ enumerable: true,
929
+ get: () => this.entity(name)
930
+ });
931
+ }
932
+ get runtime() {
933
+ return this.$engine.runtime;
934
+ }
935
+ get $ctx() {
936
+ return this.ctx;
937
+ }
938
+ get $mode() {
939
+ return this.ctx.mode;
940
+ }
941
+ get $executor() {
942
+ return this.$tx ?? this.runtime.db;
943
+ }
944
+ get $afterCommit() {
945
+ return this.queue ?? EMPTY_QUEUE;
946
+ }
947
+ get $plugins() {
948
+ this.plugins ??= this.$engine.pluginApis(this);
949
+ return this.plugins;
950
+ }
951
+ entity(name) {
952
+ let api = this.apis.get(name);
953
+ if (api === void 0) {
954
+ api = entityApi(this, this.runtime.entity(name));
955
+ this.apis.set(name, api);
956
+ }
957
+ return api;
958
+ }
959
+ joined(tx, queue) {
960
+ return new BoundImpl(this.$engine, this.ctx, tx, queue);
961
+ }
962
+ systemTwin(reason) {
963
+ if (this.ctx.mode === "system") return this;
964
+ const ctx = {
965
+ ...this.ctx,
966
+ mode: "system",
967
+ systemReason: reason
968
+ };
969
+ return new BoundImpl(this.$engine, ctx, this.$tx, this.queue);
970
+ }
971
+ $withTx(tx) {
972
+ return this.joined(tx, this.queue ?? this.$engine.newQueue(this.ctx));
973
+ }
974
+ $run(d, extend) {
975
+ return execute(this, d, extend);
976
+ }
977
+ async $transaction(fn, config) {
978
+ const runtime = this.runtime;
979
+ await runtime.ready();
980
+ try {
981
+ if (this.$tx !== null && this.queue !== null) {
982
+ const queue = this.queue;
983
+ const mark = queue.mark();
984
+ try {
985
+ return await this.$tx.transaction((tx) => fn(this.joined(tx, queue)));
986
+ } catch (error) {
987
+ queue.truncate(mark);
988
+ throw error;
989
+ }
990
+ }
991
+ const queue = this.$engine.newQueue(this.ctx);
992
+ let result;
993
+ try {
994
+ result = await runtime.db.transaction((tx) => fn(this.joined(tx, queue)), config);
995
+ } catch (error) {
996
+ queue.discard();
997
+ throw error;
998
+ }
999
+ await queue.flush();
1000
+ return result;
1001
+ } catch (error) {
1002
+ throw normalizeError(runtime, error);
1003
+ }
1004
+ }
1005
+ };
1006
+ var EngineImpl = class {
1007
+ options;
1008
+ runtime;
1009
+ $plugins = {};
1010
+ constructor(def, options) {
1011
+ this.options = options;
1012
+ const db = options.db;
1013
+ if (db?._?.relations !== def.relations) throw new ConfigError("createEngine: the db was built with other relations; create it with drizzle({ client, relations: def.relations })", "relations_mismatch");
1014
+ this.runtime = new EngineRuntime(def, db, options);
1015
+ this.runtime.engine = this;
1016
+ for (const name of Object.keys(def.entities)) Object.defineProperty(this, name, {
1017
+ enumerable: true,
1018
+ value: {
1019
+ query: (config) => descriptor(name, "find", config ?? {}),
1020
+ queryOne: (config) => descriptor(name, "findOne", config ?? {})
1021
+ }
1022
+ });
1023
+ Object.assign(this.$plugins, this.pluginApis());
1024
+ }
1025
+ pluginApis(app) {
1026
+ const out = {};
1027
+ for (const plugin of this.runtime.def.plugins) {
1028
+ const errors = {};
1029
+ for (const [code, spec] of Object.entries(plugin.errors)) errors[code] = (input = {}) => new PluginError(input.message ?? spec.message, plugin.id, code, {
1030
+ status: spec.status,
1031
+ details: input.details,
1032
+ cause: input.cause
1033
+ });
1034
+ out[plugin.id] = {
1035
+ ...plugin.spec.api?.(this, app),
1036
+ errors
1037
+ };
1038
+ }
1039
+ return out;
1040
+ }
1041
+ get $def() {
1042
+ return this.runtime.def;
1043
+ }
1044
+ get $db() {
1045
+ return this.runtime.db;
1046
+ }
1047
+ get $dialect() {
1048
+ return this.runtime.dialect;
1049
+ }
1050
+ newQueue(ctx) {
1051
+ return new CommitQueue((error) => this.runtime.afterCommitError(error, {
1052
+ entity: "*",
1053
+ operation: "transaction",
1054
+ requestId: ctx.requestId
1055
+ }));
1056
+ }
1057
+ pluginContext(base, input, allowAsync) {
1058
+ const parts = [];
1059
+ for (const plugin of this.runtime.plugins) {
1060
+ const build = plugin.spec.context;
1061
+ if (build === void 0) continue;
1062
+ const value = build(base, input);
1063
+ if (!allowAsync && typeof value?.then === "function") throw new ConfigError(`Plugin '${plugin.id}' builds its context asynchronously: use $fromRequest() instead of $as()`);
1064
+ parts.push(value);
1065
+ }
1066
+ if (!allowAsync) return Object.assign({}, ...parts);
1067
+ return Promise.all(parts).then((values) => Object.assign({}, ...values));
1068
+ }
1069
+ bind(user, mode, opts, systemReason, callerLookup, pluginCtx) {
1070
+ if (typeof user !== "object" || user === null) throw new BadRequestError("The engine context must be an object");
1071
+ const ctx = buildContext({
1072
+ user,
1073
+ mode,
1074
+ request: opts.request ?? null,
1075
+ systemReason,
1076
+ meta: opts.meta,
1077
+ now: this.runtime.now()
1078
+ });
1079
+ const input = {
1080
+ request: opts.request ?? void 0,
1081
+ headers: opts.request?.headers,
1082
+ options: pluginBindOptions(opts),
1083
+ ...callerLookup ? { callerLookup } : {}
1084
+ };
1085
+ const full = {
1086
+ ...pluginCtx ?? this.pluginContext(ctx, input, false),
1087
+ ...ctx
1088
+ };
1089
+ return new BoundImpl(this, full, null, null);
1090
+ }
1091
+ $as(ctx, opts = {}) {
1092
+ return this.bind(ctx, "user", opts, null, false, null);
1093
+ }
1094
+ $system(opts) {
1095
+ if (typeof opts?.reason !== "string" || opts.reason.trim() === "") throw new ConfigError("$system() needs a non-empty reason: it is recorded in versions and audit rows");
1096
+ return this.bind(opts.ctx ?? {}, "system", opts, opts.reason, opts[CALLER_LOOKUP] === true, null);
1097
+ }
1098
+ $fromRequest(request, opts = {}) {
1099
+ return this.fromInput({
1100
+ request,
1101
+ headers: request.headers,
1102
+ options: pluginBindOptions(opts)
1103
+ });
1104
+ }
1105
+ $fromHeaders(headers, opts = {}) {
1106
+ return this.fromInput({
1107
+ headers,
1108
+ options: pluginBindOptions(opts)
1109
+ });
1110
+ }
1111
+ async fromInput(input) {
1112
+ const request = input.request ?? null;
1113
+ const user = this.options.context === void 0 ? {} : await this.options.context(input);
1114
+ const probe = buildContext({
1115
+ user,
1116
+ mode: "user",
1117
+ request,
1118
+ systemReason: null,
1119
+ meta: void 0,
1120
+ now: this.runtime.now()
1121
+ });
1122
+ const pluginCtx = await this.pluginContext(probe, input, true);
1123
+ return this.bind(user, "user", { request }, null, false, pluginCtx);
1124
+ }
1125
+ $withTx(ctx, tx) {
1126
+ const bound = this.$as(ctx);
1127
+ return bound.joined(tx, this.newQueue(bound.ctx));
1128
+ }
1129
+ $execute(d, bound) {
1130
+ if (!(bound instanceof BoundImpl)) return Promise.reject(new BadRequestError("$execute needs an engine bound with $as/$system"));
1131
+ return execute(bound, d);
1132
+ }
1133
+ };
1134
+ function createEngine(def, options) {
1135
+ return new EngineImpl(def, options);
1136
+ }
1137
+ //#endregion
1138
+ //#region src/runtime/typion.ts
1139
+ var typion_exports = /* @__PURE__ */ __exportAll({
1140
+ createFromConfig: () => createFromConfig,
1141
+ getTypion: () => getTypion,
1142
+ standIn: () => standIn
1143
+ });
1144
+ const instances = globalThis.__typion ??= /* @__PURE__ */ new Map();
1145
+ function createFromConfig(config, options) {
1146
+ const context = options.context ?? config.context;
1147
+ const engine = createEngine(config.definition, {
1148
+ ...options,
1149
+ onAfterCommitError: options.onAfterCommitError ?? config.onAfterCommitError,
1150
+ checkSchema: options.checkSchema ?? config.checkSchema,
1151
+ db: options.db,
1152
+ context: context === void 0 ? void 0 : (input) => context({
1153
+ ...input,
1154
+ system: engine.$system({
1155
+ reason: "context",
1156
+ [CALLER_LOOKUP]: true
1157
+ }),
1158
+ db: options.db
1159
+ })
1160
+ });
1161
+ return engine;
1162
+ }
1163
+ function standIn(config, engine) {
1164
+ const previous = instances.get(config);
1165
+ instances.set(config, engine);
1166
+ return () => {
1167
+ if (instances.get(config) !== engine) return;
1168
+ if (previous === void 0) instances.delete(config);
1169
+ else instances.set(config, previous);
1170
+ };
1171
+ }
1172
+ async function getTypion(config, options = {}) {
1173
+ const cached = options.fresh === true ? void 0 : instances.get(config);
1174
+ if (cached !== void 0) return cached;
1175
+ const adapter = options.db ?? config.db;
1176
+ if (adapter === void 0) throw new ConfigError("getTypion: the config has no database adapter (`db` in defineConfig)");
1177
+ const engine = createFromConfig(config, {
1178
+ db: adapter.connect(config.definition.relations),
1179
+ ...adapter.check === void 0 ? {} : { checkAdapter: () => adapter.check?.() }
1180
+ });
1181
+ if (options.fresh !== true) instances.set(config, engine);
1182
+ return engine;
1183
+ }
1184
+ //#endregion
1185
+ export { typion_exports as i, getTypion as n, standIn as r, createFromConfig as t };
1186
+
1187
+ //# sourceMappingURL=typion-Dh2BCSRz.js.map