typion 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +15 -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 +827 -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-CpLQy7OI.d.ts +2098 -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-D03-rJ-t.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 +28 -0
  53. package/dist/testing/index.js +92 -0
  54. package/dist/testing/index.js.map +1 -0
  55. package/dist/typion-TJWDkHGh.js +1186 -0
  56. package/dist/typion-TJWDkHGh.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 +112 -0
  62. package/docs/auth.md +140 -0
  63. package/docs/collections.md +168 -0
  64. package/docs/database.md +74 -0
  65. package/docs/features/localization.md +112 -0
  66. package/docs/features/multi-tenant.md +165 -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 +184 -0
  71. package/docs/queries.md +197 -0
  72. package/docs/testing.md +85 -0
  73. package/package.json +125 -3
package/dist/index.js ADDED
@@ -0,0 +1,1324 @@
1
+ import { n as getTypion } from "./typion-TJWDkHGh.js";
2
+ import { a as InternalError, c as PluginError, d as ValidationError, f as attempt, i as ForbiddenError, l as TypionError, n as ConfigError, o as InvalidStateError, p as isTypionError, r as ConflictError, s as NotFoundError, t as BadRequestError, u as UnsupportedError } from "./errors-D-CKO-px.js";
3
+ import { i as timestamps, n as cloneColumn, r as defaultIdColumn, t as typion } from "./schema-Rzzq97cK.js";
4
+ import { $ as allowAll, at as resolvePlugins, et as buildEntityMeta, it as definePlugin, nt as checkDependencies, rt as defineFeaturePlugin, tt as defineEntity } from "./operations-D3rOacLX.js";
5
+ import { n as defineColumnKind, r as readColumnKind } from "./column-kinds-Bkm4NJI2.js";
6
+ import { n as structuralValidation } from "./structural-BZnjFmCs.js";
7
+ import { PgTable, boolean, foreignKey, getTableConfig, index, integer, isPgEnum, jsonb, pgSchema, pgTable, text, timestamp, unique, uniqueIndex, uuid } from "drizzle-orm/pg-core";
8
+ import { and, buildRelations, createRelationsHelper, desc, eq, getTableColumns, getTableName, inArray, is, sql } from "drizzle-orm";
9
+ //#region src/define/collection.ts
10
+ const relationHelpers = {
11
+ one: (target, config) => ({
12
+ kind: "one",
13
+ target,
14
+ from: config.from,
15
+ to: config.to,
16
+ optional: config.optional ?? true
17
+ }),
18
+ many: (target, config) => ({
19
+ kind: "many",
20
+ target,
21
+ from: config.from,
22
+ to: config.to,
23
+ through: config.through,
24
+ writable: config.writable ?? false
25
+ })
26
+ };
27
+ function collection(name, structure) {
28
+ const relations = structure.relations?.(relationHelpers) ?? {};
29
+ const state = { table: void 0 };
30
+ const make = (behaviour) => ({
31
+ name,
32
+ "~structure": structure,
33
+ "~runtime": {
34
+ structure,
35
+ relations,
36
+ behaviour,
37
+ state
38
+ },
39
+ get table() {
40
+ if (state.table === void 0) throw new ConfigError(`Collection '${name}' has no table until a config includes it`);
41
+ return state.table;
42
+ }
43
+ });
44
+ return Object.assign(make({}), { define: (behaviour) => make(behaviour) });
45
+ }
46
+ function buildCollectionTable(plugins, entry) {
47
+ const { state } = entry["~runtime"];
48
+ if (state.table !== void 0) return state.table;
49
+ const options = entry["~runtime"].structure;
50
+ const structure = options;
51
+ const columns = {};
52
+ if (!("id" in structure.columns) && structure.idColumn === void 0) columns.id = defaultIdColumn();
53
+ Object.assign(columns, structure.columns);
54
+ if (structure.timestamps !== false) Object.assign(columns, timestamps());
55
+ for (const plugin of plugins) for (const [key, column] of Object.entries(plugin.collection?.columns?.(options) ?? {})) {
56
+ if (key in columns) throw new ConfigError(`Collection '${entry.name}': plugin '${plugin.id}' adds column '${key}', which the collection already declares`);
57
+ columns[key] = column;
58
+ }
59
+ const indexes = entry["~runtime"].behaviour.indexes;
60
+ state.table = pgTable(structure.dbName ?? entry.name, columns, indexes);
61
+ return state.table;
62
+ }
63
+ function collectionEntityOptions(plugins, entry) {
64
+ const { structure, behaviour } = entry["~runtime"];
65
+ const consumed = /* @__PURE__ */ new Set([
66
+ "columns",
67
+ "relations",
68
+ "dbName",
69
+ "timestamps",
70
+ "public"
71
+ ]);
72
+ for (const plugin of plugins) for (const key of plugin.collection?.consumes ?? []) consumed.add(key);
73
+ const { indexes: _table, ...entityBehaviour } = behaviour;
74
+ const access = { ...behaviour.access };
75
+ const out = {
76
+ ...entityBehaviour,
77
+ access: structure.public === true ? {
78
+ ...access,
79
+ read: true
80
+ } : access
81
+ };
82
+ for (const [key, value] of Object.entries(structure)) if (!consumed.has(key)) out[key] = value;
83
+ const links = Object.entries(entry["~runtime"].relations).filter(([, spec]) => spec.kind === "many" && spec.writable === true).map(([key]) => key);
84
+ if (links.length > 0) out.links = links;
85
+ for (const plugin of plugins) Object.assign(out, plugin.collection?.entity?.(structure));
86
+ return out;
87
+ }
88
+ //#endregion
89
+ //#region src/features/localization.ts
90
+ const localizedKind = defineColumnKind("localized");
91
+ function isPlainObject(value) {
92
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
93
+ const proto = Object.getPrototypeOf(value);
94
+ return proto === Object.prototype || proto === null;
95
+ }
96
+ function sameValue(a, b) {
97
+ if (a === b) return true;
98
+ if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((item, i) => sameValue(item, b[i]));
99
+ if (isPlainObject(a) && isPlainObject(b)) {
100
+ const keys = Object.keys(a);
101
+ return keys.length === Object.keys(b).length && keys.every((k) => sameValue(a[k], b[k]));
102
+ }
103
+ return false;
104
+ }
105
+ function castFor(sample) {
106
+ if (typeof sample === "number" || typeof sample === "bigint") return "numeric";
107
+ if (typeof sample === "boolean") return "boolean";
108
+ if (sample instanceof Date) return "timestamptz";
109
+ return null;
110
+ }
111
+ function localeExpr(dialect, column, chain, cast) {
112
+ const parts = chain.map((locale) => dialect.jsonText(column, locale));
113
+ const expr = parts.length === 1 ? parts[0] : sql`coalesce(${sql.join(parts, sql`, `)})`;
114
+ return cast === null ? expr : dialect.cast(expr, cast);
115
+ }
116
+ function assertLocales(locales, defaultLocale) {
117
+ if (locales.length === 0) throw new ConfigError("localization: locales must not be empty");
118
+ if (new Set(locales).size !== locales.length) throw new ConfigError("localization: locales must be unique");
119
+ if (!locales.includes(defaultLocale)) throw new ConfigError(`localization: defaultLocale '${defaultLocale}' is not in locales`);
120
+ }
121
+ function localization(options) {
122
+ const { locales, defaultLocale } = options;
123
+ assertLocales(locales, defaultLocale);
124
+ const fallback = options.fallback ?? true;
125
+ const resolve = (locale) => {
126
+ if (locale === void 0) return defaultLocale;
127
+ if (typeof locale !== "string" || !locales.includes(locale)) throw new BadRequestError(`Unknown locale '${String(locale)}' (expected one of ${locales.join(", ")})`);
128
+ return locale;
129
+ };
130
+ const stateOf = (ctxLocale, locale, strict) => {
131
+ const all = locale === "all";
132
+ const effective = all || locale === void 0 ? ctxLocale : resolve(locale);
133
+ const withFallback = strict !== false && fallback;
134
+ const chain = withFallback ? [.../* @__PURE__ */ new Set([effective, defaultLocale])] : [effective];
135
+ return {
136
+ mode: all ? "all" : withFallback ? "resolved" : "strict",
137
+ chain
138
+ };
139
+ };
140
+ const project = (value, state) => {
141
+ if (state.mode === "all" || !isPlainObject(value)) return value;
142
+ for (const locale of state.chain) {
143
+ const item = value[locale];
144
+ if (item !== void 0 && item !== null) return item;
145
+ }
146
+ return null;
147
+ };
148
+ const plugin = defineFeaturePlugin({
149
+ id: "localization",
150
+ columnKinds: {
151
+ $types: {},
152
+ readOptions: {
153
+ locale: "string",
154
+ fallback: "boolean"
155
+ },
156
+ writeOptions: { locale: "string" },
157
+ read: ({ options: given, ctx }) => stateOf(ctx.locale, given.locale, given.fallback),
158
+ write: ({ options: given, ctx }) => given.locale === "all" ? "all" : { locale: resolve(given.locale ?? ctx.locale) },
159
+ kinds: { localized: {
160
+ project: (values, state) => values.map((value) => project(value, state)),
161
+ expression: ({ column, state, sample, dialect }) => localeExpr(dialect, column, state.chain, castFor(sample)),
162
+ write: ({ value, stored, state }) => {
163
+ if (state === "all") return value;
164
+ const next = {
165
+ ...isPlainObject(stored) ? stored : {},
166
+ [state.locale]: value
167
+ };
168
+ return value === null && Object.values(next).every((item) => item === null) ? null : next;
169
+ },
170
+ validation: (column, mode) => ({
171
+ type: "keyed",
172
+ keyName: "locale",
173
+ keys: locales,
174
+ required: column.notNull && mode !== "update" ? [defaultLocale] : [],
175
+ nonNull: column.notNull ? [defaultLocale] : [],
176
+ value: localizedKind.metaOf(column.column)?.value
177
+ }),
178
+ diff: (before, after) => {
179
+ if (!isPlainObject(before) || !isPlainObject(after)) return {
180
+ before,
181
+ after
182
+ };
183
+ const b = {};
184
+ const a = {};
185
+ for (const locale of /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])) {
186
+ if (sameValue(before[locale], after[locale])) continue;
187
+ b[locale] = before[locale] ?? null;
188
+ a[locale] = after[locale] ?? null;
189
+ }
190
+ return {
191
+ before: b,
192
+ after: a
193
+ };
194
+ }
195
+ } }
196
+ },
197
+ contextOptions: { $type: {} },
198
+ requestHeaders: ["x-locale"],
199
+ context: (_base, input) => ({ locale: resolve(input.options?.locale ?? options.locale?.(input) ?? input.headers?.get("x-locale") ?? void 0) }),
200
+ seed: { columns: (entity, input) => {
201
+ const columns = {};
202
+ for (const key of entity.kindKeys) {
203
+ if (entity.columns[key]?.kind !== localizedKind.id) continue;
204
+ const values = Array.from({ length: input.count }, (_, i) => ({ [defaultLocale]: `${entity.name} ${key} ${i + 1}` }));
205
+ columns[key] = input.generators.valuesFromArray({
206
+ values: [{
207
+ weight: 1,
208
+ values
209
+ }],
210
+ isUnique: true
211
+ });
212
+ }
213
+ return columns;
214
+ } }
215
+ });
216
+ return Object.assign(plugin, {
217
+ locales,
218
+ defaultLocale
219
+ });
220
+ }
221
+ function isJsonSafe(dataType) {
222
+ return dataType.startsWith("string") || dataType.startsWith("number") || dataType === "boolean" || dataType === "object json";
223
+ }
224
+ function localized(builder) {
225
+ const name = builder.config.name;
226
+ const value = pgTable("localized", { value: builder }).value;
227
+ if (!isJsonSafe(value.dataType)) throw new ConfigError(`localized(): a ${value.dataType} value would not survive JSON; use text, numbers, booleans, enums or json`);
228
+ const column = name === "" ? jsonb() : jsonb(name);
229
+ return localizedKind.mark(column.$type(), { value });
230
+ }
231
+ function localizedValue(column, locale) {
232
+ return sql`(${column}->>${sql.raw(`'${locale.replace(/'/g, "''")}'`)})`;
233
+ }
234
+ function localizedIndex(name, column, locale) {
235
+ return index(name).on(localizedValue(column, locale));
236
+ }
237
+ function localizedUnique(name, column, locale) {
238
+ return uniqueIndex(name).on(localizedValue(column, locale));
239
+ }
240
+ //#endregion
241
+ //#region src/define/kit.ts
242
+ function buildKitSchema(input) {
243
+ const out = {};
244
+ const owners = /* @__PURE__ */ new Map();
245
+ const add = (group, entries) => {
246
+ for (const [key, value] of Object.entries(entries)) {
247
+ const owner = owners.get(key);
248
+ if (owner !== void 0) throw new ConfigError(`Schema key '${key}' is used by both ${owner} and ${group}`);
249
+ owners.set(key, group);
250
+ out[key] = value;
251
+ }
252
+ };
253
+ add("tables", input.tables);
254
+ add("generated", input.generated);
255
+ add("plugins", input.pluginTables);
256
+ const schemas = /* @__PURE__ */ new Map();
257
+ schemas.set(input.pgSchema.schemaName, input.pgSchema);
258
+ for (const value of Object.values(out)) {
259
+ const name = is(value, PgTable) ? getTableConfig(value).schema : isPgEnum(value) ? value.schema : void 0;
260
+ if (name !== void 0 && name !== "public" && !schemas.has(name)) schemas.set(name, pgSchema(name));
261
+ }
262
+ for (const [name, schema] of schemas) {
263
+ if (owners.has(name)) throw new ConfigError(`Schema key '${name}' collides with the pg schema of the same name`);
264
+ out[name] = schema;
265
+ }
266
+ return out;
267
+ }
268
+ function buildGenerated(derived, pluginTables) {
269
+ return {
270
+ ...pluginTables,
271
+ ...derived,
272
+ $names: Object.keys(derived)
273
+ };
274
+ }
275
+ //#endregion
276
+ //#region src/define/relations.ts
277
+ function mergeRelationConfigs(fragments) {
278
+ const merged = {};
279
+ const owners = /* @__PURE__ */ new Map();
280
+ for (const { owner, config } of fragments) for (const [table, record] of Object.entries(config)) {
281
+ if (record === void 0) continue;
282
+ const target = merged[table] ?? {};
283
+ merged[table] = target;
284
+ for (const [key, relation] of Object.entries(record)) {
285
+ const id = `${table}.${key}`;
286
+ const previous = owners.get(id);
287
+ if (previous !== void 0) throw new ConfigError(`Relation '${id}' is declared twice (by ${previous} and by ${owner})`);
288
+ owners.set(id, owner);
289
+ target[key] = relation;
290
+ }
291
+ }
292
+ return merged;
293
+ }
294
+ function wrapDrizzleError(error) {
295
+ if (error instanceof ConfigError) throw error;
296
+ const message = error instanceof Error ? error.message : String(error);
297
+ throw new ConfigError(message, "config", { cause: error });
298
+ }
299
+ function resolveRelations(tables, build) {
300
+ const helper = createRelationsHelper(tables);
301
+ try {
302
+ return buildRelations(tables, mergeRelationConfigs(build(helper)));
303
+ } catch (error) {
304
+ wrapDrizzleError(error);
305
+ }
306
+ }
307
+ function tablesOnly(schema) {
308
+ const out = {};
309
+ for (const [key, value] of Object.entries(schema)) if (is(value, PgTable)) out[key] = value;
310
+ return out;
311
+ }
312
+ function columnKey(table, column, context) {
313
+ for (const [key, candidate] of Object.entries(getTableColumns(table))) if (candidate === column) return key;
314
+ throw new ConfigError(`${context}: relation column does not belong to its table`);
315
+ }
316
+ function single(items) {
317
+ return items.length === 1 ? items[0] : items;
318
+ }
319
+ function mirrorRelations(r, resolved, source, mirror) {
320
+ const config = resolved[source];
321
+ if (config === void 0) throw new ConfigError(`No relations entry for '${source}'`);
322
+ const out = {};
323
+ const mirrorTable = r[mirror];
324
+ if (mirrorTable === void 0) throw new ConfigError(`No table '${mirror}' in the merged schema`);
325
+ for (const [key, rel] of Object.entries(config.relations)) {
326
+ const context = `relations -> ${mirror}.${key}`;
327
+ const target = rel.targetTableName;
328
+ const targetTable = r[target];
329
+ if (targetTable === void 0) throw new ConfigError(`${context}: unknown target '${target}'`);
330
+ const sourceTable = rel.sourceTable;
331
+ const targetPg = rel.targetTable;
332
+ let from = rel.sourceColumns.map((c) => {
333
+ const column = mirrorTable[columnKey(sourceTable, c, context)];
334
+ if (column === void 0) throw new ConfigError(`${context}: the derived table lacks a column`);
335
+ return column;
336
+ });
337
+ let to = rel.targetColumns.map((c) => {
338
+ const column = targetTable[columnKey(targetPg, c, context)];
339
+ if (column === void 0) throw new ConfigError(`${context}: target lacks a column`);
340
+ return column;
341
+ });
342
+ const through = rel.through;
343
+ if (through !== void 0) {
344
+ const junction = (cfg) => {
345
+ const column = r[cfg.tableName]?.[cfg.key];
346
+ if (column === void 0) throw new ConfigError(`${context}: junction column '${cfg.tableName}.${cfg.key}' not found`);
347
+ return column;
348
+ };
349
+ from = from.map((c, i) => {
350
+ const via = through.source[i];
351
+ if (via === void 0) throw new ConfigError(`${context}: junction columns mismatch`);
352
+ return c.through(junction(via._));
353
+ });
354
+ to = to.map((c, i) => {
355
+ const via = through.target[i];
356
+ if (via === void 0) throw new ConfigError(`${context}: junction columns mismatch`);
357
+ return c.through(junction(via._));
358
+ });
359
+ }
360
+ const cfg = {
361
+ from: single(from),
362
+ to: single(to)
363
+ };
364
+ if (rel.alias !== void 0) cfg.alias = rel.alias;
365
+ if (rel.where !== void 0) cfg.where = rel.where;
366
+ if (rel.relationType === "one") {
367
+ cfg.optional = rel.optional ?? true;
368
+ const one = r.one[target];
369
+ if (one === void 0) throw new ConfigError(`${context}: unknown target '${target}'`);
370
+ out[key] = one(cfg);
371
+ } else {
372
+ const many = r.many[target];
373
+ if (many === void 0) throw new ConfigError(`${context}: unknown target '${target}'`);
374
+ out[key] = many(cfg);
375
+ }
376
+ }
377
+ return out;
378
+ }
379
+ //#endregion
380
+ //#region src/define/engine.ts
381
+ const RESERVED_ENTITY_NAMES = /* @__PURE__ */ new Set([
382
+ "_rpc",
383
+ "_plugins",
384
+ "entity"
385
+ ]);
386
+ const DEFAULT_MAX_LIMIT = 100;
387
+ function pluginTablesOf(plugins) {
388
+ const out = {};
389
+ for (const plugin of plugins) Object.assign(out, plugin.tables);
390
+ return out;
391
+ }
392
+ function fragmentsOf(r, userRelations, plugins) {
393
+ const fragments = [];
394
+ for (const plugin of plugins) {
395
+ const build = plugin.spec.relations;
396
+ if (build !== void 0) fragments.push({
397
+ owner: `plugin '${plugin.id}'`,
398
+ config: build(r)
399
+ });
400
+ }
401
+ fragments.push({
402
+ owner: "the engine relations",
403
+ config: userRelations(r)
404
+ });
405
+ return fragments;
406
+ }
407
+ function assertEntityName(name, def, relations, plugins) {
408
+ if (def.name !== name) throw new ConfigError(`Entity registered as '${name}' is defined as '${def.name}'`, "config", { entity: name });
409
+ if (RESERVED_ENTITY_NAMES.has(name) || name.startsWith("$")) throw new ConfigError(`Entity name '${name}' is reserved`, "config", { entity: name });
410
+ if (plugins.some((p) => p.id === name)) throw new ConfigError(`Entity name '${name}' collides with the plugin of the same id`, "config", { entity: name });
411
+ if (!(name in relations)) throw new ConfigError(`Entity '${name}' does not name a table of the engine`, "config", { entity: name });
412
+ }
413
+ function deriveTables(state, entities, taken) {
414
+ const out = {
415
+ tables: {},
416
+ mirrors: /* @__PURE__ */ new Map(),
417
+ byEntity: /* @__PURE__ */ new Map()
418
+ };
419
+ const names = new Set(entities.map(({ def }) => def.name));
420
+ for (const { def } of entities) {
421
+ const options = typeof def.options === "object" && def.options !== null ? def.options : {};
422
+ const table = state.resolved[def.name]?.table;
423
+ for (const plugin of state.plugins) {
424
+ const derive = plugin.traits?.derive;
425
+ if (derive === void 0) continue;
426
+ const tables = derive({
427
+ entity: def.name,
428
+ table,
429
+ options,
430
+ schema: state.pgSchema
431
+ });
432
+ if (tables === null) continue;
433
+ const own = {};
434
+ for (const [key, entry] of Object.entries(tables)) {
435
+ if (!is(entry.table, PgTable)) throw new ConfigError(`Plugin '${plugin.id}' derives '${key}' for entity '${def.name}', which is not a pg table`, "config", { entity: def.name });
436
+ if (key in taken || key in out.tables || names.has(key)) throw new ConfigError(`Plugin '${plugin.id}' derives a table '${key}' for entity '${def.name}', and the key is taken`, "config", { entity: def.name });
437
+ out.tables[key] = entry.table;
438
+ own[key] = entry.table;
439
+ if (entry.mirror === true) out.mirrors.set(key, {
440
+ entity: def.name,
441
+ plugin: plugin.id
442
+ });
443
+ }
444
+ let byPlugin = out.byEntity.get(def.name);
445
+ if (byPlugin === void 0) {
446
+ byPlugin = /* @__PURE__ */ new Map();
447
+ out.byEntity.set(def.name, byPlugin);
448
+ }
449
+ byPlugin.set(plugin.id, own);
450
+ }
451
+ }
452
+ return out;
453
+ }
454
+ function buildDefinition(state, userEntities) {
455
+ const entities = [];
456
+ const names = /* @__PURE__ */ new Set();
457
+ for (const [name, def] of Object.entries(userEntities)) {
458
+ assertEntityName(name, def, state.resolved, state.plugins);
459
+ names.add(name);
460
+ entities.push({
461
+ def,
462
+ owner: "user"
463
+ });
464
+ }
465
+ for (const plugin of state.plugins) for (const [name, def] of Object.entries(plugin.entities)) {
466
+ if (names.has(name)) throw new ConfigError(`Entity '${name}' is defined both by the engine and by plugin '${plugin.id}'`, "config", { entity: name });
467
+ assertEntityName(name, def, state.resolved, state.plugins);
468
+ names.add(name);
469
+ entities.push({
470
+ def,
471
+ owner: { plugin: plugin.id }
472
+ });
473
+ }
474
+ const pluginTables = pluginTablesOf(state.plugins);
475
+ const userTables = tablesOnly(state.tables);
476
+ const derived = deriveTables(state, entities, {
477
+ ...userTables,
478
+ ...pluginTables
479
+ });
480
+ const relations = resolveRelations({
481
+ ...userTables,
482
+ ...pluginTables,
483
+ ...derived.tables
484
+ }, (r) => {
485
+ const fragments = fragmentsOf(r, state.userRelations, state.plugins);
486
+ for (const [key, { entity, plugin }] of derived.mirrors) fragments.push({
487
+ owner: `plugin '${plugin}'`,
488
+ config: { [key]: mirrorRelations(r, state.resolved, entity, key) }
489
+ });
490
+ return fragments;
491
+ });
492
+ const meta = {};
493
+ for (const { def, owner } of entities) meta[def.name] = buildEntityMeta({
494
+ name: def.name,
495
+ options: def.options,
496
+ table: relations[def.name]?.table,
497
+ relations,
498
+ plugins: state.plugins,
499
+ derived: derived.byEntity.get(def.name) ?? /* @__PURE__ */ new Map(),
500
+ maxLimit: state.maxLimit,
501
+ owner
502
+ });
503
+ return {
504
+ $types: void 0,
505
+ relations,
506
+ schema: buildKitSchema({
507
+ tables: state.tables,
508
+ pgSchema: state.pgSchema,
509
+ generated: derived.tables,
510
+ pluginTables
511
+ }),
512
+ generated: buildGenerated(derived.tables, pluginTables),
513
+ entities: meta,
514
+ plugins: state.plugins,
515
+ pgSchema: state.pgSchema,
516
+ maxLimit: state.maxLimit,
517
+ validation: state.validation
518
+ };
519
+ }
520
+ function blueprintOf(state) {
521
+ return {
522
+ $types: void 0,
523
+ entity: defineEntity,
524
+ entities: (userEntities) => buildDefinition(state, userEntities)
525
+ };
526
+ }
527
+ function pluginsStepOf(settings, tables, pluginDefs) {
528
+ const plugins = resolvePlugins(pluginDefs, tables);
529
+ return {
530
+ plugins: (next) => pluginsStepOf(settings, tables, next),
531
+ relations: (userRelations) => {
532
+ const resolved = resolveRelations({
533
+ ...tablesOnly(tables),
534
+ ...pluginTablesOf(plugins)
535
+ }, (r) => fragmentsOf(r, userRelations, plugins));
536
+ return blueprintOf({
537
+ ...settings,
538
+ tables,
539
+ plugins,
540
+ userRelations,
541
+ resolved
542
+ });
543
+ }
544
+ };
545
+ }
546
+ function tablesStepOf(settings) {
547
+ return { tables: (tables) => {
548
+ for (const [key, value] of Object.entries(tables)) if (!is(value, PgTable) && !isPgEnum(value)) throw new ConfigError(`tables.${key} is neither a pg table nor a pg enum`);
549
+ return pluginsStepOf(settings, tables, []);
550
+ } };
551
+ }
552
+ function defineEngine(options = {}) {
553
+ const maxLimit = options.maxLimit ?? DEFAULT_MAX_LIMIT;
554
+ if (!Number.isInteger(maxLimit) || maxLimit < 1) throw new ConfigError("defineEngine: maxLimit must be a positive integer");
555
+ return tablesStepOf({
556
+ pgSchema: options.schema ?? typion,
557
+ validation: options.validation ?? null,
558
+ maxLimit
559
+ });
560
+ }
561
+ //#endregion
562
+ //#region src/features/multi-tenant.ts
563
+ const COLUMN$1 = "tenantId";
564
+ function columnOf$1(state) {
565
+ return state.column;
566
+ }
567
+ function fail$2(entity, message) {
568
+ throw new ConfigError(`Entity '${entity}': ${message}`, "config", { entity });
569
+ }
570
+ function tenantColumn(entity) {
571
+ const trait = entity.traits.multiTenant;
572
+ return trait === void 0 ? null : columnOf$1(trait.state);
573
+ }
574
+ function multiTenant(options) {
575
+ return defineFeaturePlugin({
576
+ id: "multiTenant",
577
+ needsContext: { $type: {} },
578
+ systemOptions: { $type: {} },
579
+ context: (base, input) => {
580
+ const unscoped = input.options?.unscoped;
581
+ if (unscoped !== void 0 && typeof unscoped !== "boolean") throw new BadRequestError("unscoped must be a boolean");
582
+ if (unscoped === true && base.mode !== "system") throw new BadRequestError("unscoped is an option of $system(): a caller always has a tenant");
583
+ return { unscoped: base.mode === "system" && (unscoped === true || input.callerLookup === true) };
584
+ },
585
+ traits: {
586
+ $types: {},
587
+ resolve: ({ entity, options: given, columns }) => {
588
+ const option = given.tenant;
589
+ if (option === void 0 || option === false) return null;
590
+ if (option !== true) fail$2(entity, "tenant must be true");
591
+ if (columns[COLUMN$1] === void 0) fail$2(entity, `tenant needs a column '${COLUMN$1}'`);
592
+ return {
593
+ state: { column: COLUMN$1 },
594
+ managed: [COLUMN$1]
595
+ };
596
+ },
597
+ partition: ({ entity, state, ctx }) => {
598
+ if (ctx.mode === "system" && ctx.unscoped === true) return void 0;
599
+ const tenant = ctx.tenantId;
600
+ if (tenant === null || tenant === void 0) {
601
+ const hint = ctx.mode === "system" ? " (pass it through $system({ ctx }) or cross tenants with $system({ unscoped: true }))" : "";
602
+ throw new ForbiddenError(`Entity '${entity.name}' belongs to a tenant, and the context has none${hint}`, {
603
+ entity: entity.name,
604
+ rule: "tenant"
605
+ });
606
+ }
607
+ return { [columnOf$1(state)]: tenant };
608
+ }
609
+ },
610
+ seed: { columns: (entity, { generators, values }) => {
611
+ const column = tenantColumn(entity);
612
+ if (column === null) return {};
613
+ const tenant = values.tenant ?? options.seedTenant;
614
+ if (tenant === void 0 && entity.columns[column]?.notNull === true) throw new Error(`Collection '${entity.name}' belongs to a tenant: give one with multiTenant({ seedTenant }) or defineConfig({ seed: { values: { tenant } } })`);
615
+ return { [column]: generators.default({ defaultValue: tenant ?? null }) };
616
+ } },
617
+ collection: {
618
+ $options: {},
619
+ $kinds: {},
620
+ consumes: ["tenant"],
621
+ columns: (collection) => collection.tenant === true ? { [COLUMN$1]: options.column() } : {},
622
+ entity: (collection) => collection.tenant === true ? { tenant: true } : {}
623
+ }
624
+ });
625
+ }
626
+ //#endregion
627
+ //#region src/features/soft-delete.ts
628
+ function softDeleteColumns() {
629
+ return { deletedAt: timestamp("deleted_at", { withTimezone: true }) };
630
+ }
631
+ function softUnique(name, deletedAt) {
632
+ return { on: (...cols) => uniqueIndex(name).on(...cols).where(sql`${deletedAt} is null`) };
633
+ }
634
+ const COLUMN = "deletedAt";
635
+ function columnOf(state) {
636
+ return state.column;
637
+ }
638
+ function fail$1(entity, message) {
639
+ throw new ConfigError(`Entity '${entity}': ${message}`, "config", { entity });
640
+ }
641
+ function softDeleteColumn(entity) {
642
+ const trait = entity.traits.softDelete;
643
+ return trait === void 0 ? null : columnOf(trait.state);
644
+ }
645
+ function softDelete() {
646
+ return defineFeaturePlugin({
647
+ id: "softDelete",
648
+ traits: {
649
+ $types: {},
650
+ options: {
651
+ read: { withDeleted: "boolean" },
652
+ delete: { hard: "boolean" }
653
+ },
654
+ resolve: ({ entity, options, columns }) => {
655
+ const option = options.softDelete;
656
+ if (option === void 0 || option === false) return null;
657
+ if (option !== true) fail$1(entity, "softDelete must be true");
658
+ const column = columns[COLUMN];
659
+ if (column === void 0 || column.dataType !== "object date" || column.notNull) fail$1(entity, `softDelete needs a nullable timestamp column '${COLUMN}'`);
660
+ return {
661
+ state: { column: COLUMN },
662
+ managed: [COLUMN]
663
+ };
664
+ },
665
+ filter: ({ state, operation, options }) => {
666
+ const column = columnOf(state);
667
+ if (operation === "restore") return { [column]: { isNotNull: true } };
668
+ if (options.withDeleted === true || options.hard === true) return void 0;
669
+ return { [column]: { isNull: true } };
670
+ },
671
+ remove: ({ state, options, now, update }) => options.hard === true ? void 0 : update({ [columnOf(state)]: now }),
672
+ operations: { restore: {
673
+ args: ["id"],
674
+ route: {
675
+ method: "POST",
676
+ path: ":id/restore"
677
+ },
678
+ run: ({ state, input, kit }) => kit.update(input.id, { [columnOf(state)]: null })
679
+ } }
680
+ },
681
+ seed: { columns: (entity, { generators }) => {
682
+ const column = softDeleteColumn(entity);
683
+ return column === null ? {} : { [column]: generators.default({ defaultValue: null }) };
684
+ } },
685
+ collection: {
686
+ $options: {},
687
+ $kinds: {},
688
+ consumes: ["softDelete"],
689
+ columns: (options) => options.softDelete === true ? softDeleteColumns() : {},
690
+ entity: (options) => options.softDelete === true ? { softDelete: true } : {}
691
+ }
692
+ });
693
+ }
694
+ //#endregion
695
+ //#region src/features/versions/history.ts
696
+ const STATUS = "status";
697
+ const PUBLISHED_AT = "publishedAt";
698
+ function col(table, key) {
699
+ return table[key];
700
+ }
701
+ function stateOf(state, doc) {
702
+ if (!state.drafts) return "published";
703
+ return doc["status"] === "published" ? "published" : "draft";
704
+ }
705
+ function entityData(entity, row) {
706
+ const out = {};
707
+ for (const key of Object.keys(entity.columns)) out[key] = row[key];
708
+ return out;
709
+ }
710
+ function generatedAlways(entity, key) {
711
+ const column = entity.columns[key]?.column;
712
+ return column?.generated?.type === "always" || column?.generatedIdentity?.type === "always";
713
+ }
714
+ function contentPatch(entity, from, to) {
715
+ const skip = /* @__PURE__ */ new Set([entity.pk.key, ...entity.managedKeys]);
716
+ const patch = {};
717
+ for (const key of Object.keys(entity.columns)) {
718
+ if (skip.has(key) || generatedAlways(entity, key)) continue;
719
+ if (entity.columns[key]?.column.onUpdateFn !== void 0) continue;
720
+ if (!equal(from[key], to[key])) patch[key] = to[key];
721
+ }
722
+ return patch;
723
+ }
724
+ function equal(a, b) {
725
+ if (Object.is(a, b)) return true;
726
+ if (a instanceof Date || b instanceof Date) return a instanceof Date && b instanceof Date && a.getTime() === b.getTime();
727
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
728
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
729
+ if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((item, i) => equal(item, b[i]));
730
+ const left = a;
731
+ const right = b;
732
+ const keys = /* @__PURE__ */ new Set([...Object.keys(left), ...Object.keys(right)]);
733
+ for (const key of keys) if (!equal(left[key], right[key])) return false;
734
+ return true;
735
+ }
736
+ async function latestVersion(tx, entity, state, id) {
737
+ const { table } = state;
738
+ return (await tx.select().from(table).where(and(eq(col(table, entity.pk.key), id), eq(col(table, "versionIsLatest"), true))).limit(1))[0] ?? null;
739
+ }
740
+ async function pendingDraft(tx, entity, state, row) {
741
+ if (!state.drafts || row["status"] !== "published") return null;
742
+ const latest = await latestVersion(tx, entity, state, row[entity.pk.key]);
743
+ return latest !== null && latest.versionState === "draft" ? entityData(entity, latest) : null;
744
+ }
745
+ async function recordVersion(tx, entity, state, doc, versionState, operation, ctx) {
746
+ const { table } = state;
747
+ const idColumn = col(table, entity.pk.key);
748
+ const id = doc[entity.pk.key];
749
+ const [next] = await tx.select({ value: sql`coalesce(max(${col(table, "versionNumber")}), 0) + 1`.mapWith(Number) }).from(table).where(eq(idColumn, id));
750
+ await tx.update(table).set({ versionIsLatest: false }).where(and(eq(idColumn, id), eq(col(table, "versionIsLatest"), true)));
751
+ const [row] = await tx.insert(table).values({
752
+ ...entityData(entity, doc),
753
+ versionNumber: next?.value ?? 1,
754
+ versionIsLatest: true,
755
+ versionState,
756
+ versionOperation: operation,
757
+ versionCreatedBy: ctx.actorId,
758
+ versionMode: ctx.mode,
759
+ versionSystemReason: ctx.systemReason,
760
+ versionRequestId: ctx.requestId
761
+ }).returning();
762
+ await prune(tx, entity, state, id);
763
+ return {
764
+ versionId: row?.versionId,
765
+ versionNumber: row?.versionNumber,
766
+ state: versionState
767
+ };
768
+ }
769
+ async function prune(tx, entity, state, id) {
770
+ if (state.max === null) return;
771
+ const { table } = state;
772
+ const rows = await tx.select({
773
+ id: col(table, "versionId"),
774
+ state: col(table, "versionState"),
775
+ latest: col(table, "versionIsLatest")
776
+ }).from(table).where(eq(col(table, entity.pk.key), id)).orderBy(desc(col(table, "versionNumber")));
777
+ if (rows.length <= state.max) return;
778
+ const keep = new Set(rows.slice(0, state.max).map((r) => r.id));
779
+ const lastPublished = rows.find((r) => r.state === "published");
780
+ if (lastPublished !== void 0) keep.add(lastPublished.id);
781
+ const latest = rows.find((r) => r.latest);
782
+ if (latest !== void 0) keep.add(latest.id);
783
+ const drop = rows.filter((r) => !keep.has(r.id)).map((r) => r.id);
784
+ if (drop.length > 0) await tx.delete(table).where(inArray(col(table, "versionId"), drop));
785
+ }
786
+ function toVersion(entity, row) {
787
+ const data = {};
788
+ for (const key of entity.visibleKeys) data[key] = row[key];
789
+ return {
790
+ versionId: row.versionId,
791
+ versionNumber: row.versionNumber,
792
+ state: row.versionState,
793
+ operation: row.versionOperation,
794
+ isLatest: row.versionIsLatest,
795
+ createdAt: row.versionCreatedAt,
796
+ createdBy: row.versionCreatedBy,
797
+ mode: row.versionMode,
798
+ systemReason: row.versionSystemReason,
799
+ requestId: row.versionRequestId,
800
+ data
801
+ };
802
+ }
803
+ //#endregion
804
+ //#region src/features/versions/table.ts
805
+ function draftColumns() {
806
+ return {
807
+ status: text("status", { enum: ["draft", "published"] }).notNull().default("draft"),
808
+ publishedAt: timestamp("published_at", { withTimezone: true })
809
+ };
810
+ }
811
+ const VERSION_STATES = ["draft", "published"];
812
+ const VERSION_MODES = ["user", "system"];
813
+ const VERSION_COLUMNS = () => ({
814
+ versionId: uuid("version_id").primaryKey().defaultRandom(),
815
+ versionNumber: integer("version_number").notNull(),
816
+ versionIsLatest: boolean("version_is_latest").notNull().default(false),
817
+ versionState: text("version_state", { enum: VERSION_STATES }).notNull(),
818
+ versionOperation: text("version_operation").notNull(),
819
+ versionCreatedAt: timestamp("version_created_at", { withTimezone: true }).notNull().defaultNow(),
820
+ versionCreatedBy: text("version_created_by"),
821
+ versionMode: text("version_mode", { enum: VERSION_MODES }).notNull(),
822
+ versionSystemReason: text("version_system_reason"),
823
+ versionRequestId: text("version_request_id")
824
+ });
825
+ function singlePrimaryKey(source) {
826
+ const tableName = getTableName(source);
827
+ if (getTableConfig(source).primaryKeys.length > 0) throw new ConfigError(`Versioned table '${tableName}' has a composite primary key; versions need a single-column pk`);
828
+ const pks = Object.entries(getTableColumns(source)).filter(([, c]) => c.primary);
829
+ const first = pks[0];
830
+ if (first === void 0 || pks.length !== 1) throw new ConfigError(`Versioned table '${tableName}' must have exactly one primary-key column (found ${pks.length})`);
831
+ return {
832
+ key: first[0],
833
+ column: first[1]
834
+ };
835
+ }
836
+ function buildVersionsTable(source, opts) {
837
+ const sourceName = getTableName(source);
838
+ const name = opts.name ?? `${sourceName}_versions`;
839
+ const pk = singlePrimaryKey(source);
840
+ const bookkeeping = VERSION_COLUMNS();
841
+ const cloned = {};
842
+ for (const [key, column] of Object.entries(getTableColumns(source))) {
843
+ if (key in bookkeeping || column.name.startsWith("version_")) throw new ConfigError(`Versioned table '${sourceName}': column '${key}' ('${column.name}') collides with the version_* bookkeeping columns`);
844
+ cloned[key] = cloneColumn(column, { keepDefault: key !== pk.key });
845
+ }
846
+ const columns = {
847
+ ...bookkeeping,
848
+ ...cloned
849
+ };
850
+ return opts.schema.table(name, columns, (t) => {
851
+ const id = t[pk.key];
852
+ const number = t.versionNumber;
853
+ const latest = t.versionIsLatest;
854
+ const createdAt = t.versionCreatedAt;
855
+ return [
856
+ foreignKey({
857
+ name: `${name}_doc_fk`,
858
+ columns: [id],
859
+ foreignColumns: [pk.column]
860
+ }).onDelete("cascade"),
861
+ unique(`${name}_doc_number`).on(id, number),
862
+ uniqueIndex(`${name}_latest`).on(id).where(sql`${latest}`),
863
+ index(`${name}_created`).on(id, createdAt.desc())
864
+ ];
865
+ });
866
+ }
867
+ //#endregion
868
+ //#region src/features/versions/index.ts
869
+ function fail(entity, message) {
870
+ throw new ConfigError(`Entity '${entity}': ${message}`, "config", { entity });
871
+ }
872
+ function parse(entity, option) {
873
+ if (option === void 0 || option === false) return null;
874
+ if (option !== true && (typeof option !== "object" || option === null)) fail(entity, "versions must be true or an object");
875
+ const opts = option === true ? {} : option;
876
+ for (const key of Object.keys(opts)) if (![
877
+ "drafts",
878
+ "max",
879
+ "onPendingDraft",
880
+ "table"
881
+ ].includes(key)) fail(entity, `versions.${key} is not an option`);
882
+ const { drafts, max, onPendingDraft, table } = opts;
883
+ if (drafts !== void 0 && typeof drafts !== "boolean") fail(entity, "versions.drafts must be a boolean");
884
+ if (max !== void 0 && (!Number.isInteger(max) || max < 1)) fail(entity, "versions.max must be a positive integer");
885
+ if (onPendingDraft !== void 0 && onPendingDraft !== "supersede" && onPendingDraft !== "reject") fail(entity, `versions.onPendingDraft must be 'supersede' or 'reject'`);
886
+ if (table !== void 0 && !is(table, PgTable)) fail(entity, "versions.table is not a pg table");
887
+ return {
888
+ drafts: drafts === true,
889
+ max: max ?? null,
890
+ onPendingDraft: onPendingDraft ?? "supersede",
891
+ table
892
+ };
893
+ }
894
+ function keyOf(entity) {
895
+ return `${entity}Versions`;
896
+ }
897
+ function versionsState(state) {
898
+ return state;
899
+ }
900
+ function versionsTable(entity) {
901
+ const trait = entity.traits.versions;
902
+ return trait === void 0 ? null : versionsState(trait.state).table;
903
+ }
904
+ function recordedAs(operation, draft) {
905
+ if (operation === "update") return draft ? "draft" : "update";
906
+ if (operation === "versions.restore") return "restoreVersion";
907
+ if (operation === "discardDraft") return "discard";
908
+ return operation;
909
+ }
910
+ function assertId(entity, id) {
911
+ if (id === void 0 || id === null) throw new BadRequestError(`${entity.name}: an id is required`, { entity: entity.name });
912
+ }
913
+ function history(entity, state, { executor, partition }, id) {
914
+ const { table } = state;
915
+ return {
916
+ executor,
917
+ table,
918
+ conditions: [eq(col(table, entity.pk.key), id), ...Object.entries(partition).map(([key, value]) => eq(col(table, key), value))]
919
+ };
920
+ }
921
+ async function loadVersion(entity, state, input, id, versionId) {
922
+ if (typeof versionId !== "string") throw new BadRequestError("versionId must be a string");
923
+ const { executor, table, conditions } = history(entity, state, input, id);
924
+ const row = (await executor.select().from(table).where(and(...conditions, eq(col(table, "versionId"), versionId))).limit(1))[0];
925
+ if (row === void 0) throw new NotFoundError(`Version '${versionId}' of ${entity.name} '${String(id)}' not found`, { entity: entity.name });
926
+ return row;
927
+ }
928
+ function readVersions(kit, fn) {
929
+ return kit.read(fn, { rule: "readVersions" });
930
+ }
931
+ function versions() {
932
+ return defineFeaturePlugin({
933
+ id: "versions",
934
+ traits: {
935
+ $types: {},
936
+ options: {
937
+ read: { draft: "boolean" },
938
+ create: {
939
+ draft: "boolean",
940
+ publish: "boolean"
941
+ },
942
+ update: { draft: "boolean" }
943
+ },
944
+ rules: {
945
+ readDrafts: "filter",
946
+ readVersions: "boolean",
947
+ publish: "filter"
948
+ },
949
+ hookArgs: {
950
+ change: { draft: false },
951
+ afterChange: {
952
+ previousDraft: null,
953
+ version: null
954
+ },
955
+ afterDelete: { version: null }
956
+ },
957
+ derive: ({ entity, table, options, schema }) => {
958
+ const parsed = parse(entity, options.versions);
959
+ if (parsed === null) return null;
960
+ return { [keyOf(entity)]: {
961
+ table: parsed.table ?? buildVersionsTable(table, { schema }),
962
+ mirror: true
963
+ } };
964
+ },
965
+ resolve: ({ entity, options, columns, derived }) => {
966
+ const parsed = parse(entity, options.versions);
967
+ if (parsed === null) return null;
968
+ if (parsed.drafts) {
969
+ for (const key of [STATUS, PUBLISHED_AT]) if (!(key in columns)) fail(entity, `versions.drafts needs a column '${key}' (add draftColumns() to the table)`);
970
+ const values = columns[STATUS]?.enumValues;
971
+ if (values !== void 0 && !(values.includes("draft") && values.includes("published"))) fail(entity, `versions.drafts needs status to allow 'draft' and 'published'`);
972
+ }
973
+ const key = keyOf(entity);
974
+ const state = {
975
+ key,
976
+ table: derived[key],
977
+ drafts: parsed.drafts,
978
+ max: parsed.max,
979
+ onPendingDraft: parsed.onPendingDraft
980
+ };
981
+ return parsed.drafts ? {
982
+ state,
983
+ managed: [STATUS, PUBLISHED_AT],
984
+ options: [
985
+ "read",
986
+ "create",
987
+ "update"
988
+ ]
989
+ } : {
990
+ state,
991
+ options: [],
992
+ rules: ["readVersions"]
993
+ };
994
+ },
995
+ filter: ({ state, writing }) => versionsState(state).drafts && !writing ? { [STATUS]: { eq: "published" } } : void 0,
996
+ source: ({ state, options }) => options.draft === true ? {
997
+ table: versionsState(state).key,
998
+ filter: { versionIsLatest: { eq: true } },
999
+ rule: "readDrafts"
1000
+ } : void 0,
1001
+ create: ({ state, options, row, now }) => {
1002
+ if (!versionsState(state).drafts) return void 0;
1003
+ const publish = options.publish === true;
1004
+ return {
1005
+ set: publish ? {
1006
+ [STATUS]: "published",
1007
+ [PUBLISHED_AT]: row["publishedAt"] ?? now
1008
+ } : {},
1009
+ rules: publish ? ["publish"] : [],
1010
+ args: { change: { draft: options.draft === true } }
1011
+ };
1012
+ },
1013
+ update: async ({ entity, state: raw, operation, options, row, tx }) => {
1014
+ const state = versionsState(raw);
1015
+ if (!state.drafts) return void 0;
1016
+ const pending = await pendingDraft(tx, entity, state, row);
1017
+ const after = { afterChange: { previousDraft: pending } };
1018
+ if (operation === "publish" || operation === "unpublish") return { args: after };
1019
+ if (operation === "discardDraft") return {
1020
+ base: pending ?? row,
1021
+ keep: true,
1022
+ args: after
1023
+ };
1024
+ if ((options.draft ?? operation === "versions.restore") === true && row["status"] === "published") return {
1025
+ base: pending ?? row,
1026
+ keep: true,
1027
+ args: {
1028
+ ...after,
1029
+ change: { draft: true }
1030
+ }
1031
+ };
1032
+ if (pending !== null && state.onPendingDraft === "reject") throw new InvalidStateError(`${entity.name} '${String(row[entity.pk.key])}' has a pending draft: publish or discard it first`, { entity: entity.name });
1033
+ return { args: after };
1034
+ },
1035
+ record: async ({ entity, state: raw, operation, step, row, written, tx, ctx }) => {
1036
+ const state = versionsState(raw);
1037
+ if (step === "delete" && written) return void 0;
1038
+ const draft = step === "update" && !written && state.drafts;
1039
+ return { version: await recordVersion(tx, entity, state, row, operation === "discardDraft" ? "published" : draft ? "draft" : stateOf(state, row), step === "update" ? recordedAs(operation, draft) : operation, ctx) };
1040
+ },
1041
+ operations: {
1042
+ publish: {
1043
+ args: ["id"],
1044
+ route: {
1045
+ method: "POST",
1046
+ path: ":id/publish"
1047
+ },
1048
+ run: ({ entity, state, input, kit }) => kit.update(input.id, async ({ row, tx, now }) => {
1049
+ const pending = await pendingDraft(tx, entity, versionsState(state), row);
1050
+ if (row["status"] === "published" && pending === null) return void 0;
1051
+ return {
1052
+ ...pending === null ? {} : contentPatch(entity, row, pending),
1053
+ [STATUS]: "published",
1054
+ [PUBLISHED_AT]: now
1055
+ };
1056
+ }, { rule: "publish" })
1057
+ },
1058
+ unpublish: {
1059
+ args: ["id"],
1060
+ route: {
1061
+ method: "POST",
1062
+ path: ":id/unpublish"
1063
+ },
1064
+ run: ({ entity, input, kit }) => kit.update(input.id, ({ row }) => {
1065
+ if (row["status"] !== "published") throw new InvalidStateError(`${entity.name} '${String(input.id)}' is not published`, { entity: entity.name });
1066
+ return { [STATUS]: "draft" };
1067
+ }, { rule: "publish" })
1068
+ },
1069
+ discardDraft: {
1070
+ args: ["id"],
1071
+ route: {
1072
+ method: "POST",
1073
+ path: ":id/discard-draft"
1074
+ },
1075
+ run: ({ entity, state, input, kit }) => kit.update(input.id, async ({ row, tx }) => {
1076
+ const pending = await pendingDraft(tx, entity, versionsState(state), row);
1077
+ return pending === null ? void 0 : contentPatch(entity, pending, row);
1078
+ })
1079
+ },
1080
+ "versions.list": {
1081
+ args: ["id"],
1082
+ options: {
1083
+ limit: "number",
1084
+ offset: "number",
1085
+ state: "string"
1086
+ },
1087
+ route: {
1088
+ method: "GET",
1089
+ path: ":id/versions"
1090
+ },
1091
+ run: ({ entity, state, input, kit }) => {
1092
+ assertId(entity, input.id);
1093
+ const limit = input.limit ?? entity.maxLimit;
1094
+ const offset = input.offset ?? 0;
1095
+ for (const [name, value] of [["limit", limit], ["offset", offset]]) if (typeof value !== "number" || !Number.isInteger(value) || value < 0) throw new BadRequestError(`${name} must be a non-negative integer`);
1096
+ if (limit > entity.maxLimit) throw new BadRequestError(`limit exceeds the maximum of ${entity.maxLimit}`);
1097
+ if (input.state !== void 0 && input.state !== "draft" && input.state !== "published") throw new BadRequestError(`state must be 'draft' or 'published'`);
1098
+ return readVersions(kit, async (read) => {
1099
+ const { executor, table, conditions } = history(entity, versionsState(state), read, input.id);
1100
+ return (await executor.select().from(table).where(and(...conditions, input.state === void 0 ? void 0 : eq(col(table, "versionState"), input.state))).orderBy(desc(col(table, "versionNumber"))).limit(limit).offset(offset)).map((row) => toVersion(entity, row));
1101
+ });
1102
+ }
1103
+ },
1104
+ "versions.get": {
1105
+ args: ["id", "versionId"],
1106
+ route: {
1107
+ method: "GET",
1108
+ path: ":id/versions/:versionId"
1109
+ },
1110
+ run: ({ entity, state, input, kit }) => {
1111
+ assertId(entity, input.id);
1112
+ return readVersions(kit, async (read) => toVersion(entity, await loadVersion(entity, versionsState(state), read, input.id, input.versionId)));
1113
+ }
1114
+ },
1115
+ "versions.diff": {
1116
+ args: [
1117
+ "id",
1118
+ "from",
1119
+ "to"
1120
+ ],
1121
+ route: {
1122
+ method: "GET",
1123
+ path: ":id/diff"
1124
+ },
1125
+ run: ({ entity, state, input, kit }) => {
1126
+ assertId(entity, input.id);
1127
+ return readVersions(kit, async (read) => {
1128
+ const before = await loadVersion(entity, versionsState(state), read, input.id, input.from);
1129
+ const after = await loadVersion(entity, versionsState(state), read, input.id, input.to);
1130
+ const out = [];
1131
+ for (const key of entity.visibleKeys) if (!equal(before[key], after[key])) out.push({
1132
+ field: key,
1133
+ before: before[key],
1134
+ after: after[key]
1135
+ });
1136
+ return out;
1137
+ });
1138
+ }
1139
+ },
1140
+ "versions.restore": {
1141
+ args: ["id", "versionId"],
1142
+ options: { draft: "boolean" },
1143
+ route: {
1144
+ method: "POST",
1145
+ path: ":id/versions/:versionId/restore"
1146
+ },
1147
+ run: ({ entity, state: raw, input, kit }) => {
1148
+ const state = versionsState(raw);
1149
+ if (typeof input.versionId !== "string") throw new BadRequestError("versionId must be a string");
1150
+ if (input.draft === true && !state.drafts) throw new UnsupportedError(`Entity '${entity.name}' has no drafts`, { entity: entity.name });
1151
+ return kit.update(input.id, async ({ row, tx }) => {
1152
+ const { table } = state;
1153
+ const version = (await tx.select().from(table).where(and(eq(col(table, entity.pk.key), input.id), eq(col(table, "versionId"), input.versionId))).limit(1))[0];
1154
+ if (version === void 0) throw new NotFoundError(`Version '${input.versionId}' of ${entity.name} '${String(input.id)}' not found`, { entity: entity.name });
1155
+ return contentPatch(entity, row, entityData(entity, version));
1156
+ }, { options: { draft: input.draft } });
1157
+ }
1158
+ }
1159
+ }
1160
+ },
1161
+ seed: { columns: (entity, { generators }) => {
1162
+ const trait = entity.traits.versions;
1163
+ return trait !== void 0 && versionsState(trait.state).drafts ? {
1164
+ [STATUS]: generators.default({ defaultValue: "published" }),
1165
+ [PUBLISHED_AT]: generators.default({ defaultValue: /* @__PURE__ */ new Date() })
1166
+ } : {};
1167
+ } },
1168
+ collection: {
1169
+ $options: {},
1170
+ $kinds: {},
1171
+ consumes: ["versions"],
1172
+ columns: (options) => typeof options.versions === "object" && options.versions !== null && options.versions.drafts === true ? draftColumns() : {},
1173
+ entity: (options) => options.versions === void 0 ? {} : { versions: options.versions }
1174
+ }
1175
+ });
1176
+ }
1177
+ //#endregion
1178
+ //#region src/features/index.ts
1179
+ const FEATURE_IDS = /* @__PURE__ */ new Set([
1180
+ "localization",
1181
+ "versions",
1182
+ "softDelete",
1183
+ "multiTenant"
1184
+ ]);
1185
+ function featurePlugins(options) {
1186
+ const plugins = [];
1187
+ if (options.localization !== void 0) plugins.push(localization(options.localization));
1188
+ plugins.push(versions(), softDelete());
1189
+ if (options.multiTenant !== void 0) plugins.push(multiTenant(options.multiTenant));
1190
+ return plugins;
1191
+ }
1192
+ //#endregion
1193
+ //#region src/define/setup.ts
1194
+ function defineSetup(options) {
1195
+ const plugins = featurePlugins(options);
1196
+ const ids = new Set(plugins.map((plugin) => plugin.id));
1197
+ for (const plugin of options.plugins ?? []) {
1198
+ if (FEATURE_IDS.has(plugin.id)) throw new ConfigError(`Plugin '${plugin.id}' has the name of a feature of the core`);
1199
+ if (ids.has(plugin.id)) throw new ConfigError(`Plugin '${plugin.id}' is installed twice`);
1200
+ ids.add(plugin.id);
1201
+ plugins.push(plugin);
1202
+ }
1203
+ checkDependencies(plugins);
1204
+ const out = { "~plugins": Object.freeze(plugins) };
1205
+ const localization = plugins.find((plugin) => plugin.id === "localization");
1206
+ if (localization !== void 0) out.localization = Object.freeze({
1207
+ locales: localization.locales,
1208
+ defaultLocale: localization.defaultLocale
1209
+ });
1210
+ return Object.freeze(out);
1211
+ }
1212
+ function defaultSetup() {
1213
+ return { "~plugins": featurePlugins({}) };
1214
+ }
1215
+ //#endregion
1216
+ //#region src/define/config.ts
1217
+ function enumsOf(tables) {
1218
+ const out = /* @__PURE__ */ new Map();
1219
+ for (const table of tables) for (const column of Object.values(getTableColumns(table))) {
1220
+ const candidate = column.enum;
1221
+ if (isPgEnum(candidate)) out.set(candidate, candidate.enumName);
1222
+ }
1223
+ return out;
1224
+ }
1225
+ function relationConfig(r, name, field, spec, collections, plainTables) {
1226
+ const where = `Relation '${name}.${field}'`;
1227
+ if (!collections.has(spec.target)) throw new ConfigError(`${where} targets '${spec.target}', which is not a collection of this config`);
1228
+ const source = r[name];
1229
+ const target = r[spec.target];
1230
+ const from = source[spec.from];
1231
+ const to = target[spec.to];
1232
+ if (from === void 0) throw new ConfigError(`${where}: collection '${name}' has no column '${spec.from}'`);
1233
+ if (to === void 0) throw new ConfigError(`${where}: collection '${spec.target}' has no column '${spec.to}'`);
1234
+ if (spec.kind === "one") return r.one[spec.target]({
1235
+ from,
1236
+ to,
1237
+ optional: spec.optional
1238
+ });
1239
+ const through = spec.through;
1240
+ if (through === void 0) return r.many[spec.target]({
1241
+ from,
1242
+ to
1243
+ });
1244
+ if (!plainTables.has(through.table)) throw new ConfigError(`${where} goes through '${through.table}', which is not in the config's tables (a collection cannot be the join: its rules would be bypassed)`);
1245
+ const join = r[through.table];
1246
+ const joinFrom = join[through.from];
1247
+ const joinTo = join[through.to];
1248
+ if (joinFrom === void 0 || joinTo === void 0) throw new ConfigError(`${where}: table '${through.table}' has no column '${joinFrom === void 0 ? through.from : through.to}'`);
1249
+ return r.many[spec.target]({
1250
+ from: from.through(joinFrom),
1251
+ to: to.through(joinTo)
1252
+ });
1253
+ }
1254
+ function defineConfig(options) {
1255
+ const plugins = (options.setup ?? defaultSetup())["~plugins"];
1256
+ const ids = new Set(plugins.map((plugin) => plugin.id));
1257
+ const collections = {};
1258
+ const tables = {};
1259
+ for (const entry of options.collections) {
1260
+ if (entry.name in collections) throw new ConfigError(`Collection '${entry.name}' is listed twice`);
1261
+ collections[entry.name] = entry;
1262
+ const table = buildCollectionTable(plugins, entry);
1263
+ tables[entry.name] = table;
1264
+ const tenant = entry["~runtime"].structure.tenant;
1265
+ if (tenant !== void 0 && tenant !== false && !ids.has("multiTenant")) throw new ConfigError(`Collection '${entry.name}' belongs to a tenant, and the setup configures no multi-tenancy (defineSetup({ multiTenant }))`);
1266
+ if (!ids.has("localization")) {
1267
+ for (const [key, column] of Object.entries(getTableColumns(table))) if (readColumnKind(column) === localizedKind.id) throw new ConfigError(`Collection '${entry.name}': column '${key}' is localized, and the setup configures no localization (defineSetup({ localization }))`);
1268
+ }
1269
+ }
1270
+ const plainTables = /* @__PURE__ */ new Set();
1271
+ for (const [key, value] of Object.entries(options.tables ?? {})) {
1272
+ if (key in collections) throw new ConfigError(`tables.${key} has the name of a collection`);
1273
+ tables[key] = value;
1274
+ if (is(value, PgTable)) plainTables.add(key);
1275
+ }
1276
+ const listed = new Set(Object.values(tables));
1277
+ for (const [found, enumName] of enumsOf(Object.values(tables).filter((value) => is(value, PgTable)))) {
1278
+ if (listed.has(found)) continue;
1279
+ if (enumName in tables) throw new ConfigError(`Enum '${enumName}' is used by a column, and the config already has a table or an enum under that key`);
1280
+ tables[enumName] = found;
1281
+ }
1282
+ const names = new Set(Object.keys(collections));
1283
+ const blueprint = defineEngine({
1284
+ maxLimit: options.maxLimit,
1285
+ validation: options.validation
1286
+ }).tables(tables).plugins(plugins).relations((r) => {
1287
+ const config = {};
1288
+ for (const [name, entry] of Object.entries(collections)) {
1289
+ const record = {};
1290
+ for (const [field, spec] of Object.entries(entry["~runtime"].relations)) record[field] = relationConfig(r, name, field, spec, names, plainTables);
1291
+ config[name] = record;
1292
+ }
1293
+ return config;
1294
+ });
1295
+ const entities = {};
1296
+ for (const [name, entry] of Object.entries(collections)) entities[name] = blueprint.entity(name, collectionEntityOptions(plugins, entry));
1297
+ const definition = blueprint.entities(entities);
1298
+ return {
1299
+ definition,
1300
+ schema: definition.schema,
1301
+ db: options.db,
1302
+ context: options.context,
1303
+ migrations: options.migrations,
1304
+ seed: options.seed,
1305
+ onAfterCommitError: options.onAfterCommitError,
1306
+ checkSchema: options.checkSchema
1307
+ };
1308
+ }
1309
+ //#endregion
1310
+ //#region src/db/adapter.ts
1311
+ function dbAdapter(connect, options = {}) {
1312
+ return {
1313
+ connect,
1314
+ check: () => {
1315
+ if ("url" in options && (options.url === void 0 || options.url === "")) throw new ConfigError("The config's database url is empty: set the variable it reads (DATABASE_URL?) in the environment or in .env");
1316
+ },
1317
+ ...options.close === void 0 ? {} : { close: options.close },
1318
+ ...options.url === void 0 ? {} : { credentials: () => ({ url: options.url }) }
1319
+ };
1320
+ }
1321
+ //#endregion
1322
+ export { BadRequestError, ConfigError, ConflictError, ForbiddenError, InternalError, InvalidStateError, NotFoundError, PluginError, TypionError, UnsupportedError, ValidationError, allowAll, attempt, collection, dbAdapter, defineConfig, definePlugin, defineSetup, getTypion, isTypionError, localized, localizedIndex, localizedUnique, localizedValue, softDeleteColumn, softUnique, structuralValidation, tenantColumn, versionsTable };
1323
+
1324
+ //# sourceMappingURL=index.js.map