@atscript/db 0.1.39 → 0.1.40

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 (64) hide show
  1. package/README.md +18 -18
  2. package/dist/agg.cjs +8 -3
  3. package/dist/agg.d.cts +7 -0
  4. package/dist/agg.d.mts +7 -0
  5. package/dist/agg.mjs +7 -3
  6. package/dist/control-DRgryKeg.cjs +14 -0
  7. package/dist/{control_as-bjmwe24C.mjs → control-IANbnfjG.mjs} +6 -18
  8. package/dist/db-readable-BQQzfguJ.d.cts +1249 -0
  9. package/dist/db-readable-Bbr4CjMb.d.mts +1249 -0
  10. package/dist/db-space-BUrQ5BFm.d.mts +309 -0
  11. package/dist/db-space-Vxpcnyt5.d.cts +309 -0
  12. package/dist/db-validator-plugin-07kDiis2.d.cts +22 -0
  13. package/dist/db-validator-plugin-CiqsHTI_.d.mts +22 -0
  14. package/dist/db-view-BntnAmXO.cjs +3071 -0
  15. package/dist/db-view-ZsoN91-q.mjs +2970 -0
  16. package/dist/index.cjs +95 -2801
  17. package/dist/index.d.cts +137 -0
  18. package/dist/index.d.mts +137 -0
  19. package/dist/index.mjs +55 -2761
  20. package/dist/{nested-writer-BkqL7cp3.cjs → nested-writer-BDXsDMPP.cjs} +196 -150
  21. package/dist/{nested-writer-NEN51mnR.mjs → nested-writer-Dmm1gbZV.mjs} +118 -70
  22. package/dist/ops-BdRAFLKY.d.mts +67 -0
  23. package/dist/ops-DXJ4Zw0P.d.cts +67 -0
  24. package/dist/ops.cjs +123 -0
  25. package/dist/ops.d.cts +2 -0
  26. package/dist/ops.d.mts +2 -0
  27. package/dist/ops.mjs +112 -0
  28. package/dist/plugin.cjs +90 -109
  29. package/dist/plugin.d.cts +6 -0
  30. package/dist/plugin.d.mts +6 -0
  31. package/dist/plugin.mjs +29 -49
  32. package/dist/rel.cjs +20 -20
  33. package/dist/rel.d.cts +119 -0
  34. package/dist/rel.d.mts +119 -0
  35. package/dist/rel.mjs +4 -5
  36. package/dist/{relation-helpers-guFL_oRf.cjs → relation-helpers-BYvsE1tR.cjs} +26 -22
  37. package/dist/{relation-helpers-DyBIlQnB.mjs → relation-helpers-CLasawQq.mjs} +11 -6
  38. package/dist/{relation-loader-Dv7qXYq7.mjs → relation-loader-BEOTXNcq.mjs} +63 -43
  39. package/dist/{relation-loader-CpnDRf9k.cjs → relation-loader-CRC5LcqM.cjs} +74 -49
  40. package/dist/shared.cjs +13 -13
  41. package/dist/{shared.d.ts → shared.d.cts} +14 -13
  42. package/dist/shared.d.mts +71 -0
  43. package/dist/shared.mjs +2 -3
  44. package/dist/sync.cjs +300 -252
  45. package/dist/sync.d.cts +369 -0
  46. package/dist/sync.d.mts +369 -0
  47. package/dist/sync.mjs +284 -233
  48. package/dist/{validation-utils-DEoCMmEb.cjs → validation-utils-DVJDijnB.cjs} +141 -109
  49. package/dist/{validation-utils-DhR_mtKa.mjs → validation-utils-DhjIjP1-.mjs} +71 -37
  50. package/package.json +30 -29
  51. package/LICENSE +0 -21
  52. package/dist/agg-BJFJ3dFQ.mjs +0 -8
  53. package/dist/agg-DnUWAOK8.cjs +0 -14
  54. package/dist/agg.d.ts +0 -13
  55. package/dist/chunk-CrpGerW8.cjs +0 -31
  56. package/dist/control_as-BFPERAF_.cjs +0 -28
  57. package/dist/index.d.ts +0 -1706
  58. package/dist/logger-B7oxCfLQ.mjs +0 -12
  59. package/dist/logger-Dt2v_-wb.cjs +0 -18
  60. package/dist/plugin.d.ts +0 -5
  61. package/dist/rel.d.ts +0 -1305
  62. package/dist/relation-loader-D4mTw6yH.cjs +0 -4
  63. package/dist/relation-loader-Ggy1ujwR.mjs +0 -4
  64. package/dist/sync.d.ts +0 -1878
package/dist/index.cjs CHANGED
@@ -1,2759 +1,55 @@
1
- "use strict";
2
- const require_chunk = require('./chunk-CrpGerW8.cjs');
3
- const require_nested_writer = require('./nested-writer-BkqL7cp3.cjs');
4
- const require_logger = require('./logger-Dt2v_-wb.cjs');
5
- const require_agg = require('./agg-DnUWAOK8.cjs');
6
- const require_relation_helpers = require('./relation-helpers-guFL_oRf.cjs');
7
- const __atscript_typescript_utils = require_chunk.__toESM(require("@atscript/typescript/utils"));
8
- const node_async_hooks = require_chunk.__toESM(require("node:async_hooks"));
9
- const __uniqu_core = require_chunk.__toESM(require("@uniqu/core"));
10
-
11
- //#region packages/db/src/table/table-metadata.ts
12
- function _define_property$6(obj, key, value) {
13
- if (key in obj) Object.defineProperty(obj, key, {
14
- value,
15
- enumerable: true,
16
- configurable: true,
17
- writable: true
18
- });
19
- else obj[key] = value;
20
- return obj;
21
- }
22
- const INDEX_PREFIX = "atscript__";
23
- function indexKey(type, name) {
24
- const cleanName = name.replace(/[^a-z0-9_.-]/gi, "_").replace(/_+/g, "_").slice(0, 127 - INDEX_PREFIX.length - type.length - 2);
25
- return `${INDEX_PREFIX}${type}__${cleanName}`;
26
- }
27
- function findAncestorInSet(path, set) {
28
- let pos = path.length;
29
- while ((pos = path.lastIndexOf(".", pos - 1)) !== -1) {
30
- const ancestor = path.slice(0, pos);
31
- if (set.has(ancestor)) return ancestor;
32
- }
33
- return undefined;
34
- }
35
- /** Returns true if `metadata` indicates a navigation relation field. */ function isNavRelation(metadata) {
36
- return metadata.has("db.rel.to") || metadata.has("db.rel.from") || metadata.has("db.rel.via");
37
- }
38
- var TableMetadata = class {
39
- get isBuilt() {
40
- return this._built;
41
- }
42
- /**
43
- * Runs the full metadata compilation pipeline. Called once by
44
- * `AtscriptDbReadable._ensureBuilt()` on first metadata access.
45
- *
46
- * Pipeline steps:
47
- * 1. `adapter.onBeforeFlatten(type)` — adapter hook
48
- * 2. `flattenAnnotatedType()` — collect field tuples, detect nav fields eagerly
49
- * 3. Replay non-nav-descendant tuples through annotation scanning + adapter.onFieldScanned
50
- * 4. Classify fields and build path maps (skipped for nested-objects adapters)
51
- * 5. `adapter.getMetadataOverrides()` → `_applyOverrides()` (PK/unique/inject adjustments)
52
- * 6. Build field descriptors (TDbFieldMeta[])
53
- * 7. Build leaf field indexes (skipped for nested-objects adapters)
54
- * 8. Finalize indexes (resolve field names to physical)
55
- * 9. `adapter.onAfterFlatten()` — adapter hook (read-only bookkeeping)
56
- * 10. Build allPhysicalFields list
57
- */ build(type, adapter, logger) {
58
- if (this._built) return;
59
- adapter.onBeforeFlatten?.(type);
60
- const collected = [];
61
- this.flatMap = (0, __atscript_typescript_utils.flattenAnnotatedType)(type, {
62
- topLevelArrayTag: adapter.getTopLevelArrayTag?.() ?? "db.__topLevelArray",
63
- excludePhantomTypes: true,
64
- onField: (path, fieldType, metadata) => {
65
- if (isNavRelation(metadata)) this.navFields.add(path);
66
- collected.push({
67
- path,
68
- type: fieldType,
69
- metadata
70
- });
71
- }
72
- });
73
- for (const entry of collected) {
74
- if (findAncestorInSet(entry.path, this.navFields) !== undefined) continue;
75
- this._scanGenericAnnotations(entry.path, entry.type, entry.metadata, logger);
76
- adapter.onFieldScanned?.(entry.path, entry.type, entry.metadata);
77
- }
78
- if (!this.nestedObjects) this._classifyFields();
79
- const overrides = adapter.getMetadataOverrides?.(this);
80
- if (overrides) this._applyOverrides(overrides);
81
- this._buildFieldDescriptors(adapter);
82
- if (!this.nestedObjects) this._buildLeafIndexes();
83
- this._finalizeIndexes();
84
- this._collateMap.clear();
85
- this._columnFromMap.clear();
86
- this.jsonFields.clear();
87
- this._built = true;
88
- adapter.onAfterFlatten?.();
89
- if (this.nestedObjects && this.flatMap) {
90
- for (const path of this.flatMap.keys()) if (path && !this.ignoredFields.has(path)) this.allPhysicalFields.push(path);
91
- } else for (const physical of this.pathToPhysical.values()) this.allPhysicalFields.push(physical);
92
- }
93
- /**
94
- * Applies adapter-provided metadata overrides atomically.
95
- * Processing order: injectFields → removePrimaryKeys → addPrimaryKeys → addUniqueFields.
96
- */ _applyOverrides(overrides) {
97
- if (overrides.injectFields) for (const { path, type } of overrides.injectFields) this.flatMap.set(path, type);
98
- if (overrides.removePrimaryKeys) for (const field of overrides.removePrimaryKeys) {
99
- const idx = this.primaryKeys.indexOf(field);
100
- if (idx >= 0) this.primaryKeys.splice(idx, 1);
101
- }
102
- if (overrides.addPrimaryKeys) {
103
- for (const field of overrides.addPrimaryKeys) if (!this.primaryKeys.includes(field)) this.primaryKeys.push(field);
104
- }
105
- if (overrides.addUniqueFields) for (const field of overrides.addUniqueFields) this.uniqueProps.add(field);
106
- }
107
- /**
108
- * Scans `@db.*` and `@meta.id` annotations on a field during flattening.
109
- */ _scanGenericAnnotations(fieldName, fieldType, metadata, logger) {
110
- if (metadata.has("meta.id")) {
111
- this.primaryKeys.push(fieldName);
112
- this.originalMetaIdFields.push(fieldName);
113
- }
114
- const column = metadata.get("db.column");
115
- if (column) this.columnMap.set(fieldName, column);
116
- const columnFrom = metadata.get("db.column.renamed");
117
- if (columnFrom) this._columnFromMap.set(fieldName, columnFrom);
118
- const resolvedDefault = resolveDefaultFromMetadata(metadata);
119
- if (resolvedDefault) this.defaults.set(fieldName, resolvedDefault);
120
- if (metadata.has("db.ignore")) this.ignoredFields.add(fieldName);
121
- if (isNavRelation(metadata)) {
122
- this.navFields.add(fieldName);
123
- this.ignoredFields.add(fieldName);
124
- const direction = metadata.has("db.rel.to") ? "to" : metadata.has("db.rel.from") ? "from" : "via";
125
- const raw = direction === "via" ? metadata.get("db.rel.via") : metadata.get(`db.rel.${direction}`);
126
- const alias = raw === true || typeof raw === "function" ? undefined : raw;
127
- const isArr = fieldType.type.kind === "array";
128
- const elementType = isArr ? fieldType.type.of : fieldType;
129
- const resolveTarget = () => elementType?.ref?.type() ?? elementType;
130
- this.relations.set(fieldName, {
131
- direction,
132
- alias,
133
- targetType: resolveTarget,
134
- isArray: isArr,
135
- ...direction === "via" ? { viaType: raw } : {}
136
- });
137
- }
138
- if (metadata.has("db.rel.FK")) {
139
- const raw = metadata.get("db.rel.FK");
140
- const alias = raw === true ? undefined : raw;
141
- if (fieldType.ref) {
142
- const refTarget = fieldType.ref.type();
143
- const targetTable = refTarget?.metadata?.get("db.table") || refTarget?.id || "";
144
- const targetField = fieldType.ref.field;
145
- const key = alias || `__auto_${fieldName}`;
146
- const existing = this.foreignKeys.get(key);
147
- if (existing) {
148
- existing.fields.push(fieldName);
149
- existing.targetFields.push(targetField);
150
- } else this.foreignKeys.set(key, {
151
- fields: [fieldName],
152
- targetTable,
153
- targetFields: [targetField],
154
- targetTypeRef: fieldType.ref.type,
155
- alias
156
- });
157
- }
158
- }
159
- const onDelete = metadata.get("db.rel.onDelete");
160
- const onUpdate = metadata.get("db.rel.onUpdate");
161
- if (onDelete || onUpdate) {
162
- for (const fk of this.foreignKeys.values()) if (fk.fields.includes(fieldName)) {
163
- if (onDelete) fk.onDelete = onDelete;
164
- if (onUpdate) fk.onUpdate = onUpdate;
165
- break;
166
- }
167
- }
168
- for (const index of metadata.get("db.index.plain") || []) {
169
- const name = index === true ? fieldName : index?.name || fieldName;
170
- const sort = (index === true ? undefined : index?.sort) || "asc";
171
- this._addIndexField("plain", name, fieldName, { sort });
172
- }
173
- for (const index of metadata.get("db.index.unique") || []) {
174
- const name = index === true ? fieldName : typeof index === "string" ? index : index?.name || fieldName;
175
- this._addIndexField("unique", name, fieldName);
176
- }
177
- for (const index of metadata.get("db.index.fulltext") || []) {
178
- const name = index === true ? fieldName : typeof index === "string" ? index : index?.name || fieldName;
179
- const weight = index !== true && typeof index === "object" ? index?.weight : undefined;
180
- this._addIndexField("fulltext", name, fieldName, { weight });
181
- }
182
- const collate = metadata.get("db.column.collate");
183
- if (collate) this._collateMap.set(fieldName, collate);
184
- const hasExplicitIndex = metadata.has("db.index.plain") || metadata.has("db.index.unique") || metadata.has("db.index.fulltext");
185
- if (metadata.has("db.json")) {
186
- this.jsonFields.add(fieldName);
187
- if (hasExplicitIndex) logger.warn(`@db.index on a @db.json field "${fieldName}" — most databases cannot index into JSON columns`);
188
- }
189
- if (metadata.has("db.column.dimension")) {
190
- this.dimensions.push(fieldName);
191
- if (!hasExplicitIndex) this._addIndexField("plain", fieldName, fieldName);
192
- }
193
- if (metadata.has("db.column.measure")) this.measures.push(fieldName);
194
- }
195
- _addIndexField(type, name, field, opts) {
196
- const key = indexKey(type, name);
197
- const index = this.indexes.get(key);
198
- const indexField = {
199
- name: field,
200
- sort: opts?.sort ?? "asc"
201
- };
202
- if (opts?.weight !== undefined) indexField.weight = opts.weight;
203
- if (index) index.fields.push(indexField);
204
- else this.indexes.set(key, {
205
- key,
206
- name,
207
- type,
208
- fields: [indexField]
209
- });
210
- }
211
- /**
212
- * Classifies each field as column, flattened, json, or parent-object.
213
- * Builds the bidirectional pathToPhysical / physicalToPath maps.
214
- */ _classifyFields() {
215
- for (const [path, type] of this.flatMap.entries()) {
216
- if (!path) continue;
217
- const designType = resolveDesignType(type);
218
- const isJson = this.jsonFields.has(path);
219
- const isArray = designType === "array";
220
- const isObject = designType === "object";
221
- if (isArray) this.jsonFields.add(path);
222
- else if (isObject && isJson) {} else if (isObject && !isJson) this.flattenedParents.add(path);
223
- }
224
- for (const ignoredField of this.ignoredFields) if (this.flattenedParents.has(ignoredField)) {
225
- const prefix = `${ignoredField}.`;
226
- for (const path of this.flatMap.keys()) if (path.startsWith(prefix)) this.ignoredFields.add(path);
227
- }
228
- for (const parentPath of this.flattenedParents) if (this.columnMap.has(parentPath)) throw new Error(`@db.column cannot rename a flattened object field "${parentPath}" — ` + `apply @db.column to individual nested fields, or use @db.json to store as a single column`);
229
- for (const [path] of this.flatMap.entries()) {
230
- if (!path) continue;
231
- if (this.flattenedParents.has(path)) continue;
232
- if (findAncestorInSet(path, this.jsonFields) !== undefined) continue;
233
- const isFlattened = findAncestorInSet(path, this.flattenedParents) !== undefined;
234
- const columnOverride = this.columnMap.get(path);
235
- let physicalName;
236
- if (columnOverride) physicalName = isFlattened ? this._flattenedPrefix(path) + columnOverride : columnOverride;
237
- else physicalName = isFlattened ? path.replace(/\./g, "__") : path;
238
- this.pathToPhysical.set(path, physicalName);
239
- this.physicalToPath.set(physicalName, path);
240
- const fieldType = this.flatMap.get(path);
241
- if (fieldType) {
242
- const dt = resolveDesignType(fieldType);
243
- if (dt === "boolean") this.booleanFields.add(physicalName);
244
- else if (dt === "decimal") this.decimalFields.add(physicalName);
245
- }
246
- }
247
- for (const parentPath of this.flattenedParents) {
248
- const prefix = `${parentPath}.`;
249
- const leaves = [];
250
- for (const [path, physical] of this.pathToPhysical) if (path.startsWith(prefix)) leaves.push(physical);
251
- if (leaves.length > 0) this.selectExpansion.set(parentPath, leaves);
252
- }
253
- this.requiresMappings = this.flattenedParents.size > 0 || this.jsonFields.size > 0;
254
- }
255
- /** Returns the `__`-separated parent prefix for a dot-separated path, or empty string for top-level paths. */ _flattenedPrefix(path) {
256
- const lastDot = path.lastIndexOf(".");
257
- return lastDot >= 0 ? `${path.slice(0, lastDot).replace(/\./g, "__")}__` : "";
258
- }
259
- /**
260
- * Indexes `fieldDescriptors` into two lookup maps for unified
261
- * read/write field classification in the RelationalFieldMapper.
262
- */ _buildLeafIndexes() {
263
- for (const fd of this.fieldDescriptors) {
264
- if (fd.ignored) continue;
265
- this.leafByPhysical.set(fd.physicalName, fd);
266
- this.leafByLogical.set(fd.path, fd);
267
- }
268
- }
269
- /**
270
- * Builds field descriptors, physical-name lookup, and value formatters.
271
- * Called once during build() — everything it needs
272
- * (flatMap, indexes, columnMap, etc.) is already populated.
273
- */ _buildFieldDescriptors(adapter) {
274
- const descriptors = [];
275
- const skipFlattening = this.nestedObjects;
276
- const indexedFields = new Set();
277
- for (const index of this.indexes.values()) for (const f of index.fields) indexedFields.add(f.name);
278
- for (const [path, type] of this.flatMap.entries()) {
279
- if (!path) continue;
280
- if (!skipFlattening && this.flattenedParents.has(path)) continue;
281
- if (!skipFlattening && findAncestorInSet(path, this.jsonFields) !== undefined) continue;
282
- const isJson = this.jsonFields.has(path);
283
- const isFlattened = !skipFlattening && findAncestorInSet(path, this.flattenedParents) !== undefined;
284
- const designType = isJson ? "json" : resolveDesignType(type);
285
- let storage;
286
- if (skipFlattening) storage = "column";
287
- else if (isJson) storage = "json";
288
- else if (isFlattened) storage = "flattened";
289
- else storage = "column";
290
- const physicalName = skipFlattening ? this.columnMap.get(path) ?? path : this.pathToPhysical.get(path) ?? this.columnMap.get(path) ?? path;
291
- const fromLocal = this._columnFromMap.get(path);
292
- let renamedFrom;
293
- if (fromLocal) renamedFrom = isFlattened ? this._flattenedPrefix(path) + fromLocal : fromLocal;
294
- descriptors.push({
295
- path,
296
- type,
297
- physicalName,
298
- designType,
299
- optional: type.optional === true,
300
- isPrimaryKey: this.primaryKeys.includes(path),
301
- ignored: this.ignoredFields.has(path),
302
- defaultValue: this.defaults.get(path),
303
- storage,
304
- flattenedFrom: isFlattened ? path : undefined,
305
- renamedFrom,
306
- collate: this._collateMap.get(path),
307
- isIndexed: indexedFields.has(path) || undefined
308
- });
309
- }
310
- this._resolveFkTargetFields(descriptors);
311
- const fmtHook = adapter.formatValue?.bind(adapter);
312
- if (fmtHook) for (const fd of descriptors) {
313
- const fmt = fmtHook(fd);
314
- if (fmt) if (typeof fmt === "function") {
315
- if (!this.toStorageFormatters) this.toStorageFormatters = new Map();
316
- this.toStorageFormatters.set(fd.physicalName, fmt);
317
- } else {
318
- if (fmt.toStorage) {
319
- if (!this.toStorageFormatters) this.toStorageFormatters = new Map();
320
- this.toStorageFormatters.set(fd.physicalName, fmt.toStorage);
321
- }
322
- if (fmt.fromStorage) {
323
- if (!this.fromStorageFormatters) this.fromStorageFormatters = new Map();
324
- this.fromStorageFormatters.set(fd.physicalName, fmt.fromStorage);
325
- }
326
- }
327
- }
328
- Object.freeze(descriptors);
329
- this.fieldDescriptors = descriptors;
330
- }
331
- /**
332
- * Resolves `fkTargetField` for FK fields in field descriptors.
333
- */ _resolveFkTargetFields(descriptors) {
334
- if (this.foreignKeys.size === 0) return;
335
- const fkFieldToTarget = new Map();
336
- for (const fk of this.foreignKeys.values()) {
337
- if (!fk.targetTypeRef) continue;
338
- for (let i = 0; i < fk.fields.length; i++) fkFieldToTarget.set(fk.fields[i], {
339
- targetTypeRef: fk.targetTypeRef,
340
- targetField: fk.targetFields[i]
341
- });
342
- }
343
- if (fkFieldToTarget.size === 0) return;
344
- const flatCache = new Map();
345
- for (const descriptor of descriptors) {
346
- const target = fkFieldToTarget.get(descriptor.path);
347
- if (!target) continue;
348
- const targetType = target.targetTypeRef();
349
- if (!targetType) continue;
350
- let targetFlatMap = flatCache.get(targetType);
351
- if (!targetFlatMap) {
352
- targetFlatMap = (0, __atscript_typescript_utils.flattenAnnotatedType)(targetType);
353
- flatCache.set(targetType, targetFlatMap);
354
- }
355
- const targetFieldType = targetFlatMap.get(target.targetField);
356
- if (!targetFieldType) continue;
357
- const targetMetadata = targetFieldType.metadata;
358
- descriptor.fkTargetField = {
359
- path: target.targetField,
360
- type: targetFieldType,
361
- physicalName: target.targetField,
362
- designType: resolveDesignType(targetFieldType),
363
- optional: false,
364
- isPrimaryKey: targetMetadata?.has("meta.id") ?? false,
365
- ignored: false,
366
- storage: "column",
367
- defaultValue: targetMetadata ? resolveDefaultFromMetadata(targetMetadata) : undefined,
368
- collate: targetMetadata?.get("db.column.collate")
369
- };
370
- }
371
- }
372
- _finalizeIndexes() {
373
- for (const index of this.indexes.values()) if (index.type === "unique" && index.fields.length === 1) this.uniqueProps.add(index.fields[0].name);
374
- for (const index of this.indexes.values()) for (const field of index.fields) field.name = this.pathToPhysical.get(field.name) ?? this.columnMap.get(field.name) ?? field.name;
375
- }
376
- constructor(nestedObjects) {
377
- _define_property$6(this, "nestedObjects", void 0);
378
- _define_property$6(this, "flatMap", void 0);
379
- _define_property$6(this, "fieldDescriptors", void 0);
380
- _define_property$6(this, "primaryKeys", []);
381
- _define_property$6(this, "originalMetaIdFields", []);
382
- _define_property$6(this, "indexes", new Map());
383
- _define_property$6(this, "foreignKeys", new Map());
384
- _define_property$6(this, "relations", new Map());
385
- _define_property$6(this, "navFields", new Set());
386
- _define_property$6(this, "ignoredFields", new Set());
387
- _define_property$6(this, "uniqueProps", new Set());
388
- _define_property$6(this, "defaults", new Map());
389
- _define_property$6(this, "columnMap", new Map());
390
- _define_property$6(this, "dimensions", []);
391
- _define_property$6(this, "measures", []);
392
- _define_property$6(this, "pathToPhysical", new Map());
393
- _define_property$6(this, "physicalToPath", new Map());
394
- _define_property$6(this, "flattenedParents", new Set());
395
- _define_property$6(this, "jsonFields", new Set());
396
- _define_property$6(this, "selectExpansion", new Map());
397
- _define_property$6(this, "booleanFields", new Set());
398
- _define_property$6(this, "decimalFields", new Set());
399
- _define_property$6(this, "allPhysicalFields", []);
400
- _define_property$6(this, "requiresMappings", false);
401
- _define_property$6(this, "toStorageFormatters", void 0);
402
- _define_property$6(this, "fromStorageFormatters", void 0);
403
- /** Leaf field descriptors indexed by physical column name (read path). */ _define_property$6(this, "leafByPhysical", new Map());
404
- /** Leaf field descriptors indexed by logical path (write/patch/filter paths). */ _define_property$6(this, "leafByLogical", new Map());
405
- _define_property$6(this, "_built", false);
406
- _define_property$6(this, "_collateMap", new Map());
407
- _define_property$6(this, "_columnFromMap", new Map());
408
- this.nestedObjects = nestedObjects;
409
- }
410
- };
411
-
412
- //#endregion
413
- //#region packages/db/src/query/uniqu-select.ts
414
- function _define_property$5(obj, key, value) {
415
- if (key in obj) Object.defineProperty(obj, key, {
416
- value,
417
- enumerable: true,
418
- configurable: true,
419
- writable: true
420
- });
421
- else obj[key] = value;
422
- return obj;
423
- }
424
- var UniquSelect = class UniquSelect {
425
- /** Type guard: checks if a value is an AggregateExpr ({$fn, $field}). */ static _isAggregateExpr(v) {
426
- return typeof v === "object" && v !== null && "$fn" in v && "$field" in v;
427
- }
428
- /**
429
- * Resolved inclusion array of plain field names (strings only).
430
- * AggregateExpr objects are filtered out.
431
- * For exclusion form, inverts using `allFields` from constructor.
432
- */ get asArray() {
433
- if (this._array !== UniquSelect.UNRESOLVED) return this._array;
434
- if (Array.isArray(this._raw)) {
435
- this._array = this._raw.filter((item) => typeof item === "string");
436
- return this._array;
437
- }
438
- const raw = this._raw;
439
- const entries = Object.entries(raw);
440
- if (entries.length === 0) {
441
- this._array = undefined;
442
- return undefined;
443
- }
444
- if (entries[0][1] === 1) this._array = entries.filter((e) => e[1] === 1).map((e) => e[0]);
445
- else if (this._allFields) {
446
- const excluded = new Set(entries.filter((e) => e[1] === 0).map((e) => e[0]));
447
- this._array = this._allFields.filter((f) => !excluded.has(f));
448
- } else this._array = undefined;
449
- return this._array;
450
- }
451
- /**
452
- * Record projection preserving original semantics.
453
- * Returns original object as-is if raw was object.
454
- * Converts `string[]` to `{field: 1}` inclusion object.
455
- * AggregateExpr objects in array form are ignored.
456
- */ get asProjection() {
457
- if (this._projection !== UniquSelect.UNRESOLVED) return this._projection;
458
- if (!Array.isArray(this._raw)) {
459
- const raw = this._raw;
460
- this._projection = Object.keys(raw).length === 0 ? undefined : raw;
461
- return this._projection;
462
- }
463
- const strings = this.asArray;
464
- if (!strings || strings.length === 0) {
465
- this._projection = undefined;
466
- return undefined;
467
- }
468
- const result = {};
469
- for (const item of strings) result[item] = 1;
470
- this._projection = result;
471
- return this._projection;
472
- }
473
- /**
474
- * Extracts AggregateExpr entries from array-form $select.
475
- * Returns undefined if no aggregates present or if $select is object form.
476
- */ get aggregates() {
477
- if (this._aggregates !== UniquSelect.UNRESOLVED) return this._aggregates;
478
- if (!Array.isArray(this._raw)) {
479
- this._aggregates = undefined;
480
- return undefined;
481
- }
482
- const aggs = this._raw.filter(UniquSelect._isAggregateExpr);
483
- this._aggregates = aggs.length > 0 ? aggs : undefined;
484
- return this._aggregates;
485
- }
486
- /** Whether the $select contains any AggregateExpr entries. */ get hasAggregates() {
487
- return !!this.aggregates?.length;
488
- }
489
- constructor(raw, allFields) {
490
- _define_property$5(this, "_raw", void 0);
491
- _define_property$5(this, "_allFields", void 0);
492
- _define_property$5(this, "_array", UniquSelect.UNRESOLVED);
493
- _define_property$5(this, "_projection", UniquSelect.UNRESOLVED);
494
- _define_property$5(this, "_aggregates", UniquSelect.UNRESOLVED);
495
- this._raw = raw;
496
- this._allFields = allFields;
497
- }
498
- };
499
- _define_property$5(UniquSelect, "UNRESOLVED", Symbol("unresolved"));
500
-
501
- //#endregion
502
- //#region packages/db/src/strategies/field-mapping.ts
503
- function toBool(value) {
504
- if (value === null || value === undefined) return value;
505
- return !!value;
506
- }
507
- function toDecimalString(value) {
508
- if (value === null || value === undefined) return value;
509
- if (typeof value === "string") return value;
510
- if (typeof value === "number") return String(value);
511
- return value;
512
- }
513
- var FieldMappingStrategy = class {
514
- /**
515
- * Recursively walks a filter expression, applying adapter-specific value
516
- * formatting via `formatFilterValue`. Shared by both document and relational
517
- * mappers (relational adds key-renaming via `translateFilterWithRename`).
518
- */ translateFilter(filter, meta) {
519
- if (!filter || typeof filter !== "object") return filter;
520
- if (!meta.toStorageFormatters) return filter;
521
- const result = {};
522
- for (const [key, value] of Object.entries(filter)) if (key === "$and" || key === "$or") result[key] = value.map((f) => this.translateFilter(f, meta));
523
- else if (key === "$not") result[key] = this.translateFilter(value, meta);
524
- else if (key.startsWith("$")) result[key] = value;
525
- else result[key] = this.formatFilterValue(key, value, meta);
526
- return result;
527
- }
528
- /**
529
- * Coerces field values from storage representation to JS types
530
- * (booleans from 0/1, decimals from number to string).
531
- */ coerceFieldValues(row, meta) {
532
- if (meta.booleanFields.size === 0 && meta.decimalFields.size === 0) return row;
533
- for (const field of meta.booleanFields) if (field in row) row[field] = toBool(row[field]);
534
- for (const field of meta.decimalFields) if (field in row) row[field] = toDecimalString(row[field]);
535
- return row;
536
- }
537
- /**
538
- * Applies adapter-specific fromStorage formatting to a row read from the database.
539
- * Converts storage representations back to JS values (e.g. Date → epoch ms).
540
- */ applyFromStorageFormatters(row, meta) {
541
- if (!meta.fromStorageFormatters) return row;
542
- for (const [col, fmt] of meta.fromStorageFormatters) {
543
- const val = row[col];
544
- if (val !== null && val !== undefined) row[col] = fmt(val);
545
- }
546
- return row;
547
- }
548
- /**
549
- * Sets a value at a dot-notation path, creating intermediate objects as needed.
550
- */ setNestedValue(obj, dotPath, value) {
551
- const parts = dotPath.split(".");
552
- let current = obj;
553
- for (let i = 0; i < parts.length - 1; i++) {
554
- const part = parts[i];
555
- if (current[part] === undefined || current[part] === null) current[part] = {};
556
- current = current[part];
557
- }
558
- current[parts[parts.length - 1]] = value;
559
- }
560
- /**
561
- * If all children of a flattened parent are null, collapse the parent to null.
562
- */ reconstructNullParent(obj, parentPath, meta) {
563
- const parts = parentPath.split(".");
564
- let current = obj;
565
- for (let i = 0; i < parts.length - 1; i++) {
566
- if (current[parts[i]] === undefined) return;
567
- current = current[parts[i]];
568
- }
569
- const lastPart = parts[parts.length - 1];
570
- const parentObj = current[lastPart];
571
- if (typeof parentObj !== "object" || parentObj === null) return;
572
- let allNull = true;
573
- const parentKeys = Object.keys(parentObj);
574
- for (const k of parentKeys) {
575
- const v = parentObj[k];
576
- if (v !== null && v !== undefined) {
577
- allNull = false;
578
- break;
579
- }
580
- }
581
- if (allNull) {
582
- const parentType = meta.flatMap?.get(parentPath);
583
- current[lastPart] = parentType?.optional ? null : {};
584
- }
585
- }
586
- /**
587
- * Applies adapter-specific value formatting to a single filter value.
588
- * Handles direct values, operator objects ({$gt: v}), and $in/$nin arrays.
589
- */ formatFilterValue(physicalName, value, meta) {
590
- const fmt = meta.toStorageFormatters?.get(physicalName);
591
- if (!fmt) return value;
592
- if (value === null || value === undefined) return value;
593
- if (typeof value !== "object") return fmt(value);
594
- const ops = value;
595
- const formatted = {};
596
- for (const [op, opVal] of Object.entries(ops)) if ((op === "$in" || op === "$nin") && Array.isArray(opVal)) formatted[op] = opVal.map((v) => v === null || v === undefined ? v : fmt(v));
597
- else if (op.startsWith("$") && opVal !== null && opVal !== undefined) formatted[op] = fmt(opVal);
598
- else formatted[op] = opVal;
599
- return formatted;
600
- }
601
- /**
602
- * Applies adapter-specific value formatting to prepared (physical-named) data.
603
- */ formatWriteValues(data, meta) {
604
- if (!meta.toStorageFormatters) return data;
605
- for (const [col, fmt] of meta.toStorageFormatters) {
606
- const val = data[col];
607
- if (val !== null && val !== undefined) data[col] = fmt(val);
608
- }
609
- return data;
610
- }
611
- /**
612
- * Prepares primary key values and strips ignored fields.
613
- * Shared pre-processing for both document and relational write paths.
614
- */ prepareCommon(data, meta, adapter) {
615
- for (const pk of meta.primaryKeys) if (data[pk] !== undefined) {
616
- const fieldType = meta.flatMap?.get(pk);
617
- if (fieldType) data[pk] = adapter.prepareId(data[pk], fieldType);
618
- }
619
- for (const field of meta.ignoredFields) if (!field.includes(".")) delete data[field];
620
- }
621
- };
622
- var DocumentFieldMapper = class extends FieldMappingStrategy {
623
- reconstructFromRead(row, meta) {
624
- return this.applyFromStorageFormatters(this.coerceFieldValues(row, meta), meta);
625
- }
626
- translateQuery(query, meta) {
627
- const controls = query.controls;
628
- return {
629
- filter: meta.toStorageFormatters ? this.translateFilter(query.filter, meta) : query.filter,
630
- controls: {
631
- ...controls,
632
- $with: undefined,
633
- $select: controls?.$select ? new UniquSelect(controls.$select, meta.allPhysicalFields) : undefined
634
- },
635
- insights: query.insights
636
- };
637
- }
638
- translateAggregateQuery(query, meta) {
639
- const controls = query.controls;
640
- return {
641
- filter: meta.toStorageFormatters ? this.translateFilter(query.filter, meta) : query.filter ?? {},
642
- controls: {
643
- ...controls,
644
- $with: undefined,
645
- $select: controls.$select ? new UniquSelect(controls.$select, meta.allPhysicalFields) : undefined
646
- },
647
- insights: query.insights
648
- };
649
- }
650
- prepareForWrite(payload, meta, adapter) {
651
- const data = { ...payload };
652
- this.prepareCommon(data, meta, adapter);
653
- for (const [logical, physical] of meta.columnMap.entries()) if (logical in data) {
654
- data[physical] = data[logical];
655
- delete data[logical];
656
- }
657
- return this.formatWriteValues(data, meta);
658
- }
659
- translatePatchKeys(update, meta) {
660
- return this.formatWriteValues(update, meta);
661
- }
662
- };
663
-
664
- //#endregion
665
- //#region packages/db/src/strategies/relational-field-mapper.ts
666
- var RelationalFieldMapper = class extends FieldMappingStrategy {
667
- reconstructFromRead(row, meta) {
668
- if (!meta.requiresMappings) return this.applyFromStorageFormatters(this.coerceFieldValues(row, meta), meta);
669
- const result = {};
670
- const fromFmts = meta.fromStorageFormatters;
671
- for (const physical of Object.keys(row)) {
672
- const fd = meta.leafByPhysical.get(physical);
673
- if (!fd) {
674
- result[physical] = row[physical];
675
- continue;
676
- }
677
- let raw = row[physical];
678
- const fromFmt = fromFmts?.get(physical);
679
- if (fromFmt && raw !== null && raw !== undefined) raw = fromFmt(raw);
680
- const value = fd.designType === "boolean" ? toBool(raw) : fd.designType === "decimal" ? toDecimalString(raw) : raw;
681
- if (fd.storage === "json") this.setNestedValue(result, fd.path, typeof value === "string" ? JSON.parse(value) : value);
682
- else if (fd.storage === "flattened") this.setNestedValue(result, fd.path, value);
683
- else result[fd.path] = value;
684
- }
685
- for (const parentPath of meta.flattenedParents) this.reconstructNullParent(result, parentPath, meta);
686
- return result;
687
- }
688
- translateQuery(query, meta) {
689
- if (!meta.requiresMappings) {
690
- const controls = query.controls;
691
- return {
692
- filter: meta.toStorageFormatters ? this.translateFilter(query.filter, meta) : query.filter,
693
- controls: {
694
- ...controls,
695
- $with: undefined,
696
- $select: controls?.$select ? new UniquSelect(controls.$select, meta.allPhysicalFields) : undefined
697
- },
698
- insights: query.insights
699
- };
700
- }
701
- return {
702
- filter: this.translateFilterWithRename(query.filter, meta),
703
- controls: query.controls ? this.translateControls(query.controls, meta) : {},
704
- insights: query.insights
705
- };
706
- }
707
- translateAggregateQuery(query, meta) {
708
- const controls = query.controls;
709
- const filter = meta.requiresMappings ? this.translateFilterWithRename(query.filter ?? {}, meta) : meta.toStorageFormatters ? this.translateFilter(query.filter ?? {}, meta) : query.filter ?? {};
710
- const groupBy = controls.$groupBy.map((field) => meta.leafByLogical.get(field)?.physicalName ?? field);
711
- let select;
712
- if (controls.$select) select = controls.$select.map((item) => {
713
- if (typeof item === "string") return meta.leafByLogical.get(item)?.physicalName ?? item;
714
- if (item.$field === "*") return item;
715
- return {
716
- ...item,
717
- $field: meta.leafByLogical.get(item.$field)?.physicalName ?? item.$field
718
- };
719
- });
720
- const aliases = new Set();
721
- if (controls.$select) {
722
- for (const item of controls.$select) if (typeof item !== "string") aliases.add(require_agg.resolveAlias(item));
723
- }
724
- let sort;
725
- if (controls.$sort) {
726
- const translated = {};
727
- for (const [key, dir] of Object.entries(controls.$sort)) if (aliases.has(key)) translated[key] = dir;
728
- else {
729
- const physical = meta.leafByLogical.get(key)?.physicalName ?? key;
730
- translated[physical] = dir;
731
- }
732
- sort = translated;
733
- }
734
- let having;
735
- if (controls.$having) having = meta.requiresMappings ? this.translateFilterWithRename(controls.$having, meta) : meta.toStorageFormatters ? this.translateFilter(controls.$having, meta) : controls.$having;
736
- return {
737
- filter,
738
- controls: {
739
- $groupBy: groupBy,
740
- $select: select ? new UniquSelect(select, meta.allPhysicalFields) : undefined,
741
- $sort: sort,
742
- $having: having,
743
- $skip: controls.$skip,
744
- $limit: controls.$limit,
745
- $count: controls.$count
746
- },
747
- insights: query.insights
748
- };
749
- }
750
- /**
751
- * Translates filter with key renaming from logical to physical names.
752
- * Used by the relational query path where field paths must be mapped
753
- * to `__`-separated column names.
754
- */ translateFilterWithRename(filter, meta) {
755
- if (!filter || typeof filter !== "object") return filter;
756
- const result = {};
757
- for (const [key, value] of Object.entries(filter)) if (key === "$and" || key === "$or") result[key] = value.map((f) => this.translateFilterWithRename(f, meta));
758
- else if (key === "$not") result[key] = this.translateFilterWithRename(value, meta);
759
- else if (key.startsWith("$")) result[key] = value;
760
- else {
761
- const physical = meta.leafByLogical.get(key)?.physicalName ?? key;
762
- result[physical] = this.formatFilterValue(physical, value, meta);
763
- }
764
- return result;
765
- }
766
- prepareForWrite(payload, meta, adapter) {
767
- const data = { ...payload };
768
- this.prepareCommon(data, meta, adapter);
769
- if (!meta.requiresMappings) {
770
- for (const [logical, physical] of meta.columnMap.entries()) if (logical in data) {
771
- data[physical] = data[logical];
772
- delete data[logical];
773
- }
774
- return this.formatWriteValues(data, meta);
775
- }
776
- return this.formatWriteValues(this.flattenPayload(data, meta), meta);
777
- }
778
- translatePatchKeys(update, meta) {
779
- if (!meta.requiresMappings && !meta.toStorageFormatters) return update;
780
- const result = {};
781
- const updateKeys = Object.keys(update);
782
- for (const key of updateKeys) {
783
- const value = update[key];
784
- const operatorMatch = key.match(/^(.+?)(\.__\$.+)$/);
785
- const basePath = operatorMatch ? operatorMatch[1] : key;
786
- const suffix = operatorMatch ? operatorMatch[2] : "";
787
- const fd = meta.leafByLogical.get(basePath);
788
- const finalKey = (fd?.physicalName ?? basePath) + suffix;
789
- if (fd?.storage === "json" && typeof value === "object" && value !== null && !suffix) result[finalKey] = JSON.stringify(value);
790
- else result[finalKey] = value;
791
- }
792
- return this.formatWriteValues(result, meta);
793
- }
794
- /**
795
- * Translates field names in sort and projection controls from
796
- * logical dot-paths to physical column names.
797
- */ translateControls(controls, meta) {
798
- if (!controls) return {};
799
- const result = {
800
- ...controls,
801
- $select: undefined,
802
- $with: undefined
803
- };
804
- if (controls.$sort) {
805
- const translated = {};
806
- const sortObj = controls.$sort;
807
- const sortKeys = Object.keys(sortObj);
808
- for (const key of sortKeys) {
809
- if (meta.flattenedParents.has(key)) continue;
810
- const physical = meta.leafByLogical.get(key)?.physicalName ?? key;
811
- translated[physical] = sortObj[key];
812
- }
813
- result.$sort = translated;
814
- }
815
- if (controls.$select) {
816
- let translatedRaw;
817
- if (Array.isArray(controls.$select)) {
818
- const expanded = [];
819
- for (const key of controls.$select) {
820
- const expansion = meta.selectExpansion.get(key);
821
- if (expansion) expanded.push(...expansion);
822
- else expanded.push(meta.leafByLogical.get(key)?.physicalName ?? key);
823
- }
824
- translatedRaw = expanded;
825
- } else {
826
- const translated = {};
827
- const selectObj = controls.$select;
828
- const selectKeys = Object.keys(selectObj);
829
- for (const key of selectKeys) {
830
- const val = selectObj[key];
831
- const expansion = meta.selectExpansion.get(key);
832
- if (expansion) for (const leaf of expansion) translated[leaf] = val;
833
- else {
834
- const physical = meta.leafByLogical.get(key)?.physicalName ?? key;
835
- translated[physical] = val;
836
- }
837
- }
838
- translatedRaw = translated;
839
- }
840
- result.$select = new UniquSelect(translatedRaw, meta.allPhysicalFields);
841
- }
842
- return result;
843
- }
844
- /**
845
- * Flattens nested object fields into __-separated keys and
846
- * JSON-stringifies @db.json / array fields.
847
- */ flattenPayload(data, meta) {
848
- const result = {};
849
- for (const key of Object.keys(data)) this.writeFlattenedField(key, data[key], result, meta);
850
- return result;
851
- }
852
- /**
853
- * Classifies and writes a single field to the result object.
854
- * Recurses into nested objects that should be flattened.
855
- */ writeFlattenedField(path, value, result, meta) {
856
- if (meta.ignoredFields.has(path)) return;
857
- if (meta.flattenedParents.has(path)) {
858
- if (value === null || value === undefined) this.setFlattenedChildrenNull(path, result, meta);
859
- else if (typeof value === "object" && !Array.isArray(value)) {
860
- const obj = value;
861
- for (const key of Object.keys(obj)) this.writeFlattenedField(`${path}.${key}`, obj[key], result, meta);
862
- }
863
- } else {
864
- const fd = meta.leafByLogical.get(path);
865
- const physical = fd?.physicalName ?? path.replace(/\./g, "__");
866
- if (fd?.storage === "json") result[physical] = value !== undefined && value !== null ? JSON.stringify(value) : value;
867
- else result[physical] = value;
868
- }
869
- }
870
- /**
871
- * When a parent object is null/undefined, set all its flattened children to null.
872
- */ setFlattenedChildrenNull(parentPath, result, meta) {
873
- const prefix = `${parentPath}.`;
874
- for (const [path, fd] of meta.leafByLogical.entries()) if (path.startsWith(prefix)) result[fd.physicalName] = null;
875
- }
876
- };
877
-
878
- //#endregion
879
- //#region packages/db/src/table/db-readable.ts
880
- function _define_property$4(obj, key, value) {
881
- if (key in obj) Object.defineProperty(obj, key, {
882
- value,
883
- enumerable: true,
884
- configurable: true,
885
- writable: true
886
- });
887
- else obj[key] = value;
888
- return obj;
889
- }
890
- function resolveDesignType(fieldType) {
891
- if (fieldType.type.kind === "") return fieldType.type.designType ?? "string";
892
- if (fieldType.type.kind === "object") return "object";
893
- if (fieldType.type.kind === "array") return "array";
894
- if (fieldType.type.kind === "union") {
895
- const items = fieldType.type.items;
896
- if (items.length > 0) {
897
- const resolved = items.map((item) => resolveDesignType(item));
898
- if (resolved.every((type) => type === resolved[0])) return resolved[0];
899
- }
900
- return "union";
901
- }
902
- return "string";
903
- }
904
- function resolveDefaultFromMetadata(metadata) {
905
- const defaultValue = metadata.get("db.default");
906
- if (defaultValue !== undefined) return {
907
- kind: "value",
908
- value: defaultValue
909
- };
910
- if (metadata.has("db.default.increment")) {
911
- const startValue = metadata.get("db.default.increment");
912
- return {
913
- kind: "fn",
914
- fn: "increment",
915
- start: typeof startValue === "number" ? startValue : undefined
916
- };
917
- }
918
- if (metadata.has("db.default.uuid")) return {
919
- kind: "fn",
920
- fn: "uuid"
921
- };
922
- if (metadata.has("db.default.now")) return {
923
- kind: "fn",
924
- fn: "now"
925
- };
926
- return undefined;
927
- }
928
- /**
929
- * Checks whether an id value is type-compatible with a field's design type.
930
- * Used by `findById` to skip primary-key lookup when the id clearly can't match,
931
- * falling through to unique-property search instead.
932
- */ function isIdCompatible(id, fieldType) {
933
- const dt = resolveDesignType(fieldType);
934
- switch (dt) {
935
- case "number": {
936
- if (typeof id === "number") return true;
937
- if (typeof id === "string") return id !== "" && !Number.isNaN(Number(id));
938
- return false;
939
- }
940
- case "boolean": return typeof id === "boolean";
941
- case "object":
942
- case "array": return typeof id === "object" && id !== null;
943
- default: return typeof id === "string";
944
- }
945
- }
946
- var AtscriptDbReadable = class {
947
- /** Ensures metadata is built. Called before any metadata access. */ _ensureBuilt() {
948
- if (!this._meta.isBuilt) this._meta.build(this.type, this.adapter, this.logger);
949
- }
950
- /** Whether this readable is a view (overridden in AtscriptDbView). */ get isView() {
951
- return false;
952
- }
953
- /** Returns the underlying adapter with its concrete type preserved. */ getAdapter() {
954
- return this.adapter;
955
- }
956
- /** The raw annotated type. */ get type() {
957
- return this._type;
958
- }
959
- /** Lazily-built flat map of all fields (dot-notation paths → annotated types). */ get flatMap() {
960
- this._ensureBuilt();
961
- return this._meta.flatMap;
962
- }
963
- /** All computed indexes from `@db.index.*` annotations. */ get indexes() {
964
- this._ensureBuilt();
965
- return this._meta.indexes;
966
- }
967
- /** Primary key field names from `@meta.id`. */ get primaryKeys() {
968
- this._ensureBuilt();
969
- return this._meta.primaryKeys;
970
- }
971
- /** Original `@meta.id` field names as declared in the schema (before adapter manipulation). */ get originalMetaIdFields() {
972
- this._ensureBuilt();
973
- return this._meta.originalMetaIdFields;
974
- }
975
- /** Dimension fields from `@db.column.dimension`. */ get dimensions() {
976
- this._ensureBuilt();
977
- return this._meta.dimensions;
978
- }
979
- /** Measure fields from `@db.column.measure`. */ get measures() {
980
- this._ensureBuilt();
981
- return this._meta.measures;
982
- }
983
- /** Sync method for structural changes: 'drop' (lossy), 'recreate' (lossless), or undefined (manual). */ get syncMethod() {
984
- return this._syncMethod;
985
- }
986
- /** Logical → physical column name mapping from `@db.column`. */ get columnMap() {
987
- this._ensureBuilt();
988
- return this._meta.columnMap;
989
- }
990
- /** Default values from `@db.default.*`. */ get defaults() {
991
- this._ensureBuilt();
992
- return this._meta.defaults;
993
- }
994
- /** Fields excluded from DB via `@db.ignore`. */ get ignoredFields() {
995
- this._ensureBuilt();
996
- return this._meta.ignoredFields;
997
- }
998
- /** Navigational fields (`@db.rel.to` / `@db.rel.from`) — not stored as columns. */ get navFields() {
999
- this._ensureBuilt();
1000
- return this._meta.navFields;
1001
- }
1002
- /** Single-field unique index properties. */ get uniqueProps() {
1003
- this._ensureBuilt();
1004
- return this._meta.uniqueProps;
1005
- }
1006
- /** Foreign key constraints from `@db.rel.FK` annotations. */ get foreignKeys() {
1007
- this._ensureBuilt();
1008
- return this._meta.foreignKeys;
1009
- }
1010
- /** Navigational relation metadata from `@db.rel.to` / `@db.rel.from`. */ get relations() {
1011
- this._ensureBuilt();
1012
- return this._meta.relations;
1013
- }
1014
- /** The underlying database adapter instance. */ get dbAdapter() {
1015
- return this.adapter;
1016
- }
1017
- /**
1018
- * Enables or disables verbose (debug-level) DB call logging for this table/view.
1019
- * When disabled (default), no log strings are constructed — zero overhead.
1020
- */ setVerbose(enabled) {
1021
- this.adapter.setVerbose(enabled);
1022
- }
1023
- /** Precomputed logical dot-path → physical column name map. */ get pathToPhysical() {
1024
- this._ensureBuilt();
1025
- return this._meta.pathToPhysical;
1026
- }
1027
- /** Precomputed physical column name → logical dot-path map (inverse). */ get physicalToPath() {
1028
- this._ensureBuilt();
1029
- return this._meta.physicalToPath;
1030
- }
1031
- /** Descriptor for the primary ID field(s). */ getIdDescriptor() {
1032
- this._ensureBuilt();
1033
- return {
1034
- fields: [...this._meta.primaryKeys],
1035
- isComposite: this._meta.primaryKeys.length > 1
1036
- };
1037
- }
1038
- /**
1039
- * Pre-computed field metadata for adapter use.
1040
- */ get fieldDescriptors() {
1041
- this._ensureBuilt();
1042
- return this._meta.fieldDescriptors;
1043
- }
1044
- /**
1045
- * Creates a new validator with custom options.
1046
- */ createValidator(opts) {
1047
- return this._type.validator(opts);
1048
- }
1049
- /**
1050
- * Finds a single record matching the query.
1051
- * The return type automatically excludes nav props unless they are
1052
- * explicitly requested via `$with`.
1053
- */ async findOne(query) {
1054
- this._ensureBuilt();
1055
- const withRelations = query.controls?.$with;
1056
- const translatedQuery = this._fieldMapper.translateQuery(query, this._meta);
1057
- const result = await this.adapter.findOne(translatedQuery);
1058
- if (!result) return null;
1059
- const row = this._fieldMapper.reconstructFromRead(result, this._meta);
1060
- if (withRelations?.length) await this.loadRelations([row], withRelations);
1061
- return row;
1062
- }
1063
- /**
1064
- * Finds all records matching the query.
1065
- * The return type automatically excludes nav props unless they are
1066
- * explicitly requested via `$with`.
1067
- */ async findMany(query) {
1068
- this._ensureBuilt();
1069
- const withRelations = query.controls?.$with;
1070
- const translatedQuery = this._fieldMapper.translateQuery(query, this._meta);
1071
- const results = await this.adapter.findMany(translatedQuery);
1072
- const rows = results.map((row) => this._fieldMapper.reconstructFromRead(row, this._meta));
1073
- if (withRelations?.length) await this.loadRelations(rows, withRelations);
1074
- return rows;
1075
- }
1076
- /**
1077
- * Counts records matching the query.
1078
- */ async count(query) {
1079
- this._ensureBuilt();
1080
- query ?? (query = {
1081
- filter: {},
1082
- controls: {}
1083
- });
1084
- return this.adapter.count(this._fieldMapper.translateQuery(query, this._meta));
1085
- }
1086
- /**
1087
- * Finds records and total count in a single logical call.
1088
- */ async findManyWithCount(query) {
1089
- this._ensureBuilt();
1090
- const withRelations = query.controls?.$with;
1091
- const translated = this._fieldMapper.translateQuery(query, this._meta);
1092
- const result = await this.adapter.findManyWithCount(translated);
1093
- const rows = result.data.map((row) => this._fieldMapper.reconstructFromRead(row, this._meta));
1094
- if (withRelations?.length) await this.loadRelations(rows, withRelations);
1095
- return {
1096
- data: rows,
1097
- count: result.count
1098
- };
1099
- }
1100
- /**
1101
- * Executes an aggregate query with GROUP BY and aggregate functions.
1102
- *
1103
- * Validates:
1104
- * - Plain fields in $select are a subset of $groupBy
1105
- * - When dimensions/measures are defined (strict mode): $groupBy fields
1106
- * must be dimensions, aggregate $field values must be measures (or '*')
1107
- *
1108
- * Translates field names, delegates to adapter.aggregate(),
1109
- * then reverse-maps and applies fromStorage formatters on results.
1110
- */ async aggregate(query) {
1111
- this._ensureBuilt();
1112
- const { $groupBy, $select } = query.controls;
1113
- if ($select) {
1114
- const groupBySet = new Set($groupBy);
1115
- for (const item of $select) if (typeof item === "string" && !groupBySet.has(item)) throw new require_nested_writer.DbError("INVALID_QUERY", [{
1116
- path: "$select",
1117
- message: `Plain field "${item}" in $select must also appear in $groupBy`
1118
- }]);
1119
- }
1120
- const { dimensions, measures } = this._meta;
1121
- if (dimensions.length > 0 || measures.length > 0) {
1122
- const dimSet = new Set(dimensions);
1123
- const measSet = new Set(measures);
1124
- for (const field of $groupBy) if (!dimSet.has(field)) throw new require_nested_writer.DbError("INVALID_QUERY", [{
1125
- path: "$groupBy",
1126
- message: `Field "${field}" is not a dimension`
1127
- }]);
1128
- if ($select) {
1129
- for (const item of $select) if (typeof item !== "string" && item.$field !== "*" && !measSet.has(item.$field)) throw new require_nested_writer.DbError("INVALID_QUERY", [{
1130
- path: "$select",
1131
- message: `Aggregate field "${item.$field}" is not a measure`
1132
- }]);
1133
- }
1134
- }
1135
- const dbQuery = this._fieldMapper.translateAggregateQuery(query, this._meta);
1136
- const results = await this.adapter.aggregate(dbQuery);
1137
- return results.map((row) => {
1138
- const mapped = {};
1139
- for (const [key, value] of Object.entries(row)) {
1140
- const logical = this._meta.physicalToPath.get(key) ?? key;
1141
- const fmt = this._meta.fromStorageFormatters?.get(key);
1142
- mapped[logical] = fmt && value !== null && value !== undefined ? fmt(value) : value;
1143
- }
1144
- return mapped;
1145
- });
1146
- }
1147
- /** Whether the underlying adapter supports text search. */ isSearchable() {
1148
- return this.adapter.isSearchable();
1149
- }
1150
- /** Returns available search indexes from the adapter. */ getSearchIndexes() {
1151
- return this.adapter.getSearchIndexes();
1152
- }
1153
- /**
1154
- * Full-text search with query translation and result reconstruction.
1155
- */ async search(text, query, indexName) {
1156
- this._ensureBuilt();
1157
- const withRelations = query.controls?.$with;
1158
- const translated = this._fieldMapper.translateQuery(query, this._meta);
1159
- const results = await this.adapter.search(text, translated, indexName);
1160
- const rows = results.map((row) => this._fieldMapper.reconstructFromRead(row, this._meta));
1161
- if (withRelations?.length) await this.loadRelations(rows, withRelations);
1162
- return rows;
1163
- }
1164
- /**
1165
- * Full-text search with count for paginated search results.
1166
- */ async searchWithCount(text, query, indexName) {
1167
- this._ensureBuilt();
1168
- const withRelations = query.controls?.$with;
1169
- const translated = this._fieldMapper.translateQuery(query, this._meta);
1170
- const result = await this.adapter.searchWithCount(text, translated, indexName);
1171
- const rows = result.data.map((row) => this._fieldMapper.reconstructFromRead(row, this._meta));
1172
- if (withRelations?.length) await this.loadRelations(rows, withRelations);
1173
- return {
1174
- data: rows,
1175
- count: result.count
1176
- };
1177
- }
1178
- /** Whether the underlying adapter supports vector similarity search. */ isVectorSearchable() {
1179
- return this.adapter.isVectorSearchable();
1180
- }
1181
- /**
1182
- * Vector similarity search with query translation and result reconstruction.
1183
- *
1184
- * Overloads:
1185
- * - `vectorSearch(vector, query?)` — uses default vector index
1186
- * - `vectorSearch(indexName, vector, query?)` — targets a specific vector index
1187
- */ async vectorSearch(vectorOrIndex, maybeVectorOrQuery, maybeQuery) {
1188
- const { vector, query, indexName } = this._resolveVectorSearchArgs(vectorOrIndex, maybeVectorOrQuery, maybeQuery);
1189
- this._ensureBuilt();
1190
- const withRelations = query?.controls?.$with;
1191
- const translated = this._fieldMapper.translateQuery(query || {}, this._meta);
1192
- const results = await this.adapter.vectorSearch(vector, translated, indexName);
1193
- const rows = results.map((row) => this._fieldMapper.reconstructFromRead(row, this._meta));
1194
- if (withRelations?.length) await this.loadRelations(rows, withRelations);
1195
- return rows;
1196
- }
1197
- /**
1198
- * Vector similarity search with count for paginated results.
1199
- *
1200
- * Overloads:
1201
- * - `vectorSearchWithCount(vector, query?)` — uses default vector index
1202
- * - `vectorSearchWithCount(indexName, vector, query?)` — targets a specific vector index
1203
- */ async vectorSearchWithCount(vectorOrIndex, maybeVectorOrQuery, maybeQuery) {
1204
- const { vector, query, indexName } = this._resolveVectorSearchArgs(vectorOrIndex, maybeVectorOrQuery, maybeQuery);
1205
- this._ensureBuilt();
1206
- const withRelations = query?.controls?.$with;
1207
- const translated = this._fieldMapper.translateQuery(query || {}, this._meta);
1208
- const result = await this.adapter.vectorSearchWithCount(vector, translated, indexName);
1209
- const rows = result.data.map((row) => this._fieldMapper.reconstructFromRead(row, this._meta));
1210
- if (withRelations?.length) await this.loadRelations(rows, withRelations);
1211
- return {
1212
- data: rows,
1213
- count: result.count
1214
- };
1215
- }
1216
- /** Resolves overloaded vector search arguments into canonical form. */ _resolveVectorSearchArgs(vectorOrIndex, maybeVectorOrQuery, maybeQuery) {
1217
- if (Array.isArray(vectorOrIndex)) return {
1218
- vector: vectorOrIndex,
1219
- query: maybeVectorOrQuery,
1220
- indexName: undefined
1221
- };
1222
- return {
1223
- vector: maybeVectorOrQuery,
1224
- query: maybeQuery,
1225
- indexName: vectorOrIndex
1226
- };
1227
- }
1228
- /**
1229
- * Finds a single record by any type-compatible identifier — primary key
1230
- * or single-field unique index.
1231
- * The return type excludes nav props unless `$with` is provided in controls.
1232
- *
1233
- * ```typescript
1234
- * // Without relations — nav props stripped from result
1235
- * const user = await table.findById('123')
1236
- *
1237
- * // With relations — only requested nav props appear
1238
- * const user = await table.findById('123', { controls: { $with: [{ name: 'posts' }] } })
1239
- * ```
1240
- */ async findById(id, query) {
1241
- this._ensureBuilt();
1242
- const filter = this._resolveIdFilter(id);
1243
- if (!filter) return null;
1244
- return await this.findOne({
1245
- filter,
1246
- controls: query?.controls || {}
1247
- });
1248
- }
1249
- /**
1250
- * Resolves an id value into a filter expression.
1251
- */ _resolveIdFilter(id) {
1252
- const orFilters = [];
1253
- const pkFields = this.primaryKeys;
1254
- if (pkFields.length === 1) {
1255
- const filter = this._tryFieldFilter(pkFields[0], id);
1256
- if (filter) orFilters.push(filter);
1257
- } else if (pkFields.length > 1 && typeof id === "object" && id !== null) {
1258
- const idObj = id;
1259
- const compositeFilter = {};
1260
- let valid = true;
1261
- for (const field of pkFields) {
1262
- const fieldType = this.flatMap.get(field);
1263
- if (fieldType && !isIdCompatible(idObj[field], fieldType)) {
1264
- valid = false;
1265
- break;
1266
- }
1267
- try {
1268
- compositeFilter[field] = fieldType ? this.adapter.prepareId(idObj[field], fieldType) : idObj[field];
1269
- } catch {
1270
- valid = false;
1271
- break;
1272
- }
1273
- }
1274
- if (valid) orFilters.push(compositeFilter);
1275
- }
1276
- for (const prop of this.uniqueProps) {
1277
- const filter = this._tryFieldFilter(prop, id);
1278
- if (filter) orFilters.push(filter);
1279
- }
1280
- if (typeof id === "object" && id !== null && orFilters.length === 0) {
1281
- const idObj = id;
1282
- for (const index of this._meta.indexes.values()) {
1283
- if (index.type !== "unique" || index.fields.length < 2) continue;
1284
- const compoundFilter = {};
1285
- let valid = true;
1286
- for (const indexField of index.fields) {
1287
- const fieldName = indexField.name;
1288
- if (idObj[fieldName] === undefined) {
1289
- valid = false;
1290
- break;
1291
- }
1292
- const fieldType = this.flatMap.get(fieldName);
1293
- if (fieldType && !isIdCompatible(idObj[fieldName], fieldType)) {
1294
- valid = false;
1295
- break;
1296
- }
1297
- try {
1298
- compoundFilter[fieldName] = fieldType ? this.adapter.prepareId(idObj[fieldName], fieldType) : idObj[fieldName];
1299
- } catch {
1300
- valid = false;
1301
- break;
1302
- }
1303
- }
1304
- if (valid) orFilters.push(compoundFilter);
1305
- }
1306
- }
1307
- if (orFilters.length === 0) return null;
1308
- if (orFilters.length === 1) return orFilters[0];
1309
- return { $or: orFilters };
1310
- }
1311
- /**
1312
- * Attempts to build a single-field filter `{ field: preparedId }`.
1313
- */ _tryFieldFilter(field, id) {
1314
- const fieldType = this.flatMap.get(field);
1315
- if (fieldType && !isIdCompatible(id, fieldType)) return null;
1316
- try {
1317
- const prepared = fieldType ? this.adapter.prepareId(id, fieldType) : id;
1318
- return { [field]: prepared };
1319
- } catch {
1320
- return null;
1321
- }
1322
- }
1323
- /**
1324
- * Public entry point for relation loading. Used by adapters for nested $with delegation.
1325
- */ async loadRelations(rows, withRelations) {
1326
- const { loadRelationsImpl } = await Promise.resolve().then(function() {
1327
- return require("./relation-loader-D4mTw6yH.cjs");
1328
- });
1329
- return loadRelationsImpl(rows, withRelations, this);
1330
- }
1331
- /**
1332
- * Finds the FK entry that connects a `@db.rel.to` relation to its target.
1333
- * Thin wrapper — delegates to relation-loader for shared use with db-table.ts write path.
1334
- */ _findFKForRelation(relation) {
1335
- return require_relation_helpers.findFKForRelation(relation, this._meta.foreignKeys);
1336
- }
1337
- /**
1338
- * Finds a FK on a remote table that points back to this table.
1339
- * Thin wrapper — delegates to relation-loader for shared use with db-table.ts write path.
1340
- */ _findRemoteFK(targetTable, thisTableName, alias) {
1341
- return require_relation_helpers.findRemoteFK(targetTable, thisTableName, alias);
1342
- }
1343
- constructor(_type, adapter, logger = require_logger.NoopLogger, _tableResolver) {
1344
- _define_property$4(this, "_type", void 0);
1345
- _define_property$4(this, "adapter", void 0);
1346
- _define_property$4(this, "logger", void 0);
1347
- _define_property$4(this, "_tableResolver", void 0);
1348
- /** Resolved table/collection/view name. */ _define_property$4(this, "tableName", void 0);
1349
- /** Database schema/namespace from `@db.schema` (if set). */ _define_property$4(this, "schema", void 0);
1350
- /** Sync method from `@db.sync.method` ('drop' | 'recreate' | undefined). */ _define_property$4(this, "_syncMethod", void 0);
1351
- /** Previous table/view name from `@db.table.renamed` or `@db.view.renamed`. */ _define_property$4(this, "renamedFrom", void 0);
1352
- /** Computed metadata for this table/view. Built lazily on first access. */ _define_property$4(this, "_meta", void 0);
1353
- /** Strategy for mapping between logical field shapes and physical storage. */ _define_property$4(this, "_fieldMapper", void 0);
1354
- _define_property$4(this, "_writeTableResolver", void 0);
1355
- this._type = _type;
1356
- this.adapter = adapter;
1357
- this.logger = logger;
1358
- this._tableResolver = _tableResolver;
1359
- if (!(0, __atscript_typescript_utils.isAnnotatedType)(_type)) throw new Error("Atscript Annotated Type expected");
1360
- if (_type.type.kind !== "object") throw new Error("Database type must be an object type");
1361
- const adapterName = adapter.getAdapterTableName?.(_type);
1362
- const dbTable = _type.metadata.get("db.table");
1363
- const dbViewName = _type.metadata.get("db.view");
1364
- const fallbackName = _type.id || "";
1365
- this.tableName = adapterName || dbTable || dbViewName || fallbackName;
1366
- if (!this.tableName) throw new Error("@db.table or @db.view annotation expected");
1367
- this.schema = _type.metadata.get("db.schema");
1368
- this._syncMethod = _type.metadata.get("db.sync.method");
1369
- this.renamedFrom = _type.metadata.get("db.table.renamed") ?? _type.metadata.get("db.view.renamed");
1370
- this._meta = new TableMetadata(adapter.supportsNestedObjects());
1371
- this._fieldMapper = adapter.supportsNestedObjects() ? new DocumentFieldMapper() : new RelationalFieldMapper();
1372
- adapter.registerReadable(this, logger);
1373
- }
1374
- };
1375
-
1376
- //#endregion
1377
- //#region packages/db/src/strategies/integrity.ts
1378
- var IntegrityStrategy = class {};
1379
- var NativeIntegrity = class extends IntegrityStrategy {
1380
- async validateForeignKeys() {}
1381
- async cascadeBeforeDelete() {}
1382
- needsCascade() {
1383
- return false;
1384
- }
1385
- };
1386
-
1387
- //#endregion
1388
- //#region packages/db/src/strategies/application-integrity.ts
1389
- const MAX_CASCADE_DEPTH = 100;
1390
- const cascadeStorage = new node_async_hooks.AsyncLocalStorage();
1391
- var ApplicationIntegrity = class extends IntegrityStrategy {
1392
- /**
1393
- * Validates FK constraints by querying target tables for referenced records.
1394
- * Collects unique FK values across items, batches them into target-table
1395
- * lookups, and throws FK_VIOLATION if any references are missing.
1396
- */ async validateForeignKeys(items, meta, fkLookupResolver, writeTableResolver, partial, excludeTargetTable) {
1397
- if (!fkLookupResolver) return;
1398
- const checks = [];
1399
- for (const [, fk] of meta.foreignKeys) {
1400
- if (excludeTargetTable && fk.targetTable === excludeTargetTable) continue;
1401
- const valueSets = fk.fields.map(() => new Set());
1402
- for (const item of items) {
1403
- if (partial && !fk.fields.some((f) => f in item)) continue;
1404
- let allPresent = true;
1405
- const vals = [];
1406
- for (const field of fk.fields) {
1407
- const v = item[field];
1408
- if (v === null || v === undefined) {
1409
- allPresent = false;
1410
- break;
1411
- }
1412
- vals.push(v);
1413
- }
1414
- if (!allPresent) continue;
1415
- for (let i = 0; i < vals.length; i++) valueSets[i].add(vals[i]);
1416
- }
1417
- if (valueSets[0].size === 0) continue;
1418
- let target = fkLookupResolver(fk.targetTable);
1419
- if (!target && fk.targetTypeRef && writeTableResolver) {
1420
- const resolved = writeTableResolver(fk.targetTypeRef());
1421
- if (resolved) target = { count: (filter$1) => resolved.count({ filter: filter$1 }) };
1422
- }
1423
- if (!target) continue;
1424
- const filter = {};
1425
- const valueArrays = valueSets.map((s) => [...s]);
1426
- for (let i = 0; i < fk.targetFields.length; i++) filter[fk.targetFields[i]] = valueArrays[i].length === 1 ? valueArrays[i][0] : { $in: valueArrays[i] };
1427
- const expectedCount = valueArrays[0].length;
1428
- checks.push(async () => {
1429
- const count = await target.count(filter);
1430
- if (count < expectedCount) {
1431
- const sample = valueArrays[0].slice(0, 3).join(", ");
1432
- const suffix = valueArrays[0].length > 3 ? `, ... (${valueArrays[0].length} total)` : "";
1433
- throw new require_nested_writer.DbError("FK_VIOLATION", [{
1434
- path: fk.fields.join(", "),
1435
- message: `FK constraint violation: "${fk.fields.join(", ")}" references non-existent record in "${fk.targetTable}" (values: ${sample}${suffix})`
1436
- }]);
1437
- }
1438
- });
1439
- }
1440
- if (checks.length > 0) await Promise.all(checks.map((fn) => fn()));
1441
- }
1442
- /**
1443
- * Applies cascade/setNull actions on child tables before deleting parent records.
1444
- * Finds all records matching `filter`, extracts their PK values, then for each
1445
- * child table with a FK pointing to this table:
1446
- * - `restrict`: throws if any children exist
1447
- * - `cascade`: recursively deletes child records
1448
- * - `setNull`: sets FK fields to null
1449
- */ async cascadeBeforeDelete(filter, tableName, meta, cascadeResolver, translateFilter, adapter) {
1450
- const parentCtx = cascadeStorage.getStore();
1451
- const visited = parentCtx?.visited ?? new Set();
1452
- const depth = (parentCtx?.depth ?? 0) + 1;
1453
- if (depth > MAX_CASCADE_DEPTH) throw new require_nested_writer.DbError("CASCADE_CYCLE", [{
1454
- path: tableName,
1455
- message: `Cascade delete aborted: chain exceeded ${MAX_CASCADE_DEPTH} levels, likely caused by a circular or deeply nested cascade relationship`
1456
- }]);
1457
- const targets = cascadeResolver(tableName);
1458
- if (targets.length === 0) return;
1459
- const neededLogical = new Set();
1460
- for (const t of targets) for (const tf of t.fk.targetFields) neededLogical.add(tf);
1461
- for (const pk of meta.primaryKeys) neededLogical.add(pk);
1462
- const physicalToLogical = new Map();
1463
- const physicalFields = [];
1464
- for (const logical of neededLogical) {
1465
- const physical = meta.pathToPhysical.get(logical) ?? meta.columnMap.get(logical) ?? logical;
1466
- physicalFields.push(physical);
1467
- physicalToLogical.set(physical, logical);
1468
- }
1469
- const rawRecords = await adapter.findMany({
1470
- filter: translateFilter(filter),
1471
- controls: { $select: new UniquSelect(physicalFields) }
1472
- });
1473
- if (rawRecords.length === 0) return;
1474
- const allRecords = rawRecords.map((r) => {
1475
- const mapped = {};
1476
- for (const [key, val] of Object.entries(r)) mapped[physicalToLogical.get(key) ?? key] = val;
1477
- return mapped;
1478
- });
1479
- const pkFields = meta.primaryKeys;
1480
- const addedKeys = [];
1481
- const records = [];
1482
- for (const record of allRecords) {
1483
- const key = this.recordKey(tableName, pkFields, record);
1484
- if (visited.has(key)) continue;
1485
- visited.add(key);
1486
- addedKeys.push(key);
1487
- records.push(record);
1488
- }
1489
- if (records.length === 0) return;
1490
- try {
1491
- await cascadeStorage.run({
1492
- visited,
1493
- depth
1494
- }, async () => {
1495
- const restrictChecks = [];
1496
- for (const target of targets) {
1497
- if (target.fk.onDelete !== "restrict") continue;
1498
- const childFilter = this.buildCascadeChildFilter(records, target.fk);
1499
- if (!childFilter) continue;
1500
- restrictChecks.push(target.count(childFilter).then((count) => {
1501
- if (count > 0) throw new require_nested_writer.DbError("CONFLICT", [{
1502
- path: tableName,
1503
- message: `Cannot delete from "${tableName}": ${count} record(s) in "${target.childTable}" (${target.fk.fields.join(", ")}) reference it (RESTRICT)`
1504
- }]);
1505
- }));
1506
- }
1507
- if (restrictChecks.length > 0) await Promise.all(restrictChecks);
1508
- for (const target of targets) {
1509
- const action = target.fk.onDelete;
1510
- if (!action || action === "noAction" || action === "restrict") continue;
1511
- const childFilter = this.buildCascadeChildFilter(records, target.fk);
1512
- if (!childFilter) continue;
1513
- switch (action) {
1514
- case "cascade": {
1515
- await target.deleteMany(childFilter);
1516
- break;
1517
- }
1518
- case "setNull": {
1519
- const nullData = {};
1520
- for (const f of target.fk.fields) nullData[f] = null;
1521
- await target.updateMany(childFilter, nullData);
1522
- break;
1523
- }
1524
- }
1525
- }
1526
- });
1527
- } finally {
1528
- for (const key of addedKeys) visited.delete(key);
1529
- }
1530
- }
1531
- needsCascade(cascadeResolver) {
1532
- return !!cascadeResolver;
1533
- }
1534
- recordKey(tableName, pkFields, record) {
1535
- let key = tableName;
1536
- for (const f of pkFields) {
1537
- const v = record[f];
1538
- key += `\0${v === null || v === undefined ? "" : String(v)}`;
1539
- }
1540
- return key;
1541
- }
1542
- /**
1543
- * Builds a filter for child records whose FK matches the deleted parent's PK values.
1544
- */ buildCascadeChildFilter(parentRecords, fk) {
1545
- if (fk.fields.length === 1 && fk.targetFields.length === 1) {
1546
- const pkField = fk.targetFields[0];
1547
- const values = parentRecords.map((r) => r[pkField]).filter((v) => v !== undefined && v !== null);
1548
- if (values.length === 0) return undefined;
1549
- return values.length === 1 ? { [fk.fields[0]]: values[0] } : { [fk.fields[0]]: { $in: values } };
1550
- }
1551
- const orFilters = [];
1552
- for (const record of parentRecords) {
1553
- const condition = {};
1554
- let valid = true;
1555
- for (let i = 0; i < fk.fields.length; i++) {
1556
- const val = record[fk.targetFields[i]];
1557
- if (val === undefined || val === null) {
1558
- valid = false;
1559
- break;
1560
- }
1561
- condition[fk.fields[i]] = val;
1562
- }
1563
- if (valid) orFilters.push(condition);
1564
- }
1565
- if (orFilters.length === 0) return undefined;
1566
- return orFilters.length === 1 ? orFilters[0] : { $or: orFilters };
1567
- }
1568
- };
1569
-
1570
- //#endregion
1571
- //#region packages/db/src/patch/patch-types.ts
1572
- function getKeyProps(def) {
1573
- if (def.type.of.type.kind === "object") {
1574
- const objType = def.type.of.type;
1575
- const keyProps = new Set();
1576
- for (const [key, val] of objType.props.entries()) if (val.metadata.get("expect.array.key")) keyProps.add(key);
1577
- return keyProps;
1578
- }
1579
- return new Set();
1580
- }
1581
-
1582
- //#endregion
1583
- //#region packages/db/src/db-validator-plugin.ts
1584
- /** Set of recognised array‑patch operator keys. */ const PATCH_OPS = new Set([
1585
- "$replace",
1586
- "$insert",
1587
- "$upsert",
1588
- "$update",
1589
- "$remove"
1590
- ]);
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_nested_writer = require("./nested-writer-BDXsDMPP.cjs");
3
+ const require_db_view = require("./db-view-BntnAmXO.cjs");
4
+ const require_ops = require("./ops.cjs");
5
+ let _uniqu_core = require("@uniqu/core");
6
+ //#region src/table/db-space.ts
1591
7
  /**
1592
- * Returns `true` when `value` looks like a patch‑operator object
1593
- * (at least one key is a recognised operator and no unknown keys).
1594
- */ function isPatchOperatorObject(value) {
1595
- if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
1596
- const keys = Object.keys(value);
1597
- if (keys.length === 0) return false;
1598
- return keys.every((k) => PATCH_OPS.has(k));
1599
- }
1600
- function createDbValidatorPlugin() {
1601
- return (ctx, def, value) => {
1602
- const dbCtx = ctx.context;
1603
- if (!dbCtx) return undefined;
1604
- const isTo = def.metadata.has("db.rel.to");
1605
- const isFrom = def.metadata.has("db.rel.from");
1606
- const isVia = def.metadata.has("db.rel.via");
1607
- if (isTo || isFrom || isVia) return handleNavField(ctx, def, value, dbCtx, isTo, isFrom, isVia);
1608
- if (dbCtx.mode === "patch" && def.type.kind === "array" && dbCtx.flatMap) {
1609
- const flatEntry = dbCtx.flatMap.get(ctx.path);
1610
- if (flatEntry?.metadata?.has("db.__topLevelArray") && !flatEntry.metadata.has("db.json")) return handleArrayPatch(ctx, def, value);
1611
- }
1612
- return undefined;
1613
- };
1614
- }
1615
- function handleNavField(ctx, def, value, dbCtx, isTo, isFrom, isVia) {
1616
- const pathParts = ctx.path.split(".");
1617
- const fieldName = pathParts[pathParts.length - 1] || ctx.path;
1618
- if (value === null) {
1619
- ctx.error(`Cannot process null navigation property '${fieldName}'`);
1620
- return false;
1621
- }
1622
- if (value === undefined) return true;
1623
- if (dbCtx.mode === "patch") {
1624
- if (isFrom || isVia) {
1625
- if (isPatchOperatorObject(value)) return validateNavPatchOps(ctx, def, value, fieldName);
1626
- const relType = isFrom ? "1:N" : "M:N";
1627
- ctx.error(`Cannot patch ${relType} relation '${fieldName}' with a plain value — use patch operators ({ $insert, $remove, $replace, $update, $upsert })`);
1628
- return false;
1629
- }
1630
- }
1631
- if (isVia) return true;
1632
- return undefined;
1633
- }
1634
- /**
1635
- * Validates patch operator values against the nav field's target array type.
1636
- * Each operator's items are validated against the element type.
1637
- */ function validateNavPatchOps(ctx, def, ops, fieldName) {
1638
- if (def.type.kind !== "array") {
1639
- ctx.error(`Cannot use patch operators on non-array relation '${fieldName}'`);
1640
- return false;
1641
- }
1642
- const arrayDef = def;
1643
- for (const op of [
1644
- "$replace",
1645
- "$insert",
1646
- "$upsert"
1647
- ]) if (ops[op] !== undefined) {
1648
- if (!ctx.validateAnnotatedType(arrayDef, ops[op])) return false;
1649
- }
1650
- for (const op of ["$update", "$remove"]) if (ops[op] !== undefined) {
1651
- if (!validatePartialItems(ctx, arrayDef, ops[op], op, true)) return false;
1652
- }
1653
- return true;
1654
- }
1655
- /**
1656
- * Handles patch‑mode validation for top‑level embedded arrays.
8
+ * A database space a registry of tables and views sharing the same adapter type and driver.
1657
9
  *
1658
- * When the incoming value is:
1659
- * - A plain array falls through to default array validation ($replace semantics)
1660
- * - A patch operator object validates each operator's payload individually
1661
- */ function handleArrayPatch(ctx, def, value) {
1662
- if (Array.isArray(value)) return undefined;
1663
- if (typeof value !== "object" || value === null) return undefined;
1664
- const ops = value;
1665
- const keys = Object.keys(ops);
1666
- if (keys.length === 0 || !keys.every((k) => PATCH_OPS.has(k))) {
1667
- const hasPatchOps = keys.some((k) => PATCH_OPS.has(k));
1668
- if (hasPatchOps) {
1669
- const unknown = keys.filter((k) => !PATCH_OPS.has(k));
1670
- ctx.error(`Unknown patch operator(s): ${unknown.join(", ")}. Allowed: $replace, $insert, $upsert, $update, $remove`);
1671
- return false;
1672
- }
1673
- return undefined;
1674
- }
1675
- for (const op of [
1676
- "$replace",
1677
- "$insert",
1678
- "$upsert"
1679
- ]) if (ops[op] !== undefined) {
1680
- if (!ctx.validateAnnotatedType(def, ops[op])) return false;
1681
- }
1682
- const isMerge = def.metadata.get("db.patch.strategy") === "merge";
1683
- for (const op of ["$update", "$remove"]) if (ops[op] !== undefined) {
1684
- if (!validatePartialItems(ctx, def, ops[op], op, isMerge)) return false;
1685
- }
1686
- return true;
1687
- }
1688
- /**
1689
- * Validates `$update` / `$remove` items.
1690
- *
1691
- * Each item must be an object. For object arrays with `@expect.array.key` fields,
1692
- * key properties are required and non‑key properties are validated but optional.
1693
- * For primitive arrays, items are validated directly against the element type.
1694
- */ function validatePartialItems(ctx, arrayDef, items, op, isMerge) {
1695
- if (!Array.isArray(items)) {
1696
- ctx.error(`${op} must be an array`);
1697
- return false;
1698
- }
1699
- const elementDef = arrayDef.type.of;
1700
- if (elementDef.type.kind !== "object") return ctx.validateAnnotatedType(arrayDef, items);
1701
- const keyProps = getKeyProps(arrayDef);
1702
- for (let i = 0; i < items.length; i++) {
1703
- const item = items[i];
1704
- if (typeof item !== "object" || item === null || Array.isArray(item)) {
1705
- ctx.error(`${op}[${i}]: expected object`);
1706
- return false;
1707
- }
1708
- const rec = item;
1709
- if (keyProps.size > 0) {
1710
- for (const kp of keyProps) if (rec[kp] === undefined || rec[kp] === null) {
1711
- ctx.error(`${op}[${i}]: key field '${kp}' is required`);
1712
- return false;
1713
- }
1714
- }
1715
- const objType = elementDef.type;
1716
- for (const [key, val] of Object.entries(rec)) {
1717
- const propDef = objType.props.get(key);
1718
- if (propDef) {
1719
- if (!ctx.validateAnnotatedType(propDef, val)) return false;
1720
- }
1721
- }
1722
- if (op === "$update" && isMerge !== true) {
1723
- for (const [propName, propDef] of objType.props) if (!propDef.optional && !keyProps.has(propName) && rec[propName] === undefined) {
1724
- ctx.error(`${op}[${i}]: field '${propName}' is required (replace strategy)`);
1725
- return false;
1726
- }
1727
- }
1728
- }
1729
- return true;
1730
- }
1731
-
1732
- //#endregion
1733
- //#region packages/db/src/patch/array-ops-resolver.ts
1734
- function resolveArrayOps(update, currentRecord, table) {
1735
- const resolved = {};
1736
- const opsMap = new Map();
1737
- const seenOpsFields = new Set();
1738
- for (const [key, value] of Object.entries(update)) {
1739
- const match = key.match(/^(.+?)\.__\$(.+)$/);
1740
- if (!match) {
1741
- resolved[key] = value;
1742
- continue;
1743
- }
1744
- const field = match[1];
1745
- const op = match[2];
1746
- seenOpsFields.add(field);
1747
- let fieldOps = opsMap.get(field);
1748
- if (!fieldOps) {
1749
- fieldOps = {};
1750
- opsMap.set(field, fieldOps);
1751
- }
1752
- switch (op) {
1753
- case "insert": {
1754
- fieldOps.insert = value;
1755
- break;
1756
- }
1757
- case "remove": {
1758
- fieldOps.remove = value;
1759
- break;
1760
- }
1761
- case "upsert": {
1762
- fieldOps.upsert = value;
1763
- break;
1764
- }
1765
- case "update": {
1766
- fieldOps.update = value;
1767
- break;
1768
- }
1769
- case "keys": {
1770
- fieldOps.keys = value;
1771
- break;
1772
- }
1773
- }
1774
- }
1775
- for (const [field, ops] of opsMap) {
1776
- const raw = currentRecord?.[field] ?? [];
1777
- const current = typeof raw === "string" ? JSON.parse(raw) : raw;
1778
- const arrayType = table.flatMap.get(field);
1779
- const keyProps = arrayType ? getKeyProps(arrayType) : new Set();
1780
- const uniqueItems = arrayType?.metadata?.get("expect.array.uniqueItems");
1781
- const mergeStrategy = arrayType?.metadata?.get("db.patch.strategy") === "merge";
1782
- const effectiveKeys = ops.keys && ops.keys.length > 0 ? new Set(ops.keys) : keyProps;
1783
- resolved[field] = applyOps(current, ops, effectiveKeys, !!uniqueItems, mergeStrategy);
1784
- }
1785
- return resolved;
1786
- }
1787
- function getArrayOpsFields(update) {
1788
- const fields = new Set();
1789
- for (const key of Object.keys(update)) {
1790
- const match = key.match(/^(.+?)\.__\$(.+)$/);
1791
- if (match) fields.add(match[1]);
1792
- }
1793
- return fields;
1794
- }
1795
- /**
1796
- * Applies patch operations to an array in‑memory.
1797
- * Order: remove → update → upsert → insert (most intuitive semantics).
1798
- */ function applyOps(current, ops, keyProps, uniqueItems, mergeStrategy) {
1799
- let result = [...current];
1800
- if (ops.remove) result = applyRemove(result, ops.remove, keyProps);
1801
- if (ops.update) result = applyUpdate(result, ops.update, keyProps);
1802
- if (ops.upsert) result = applyUpsert(result, ops.upsert, keyProps, mergeStrategy);
1803
- if (ops.insert) result = applyInsert(result, ops.insert, keyProps, uniqueItems);
1804
- return result;
1805
- }
1806
- function applyRemove(arr, items, keyProps) {
1807
- if (keyProps.size === 0) {
1808
- const removeSet = new Set(items.map((i) => JSON.stringify(i)));
1809
- return arr.filter((el) => !removeSet.has(JSON.stringify(el)));
1810
- }
1811
- return arr.filter((el) => {
1812
- const elObj = el;
1813
- return !items.some((item) => matchByKeys(elObj, item, keyProps));
1814
- });
1815
- }
1816
- function applyUpdate(arr, items, keyProps) {
1817
- if (keyProps.size === 0) return arr;
1818
- return arr.map((el) => {
1819
- const elObj = el;
1820
- const match = items.find((item) => matchByKeys(elObj, item, keyProps));
1821
- if (match) return {
1822
- ...elObj,
1823
- ...match
1824
- };
1825
- return el;
1826
- });
1827
- }
1828
- function applyUpsert(arr, items, keyProps, mergeStrategy) {
1829
- const result = [...arr];
1830
- for (const item of items) if (keyProps.size === 0) {
1831
- const idx = result.findIndex((el) => JSON.stringify(el) === JSON.stringify(item));
1832
- if (idx >= 0) result[idx] = item;
1833
- else result.push(item);
1834
- } else {
1835
- const itemObj = item;
1836
- const idx = result.findIndex((el) => matchByKeys(el, itemObj, keyProps));
1837
- if (idx >= 0) result[idx] = mergeStrategy ? {
1838
- ...result[idx],
1839
- ...itemObj
1840
- } : itemObj;
1841
- else result.push(item);
1842
- }
1843
- return result;
1844
- }
1845
- function applyInsert(arr, items, keyProps, uniqueItems) {
1846
- if (!uniqueItems) return [...arr, ...items];
1847
- const result = [...arr];
1848
- for (const item of items) {
1849
- const exists = keyProps.size > 0 ? result.some((el) => matchByKeys(el, item, keyProps)) : result.some((el) => JSON.stringify(el) === JSON.stringify(item));
1850
- if (!exists) result.push(item);
1851
- }
1852
- return result;
1853
- }
1854
- function matchByKeys(a, b, keys) {
1855
- for (const key of keys) if (a[key] !== b[key]) return false;
1856
- return true;
1857
- }
1858
-
1859
- //#endregion
1860
- //#region packages/db/src/patch/patch-decomposer.ts
1861
- function decomposePatch(payload, table) {
1862
- const update = {};
1863
- const topLevelArrayTag = "db.__topLevelArray";
1864
- flattenPatchPayload(payload, "", update, table, topLevelArrayTag);
1865
- return update;
1866
- }
1867
- function flattenPatchPayload(payload, prefix, update, table, topLevelArrayTag) {
1868
- for (const [_key, value] of Object.entries(payload)) {
1869
- const key = prefix ? `${prefix}.${_key}` : _key;
1870
- if (table.primaryKeys.includes(key)) continue;
1871
- const flatType = table.flatMap.get(key);
1872
- const isTopLevelArray = flatType?.metadata?.get(topLevelArrayTag);
1873
- if (typeof value === "object" && value !== null && !Array.isArray(value) && isTopLevelArray && !flatType?.metadata?.has("db.json")) decomposeArrayPatch(key, value, flatType, update, table);
1874
- else if (typeof value === "object" && value !== null && !Array.isArray(value) && flatType?.metadata?.get("db.patch.strategy") === "merge") flattenPatchPayload(value, key, update, table, topLevelArrayTag);
1875
- else update[key] = value;
1876
- }
1877
- }
1878
- /**
1879
- * Decomposes array patch operators into simple field updates.
10
+ * `DbSpace` solves the cross-table discovery problem: when table A has a relation
11
+ * to table B, it needs to find and query table B. The space acts as the registry
12
+ * that makes this possible via the table resolver callback.
1880
13
  *
1881
- * For adapters without native array operations, this does a best-effort
1882
- * decomposition:
1883
- * - `$replace` → direct set
1884
- * - `$insert` → value to append (adapter must handle)
1885
- * - `$upsert` → value to upsert by key (adapter must handle)
1886
- * - `$update` → value to update by key (adapter must handle)
1887
- * - `$remove` → value to remove by key (adapter must handle)
14
+ * Each table/view gets its own adapter instance (created by the factory), but all
15
+ * share the same space and can discover each other for `$with` relation loading.
1888
16
  *
1889
- * Note: For full correctness with `$insert`/`$upsert`/`$update`/`$remove`,
1890
- * the adapter should implement native patch support. This generic decomposition
1891
- * handles `$replace` directly and stores the other operations in a structured
1892
- * format the adapter can interpret.
1893
- */ function decomposeArrayPatch(key, value, fieldType, update, table) {
1894
- const keyProps = fieldType.type.kind === "array" ? getKeyProps(fieldType) : new Set();
1895
- if (value.$replace !== undefined) {
1896
- update[key] = value.$replace;
1897
- return;
1898
- }
1899
- if (value.$insert !== undefined) update[`${key}.__$insert`] = value.$insert;
1900
- if (value.$upsert !== undefined) update[`${key}.__$upsert`] = value.$upsert;
1901
- if (value.$update !== undefined) update[`${key}.__$update`] = value.$update;
1902
- if (value.$remove !== undefined) update[`${key}.__$remove`] = value.$remove;
1903
- if (keyProps.size > 0 && (value.$upsert !== undefined || value.$update !== undefined || value.$remove !== undefined)) update[`${key}.__$keys`] = [...keyProps];
1904
- }
1905
-
1906
- //#endregion
1907
- //#region packages/db/src/table/db-table.ts
1908
- function _define_property$3(obj, key, value) {
1909
- if (key in obj) Object.defineProperty(obj, key, {
1910
- value,
1911
- enumerable: true,
1912
- configurable: true,
1913
- writable: true
1914
- });
1915
- else obj[key] = value;
1916
- return obj;
1917
- }
1918
- var AtscriptDbTable = class extends AtscriptDbReadable {
1919
- /**
1920
- * Sets the cascade resolver for application-level cascade deletes.
1921
- * Called by DbSpace after table creation.
1922
- */ setCascadeResolver(resolver) {
1923
- this._cascadeResolver = resolver;
1924
- }
1925
- /**
1926
- * Sets the FK lookup resolver for application-level FK validation.
1927
- * Called by DbSpace after table creation.
1928
- */ setFkLookupResolver(resolver) {
1929
- this._fkLookupResolver = resolver;
1930
- }
1931
- /**
1932
- * Returns a cached validator for the given purpose.
1933
- * Built with adapter plugins from {@link BaseDbAdapter.getValidatorPlugins}.
1934
- *
1935
- * Standard purposes: `'insert'`, `'update'`, `'patch'`.
1936
- * Adapters may define additional purposes.
1937
- */ getValidator(purpose) {
1938
- if (!this.validators.has(purpose)) {
1939
- const validator = this._buildValidator(purpose);
1940
- this.validators.set(purpose, validator);
1941
- }
1942
- return this.validators.get(purpose);
1943
- }
1944
- /**
1945
- * Inserts a single record. Delegates to {@link insertMany} for unified
1946
- * nested creation support.
1947
- */ async insertOne(payload, opts) {
1948
- const result = await this.insertMany([payload], opts);
1949
- return { insertedId: result.insertedIds[0] };
1950
- }
1951
- /**
1952
- * Inserts multiple records with batch-optimized nested creation.
1953
- *
1954
- * Supports **nested creation**: if payloads include data for navigation
1955
- * fields (`@db.rel.to` / `@db.rel.from`), related records are created
1956
- * automatically in batches. TO dependencies are batch-created first
1957
- * (their PKs become our FKs), FROM dependents are batch-created after
1958
- * (they receive our PKs as their FKs). Fully recursive — nested records
1959
- * with their own nav data trigger further batch inserts at each level.
1960
- * Recursive up to `maxDepth` (default 3).
1961
- */ async insertMany(payloads, opts) {
1962
- this._ensureBuilt();
1963
- const maxDepth = opts?.maxDepth ?? 3;
1964
- const depth = opts?._depth ?? 0;
1965
- const canNest = depth < maxDepth && this._writeTableResolver && this._meta.navFields.size > 0;
1966
- if (!canNest && this._meta.navFields.size > 0) require_nested_writer.checkDepthOverflow(payloads, maxDepth, this._meta);
1967
- return require_nested_writer.enrichFkViolation(this._meta, () => this.adapter.withTransaction(async () => {
1968
- const items = payloads.map((p) => this._applyDefaults({ ...p }));
1969
- const validator = this.getValidator("insert");
1970
- const ctx = { mode: "insert" };
1971
- require_nested_writer.validateBatch(validator, items, ctx);
1972
- const host = this;
1973
- if (canNest) await require_nested_writer.batchInsertNestedTo(host, items, maxDepth, depth);
1974
- const prepared = [];
1975
- for (const data of items) {
1976
- for (const navField of this._meta.navFields) delete data[navField];
1977
- prepared.push(this._fieldMapper.prepareForWrite(data, this._meta, this.adapter));
1978
- }
1979
- await this._integrity.validateForeignKeys(items, this._meta, this._fkLookupResolver, this._writeTableResolver);
1980
- if (canNest) await require_nested_writer.preValidateNestedFrom(host, payloads);
1981
- const result = await this.adapter.insertMany(prepared);
1982
- if (canNest) await require_nested_writer.batchInsertNestedFrom(host, payloads, result.insertedIds, maxDepth, depth);
1983
- if (canNest) await require_nested_writer.batchInsertNestedVia(host, payloads, result.insertedIds, maxDepth, depth);
1984
- return result;
1985
- }));
1986
- }
1987
- /**
1988
- * Replaces a single record identified by primary key(s).
1989
- * Delegates to {@link bulkReplace} for unified nested relation support.
1990
- */ async replaceOne(payload, opts) {
1991
- return this.bulkReplace([payload], opts);
1992
- }
1993
- /**
1994
- * Replaces multiple records with deep nested relation support.
1995
- *
1996
- * Supports all relation types (TO, FROM, VIA). TO dependencies are
1997
- * replaced first (their PKs become our FKs), FROM dependents are replaced
1998
- * after (they receive our PKs as their FKs), VIA relations clear and
1999
- * re-create junction rows. Fully recursive up to `maxDepth` (default 3).
2000
- */ async bulkReplace(payloads, opts) {
2001
- this._ensureBuilt();
2002
- const maxDepth = opts?.maxDepth ?? 3;
2003
- const depth = opts?._depth ?? 0;
2004
- const canNest = depth < maxDepth && this._writeTableResolver && this._meta.navFields.size > 0;
2005
- if (!canNest && this._meta.navFields.size > 0) require_nested_writer.checkDepthOverflow(payloads, maxDepth, this._meta);
2006
- return require_nested_writer.enrichFkViolation(this._meta, () => this.adapter.withTransaction(async () => {
2007
- const items = payloads.map((p) => this._applyDefaults({ ...p }));
2008
- const originals = canNest ? payloads.map((p) => ({ ...p })) : [];
2009
- const validator = this.getValidator("bulkReplace");
2010
- const ctx = { mode: "replace" };
2011
- require_nested_writer.validateBatch(validator, items, ctx);
2012
- const host = this;
2013
- if (canNest) await require_nested_writer.batchReplaceNestedTo(host, items, maxDepth, depth);
2014
- await this._integrity.validateForeignKeys(items, this._meta, this._fkLookupResolver, this._writeTableResolver);
2015
- if (canNest) await require_nested_writer.preValidateNestedFrom(host, originals);
2016
- let matchedCount = 0;
2017
- let modifiedCount = 0;
2018
- for (const data of items) {
2019
- for (const navField of this._meta.navFields) delete data[navField];
2020
- const filter = this._extractPrimaryKeyFilter(data);
2021
- const prepared = this._fieldMapper.prepareForWrite(data, this._meta, this.adapter);
2022
- const result = await this.adapter.replaceOne(this._fieldMapper.translateFilter(filter, this._meta), prepared);
2023
- matchedCount += result.matchedCount;
2024
- modifiedCount += result.modifiedCount;
2025
- }
2026
- if (canNest) await require_nested_writer.batchReplaceNestedFrom(host, originals, maxDepth, depth);
2027
- if (canNest) await require_nested_writer.batchReplaceNestedVia(host, originals, maxDepth, depth);
2028
- return {
2029
- matchedCount,
2030
- modifiedCount
2031
- };
2032
- }));
2033
- }
2034
- /**
2035
- * Partially updates a single record identified by primary key(s).
2036
- * Delegates to {@link bulkUpdate} for unified nested relation support.
2037
- */ async updateOne(payload, opts) {
2038
- return this.bulkUpdate([payload], opts);
2039
- }
2040
- /**
2041
- * Partially updates multiple records with deep nested relation support.
2042
- *
2043
- * Only TO relations (1:1, N:1) are supported for patching. FROM/VIA
2044
- * relations will error — use {@link bulkReplace} for those.
2045
- * Recursive up to `maxDepth` (default 3).
2046
- */ async bulkUpdate(payloads, opts) {
2047
- this._ensureBuilt();
2048
- const maxDepth = opts?.maxDepth ?? 3;
2049
- const depth = opts?._depth ?? 0;
2050
- const canNest = depth < maxDepth && this._writeTableResolver && this._meta.navFields.size > 0;
2051
- if (!canNest && this._meta.navFields.size > 0) require_nested_writer.checkDepthOverflow(payloads, maxDepth, this._meta);
2052
- return require_nested_writer.enrichFkViolation(this._meta, () => this.adapter.withTransaction(async () => {
2053
- const validator = this.getValidator("bulkUpdate");
2054
- const ctx = {
2055
- mode: "patch",
2056
- flatMap: this.flatMap
2057
- };
2058
- require_nested_writer.validateBatch(validator, payloads, ctx);
2059
- const originals = canNest ? payloads.map((p) => ({ ...p })) : [];
2060
- const host = this;
2061
- if (canNest) await require_nested_writer.batchPatchNestedTo(host, payloads, maxDepth, depth);
2062
- await this._integrity.validateForeignKeys(payloads, this._meta, this._fkLookupResolver, this._writeTableResolver, true);
2063
- let matchedCount = 0;
2064
- let modifiedCount = 0;
2065
- for (const payload of payloads) {
2066
- const data = { ...payload };
2067
- for (const navField of this._meta.navFields) delete data[navField];
2068
- const filter = this._extractPrimaryKeyFilter(data);
2069
- for (const pk of this._meta.primaryKeys) delete data[pk];
2070
- if (Object.keys(data).length === 0) {
2071
- matchedCount += 1;
2072
- modifiedCount += 0;
2073
- continue;
2074
- }
2075
- let result;
2076
- const translatedFilter = this._fieldMapper.translateFilter(filter, this._meta);
2077
- if (this.adapter.supportsNativePatch()) result = await this.adapter.nativePatch(translatedFilter, data);
2078
- else {
2079
- const update = decomposePatch(data, this);
2080
- const translatedUpdate = this._fieldMapper.translatePatchKeys(update, this._meta);
2081
- const arrayOpsFields = getArrayOpsFields(translatedUpdate);
2082
- if (arrayOpsFields.size > 0) {
2083
- const current = await this.adapter.findOne({
2084
- filter: translatedFilter,
2085
- controls: {}
2086
- });
2087
- const resolved = resolveArrayOps(translatedUpdate, current, this);
2088
- result = await this.adapter.updateOne(translatedFilter, resolved);
2089
- } else result = await this.adapter.updateOne(translatedFilter, translatedUpdate);
2090
- }
2091
- matchedCount += result.matchedCount;
2092
- modifiedCount += result.modifiedCount;
2093
- }
2094
- if (canNest) await require_nested_writer.batchPatchNestedFrom(host, originals, maxDepth, depth);
2095
- if (canNest) await require_nested_writer.batchPatchNestedVia(host, originals, maxDepth, depth);
2096
- return {
2097
- matchedCount,
2098
- modifiedCount
2099
- };
2100
- }));
2101
- }
2102
- /**
2103
- * Deletes a single record by any type-compatible identifier — primary key
2104
- * or single-field unique index. Uses the same resolution logic as `findById`.
2105
- *
2106
- * When the adapter does not support native foreign keys (e.g. MongoDB),
2107
- * cascade and setNull actions are applied before the delete.
2108
- */ async deleteOne(id) {
2109
- this._ensureBuilt();
2110
- const filter = this._resolveIdFilter(id);
2111
- if (!filter) return { deletedCount: 0 };
2112
- if (this._integrity.needsCascade(this._cascadeResolver)) return require_nested_writer.remapDeleteFkViolation(this.tableName, () => this.adapter.withTransaction(async () => {
2113
- await this._integrity.cascadeBeforeDelete(filter, this.tableName, this._meta, this._cascadeResolver, (f) => this._fieldMapper.translateFilter(f, this._meta), this.adapter);
2114
- return this.adapter.deleteOne(this._fieldMapper.translateFilter(filter, this._meta));
2115
- }));
2116
- return require_nested_writer.remapDeleteFkViolation(this.tableName, () => this.adapter.deleteOne(this._fieldMapper.translateFilter(filter, this._meta)));
2117
- }
2118
- async updateMany(filter, data) {
2119
- this._ensureBuilt();
2120
- await this._integrity.validateForeignKeys([data], this._meta, this._fkLookupResolver, this._writeTableResolver, true);
2121
- return require_nested_writer.enrichFkViolation(this._meta, () => this.adapter.updateMany(this._fieldMapper.translateFilter(filter, this._meta), this._fieldMapper.prepareForWrite({ ...data }, this._meta, this.adapter)));
2122
- }
2123
- async replaceMany(filter, data) {
2124
- this._ensureBuilt();
2125
- await this._integrity.validateForeignKeys([data], this._meta, this._fkLookupResolver, this._writeTableResolver);
2126
- return require_nested_writer.enrichFkViolation(this._meta, () => this.adapter.replaceMany(this._fieldMapper.translateFilter(filter, this._meta), this._fieldMapper.prepareForWrite({ ...data }, this._meta, this.adapter)));
2127
- }
2128
- async deleteMany(filter) {
2129
- this._ensureBuilt();
2130
- if (this._integrity.needsCascade(this._cascadeResolver)) return require_nested_writer.remapDeleteFkViolation(this.tableName, () => this.adapter.withTransaction(async () => {
2131
- await this._integrity.cascadeBeforeDelete(filter, this.tableName, this._meta, this._cascadeResolver, (f) => this._fieldMapper.translateFilter(f, this._meta), this.adapter);
2132
- return this.adapter.deleteMany(this._fieldMapper.translateFilter(filter, this._meta));
2133
- }));
2134
- return require_nested_writer.remapDeleteFkViolation(this.tableName, () => this.adapter.deleteMany(this._fieldMapper.translateFilter(filter, this._meta)));
2135
- }
2136
- /**
2137
- * Synchronizes indexes between Atscript definitions and the database.
2138
- */ async syncIndexes() {
2139
- this._ensureBuilt();
2140
- return this.adapter.syncIndexes();
2141
- }
2142
- /**
2143
- * Ensures the table/collection exists in the database.
2144
- */ async ensureTable() {
2145
- this._ensureBuilt();
2146
- return this.adapter.ensureTable();
2147
- }
2148
- /**
2149
- * Applies default values for fields that are missing from the payload.
2150
- * Defaults handled natively by the DB engine are skipped — the field stays
2151
- * absent so the DB's own DEFAULT clause applies.
2152
- */ _applyDefaults(data) {
2153
- const nativeValues = this.adapter.supportsNativeValueDefaults();
2154
- const nativeFns = this.adapter.nativeDefaultFns();
2155
- for (const [field, def] of this._meta.defaults.entries()) if (data[field] === undefined) {
2156
- if (def.kind === "value" && !nativeValues) {
2157
- const fieldType = this._meta.flatMap?.get(field);
2158
- const designType = fieldType?.type.kind === "" && fieldType.type.designType;
2159
- data[field] = designType === "string" ? def.value : JSON.parse(def.value);
2160
- } else if (def.kind === "fn" && !nativeFns.has(def.fn)) switch (def.fn) {
2161
- case "now": {
2162
- data[field] = Date.now();
2163
- break;
2164
- }
2165
- case "uuid": {
2166
- data[field] = crypto.randomUUID();
2167
- break;
2168
- }
2169
- }
2170
- }
2171
- return data;
2172
- }
2173
- /**
2174
- * Extracts primary key field(s) from a payload to build a filter.
2175
- */ _extractPrimaryKeyFilter(payload) {
2176
- const pkFields = this.primaryKeys;
2177
- if (pkFields.length === 0) throw new require_nested_writer.DbError("NOT_FOUND", [{
2178
- path: "",
2179
- message: "No primary key defined — cannot extract filter"
2180
- }]);
2181
- const filter = {};
2182
- for (const field of pkFields) {
2183
- if (payload[field] === undefined) throw new require_nested_writer.DbError("NOT_FOUND", [{
2184
- path: field,
2185
- message: `Missing primary key field "${field}" in payload`
2186
- }]);
2187
- const fieldType = this.flatMap.get(field);
2188
- filter[field] = fieldType ? this.adapter.prepareId(payload[field], fieldType) : payload[field];
2189
- }
2190
- return filter;
2191
- }
2192
- /**
2193
- * Pre-validate items (type validation + FK constraints) without inserting them.
2194
- * Used by parent tables to validate FROM children before the main insert,
2195
- * ensuring errors are caught before the parent is committed.
2196
- *
2197
- * @param opts.excludeFkTargetTable - Skip FK validation to this table (the parent).
2198
- */ async preValidateItems(items, opts) {
2199
- this._ensureBuilt();
2200
- const validator = this.getValidator("insert");
2201
- const ctx = { mode: "insert" };
2202
- const prepared = items.map((raw) => this._applyDefaults({ ...raw }));
2203
- require_nested_writer.validateBatch(validator, prepared, ctx);
2204
- await this._integrity.validateForeignKeys(items, this._meta, this._fkLookupResolver, this._writeTableResolver, false, opts?.excludeFkTargetTable);
2205
- }
2206
- /**
2207
- * Builds a validator for a given purpose with adapter plugins.
2208
- *
2209
- * Uses annotation-based `replace` callback to make `@meta.id` and
2210
- * `@db.default` fields optional — works at all nesting levels
2211
- * (including inside nav field target types).
2212
- */ _buildValidator(purpose) {
2213
- const dbPlugin = createDbValidatorPlugin();
2214
- const plugins = [...this.adapter.getValidatorPlugins(), dbPlugin];
2215
- /**
2216
- * Forces nav fields non-optional so the plugin handles null/undefined
2217
- * checks (validator skips optional+null before plugins run).
2218
- */ const forceNavNonOptional = (type) => {
2219
- if (type.metadata?.has("db.rel.to") || type.metadata?.has("db.rel.from") || type.metadata?.has("db.rel.via")) return type.optional ? {
2220
- ...type,
2221
- optional: false
2222
- } : type;
2223
- return type;
2224
- };
2225
- /** Makes PK, defaulted, and FK fields optional; forces nav fields non-optional. */ const insertReplace = (type) => {
2226
- if (type.metadata?.has("meta.id") || type.metadata?.has("db.default") || type.metadata?.has("db.default.increment") || type.metadata?.has("db.default.uuid") || type.metadata?.has("db.default.now") || type.metadata?.has("db.rel.FK")) return {
2227
- ...type,
2228
- optional: true
2229
- };
2230
- return forceNavNonOptional(type);
2231
- };
2232
- switch (purpose) {
2233
- case "insert": return this.createValidator({
2234
- plugins,
2235
- replace: insertReplace
2236
- });
2237
- case "patch": return this.createValidator({
2238
- plugins,
2239
- partial: true,
2240
- replace: forceNavNonOptional
2241
- });
2242
- case "bulkReplace": return this.createValidator({
2243
- plugins,
2244
- replace: insertReplace
2245
- });
2246
- case "bulkUpdate": {
2247
- const navFields = this._meta.navFields;
2248
- return this.createValidator({
2249
- plugins,
2250
- partial: (_def, path) => {
2251
- if (path === "") return true;
2252
- const root = path.split(".")[0];
2253
- if (navFields.has(root)) return true;
2254
- return _def.metadata.get("db.patch.strategy") === "merge";
2255
- },
2256
- replace: forceNavNonOptional
2257
- });
2258
- }
2259
- default: return this.createValidator({ plugins });
2260
- }
2261
- }
2262
- constructor(_type, adapter, logger, _tableResolver, _writeTableResolver) {
2263
- super(_type, adapter, logger, _tableResolver), _define_property$3(this, "_cascadeResolver", void 0), _define_property$3(this, "_fkLookupResolver", void 0), _define_property$3(this, "_integrity", void 0), _define_property$3(this, "validators", new Map());
2264
- if (_writeTableResolver) this._writeTableResolver = _writeTableResolver;
2265
- this._integrity = adapter.supportsNativeForeignKeys() ? new NativeIntegrity() : new ApplicationIntegrity();
2266
- }
2267
- };
2268
-
2269
- //#endregion
2270
- //#region packages/db/src/table/db-view.ts
2271
- function _define_property$2(obj, key, value) {
2272
- if (key in obj) Object.defineProperty(obj, key, {
2273
- value,
2274
- enumerable: true,
2275
- configurable: true,
2276
- writable: true
2277
- });
2278
- else obj[key] = value;
2279
- return obj;
2280
- }
2281
- var AtscriptDbView = class extends AtscriptDbReadable {
2282
- get isView() {
2283
- return true;
2284
- }
2285
- /**
2286
- * Whether this is an external view — declared with `@db.view` only,
2287
- * without `@db.view.for`. External views reference pre-existing DB views
2288
- * and are not managed (created/dropped) by schema sync.
2289
- */ get isExternal() {
2290
- return !this._type.metadata.has("db.view.for");
2291
- }
2292
- /**
2293
- * Lazily resolves the view plan from `@db.view.*` metadata.
2294
- *
2295
- * - `db.view.for` → entry type ref (required)
2296
- * - `db.view.joins` → array of `{ target, condition }` (optional, multiple)
2297
- * - `db.view.filter` → query tree (optional)
2298
- * - `db.view.materialized` → boolean (optional)
2299
- */ get viewPlan() {
2300
- if (this._viewPlan) return this._viewPlan;
2301
- if (this.isExternal) throw new Error(`Cannot compute view plan for external view "${this.tableName}". ` + `External views (declared without @db.view.for) reference pre-existing DB views.`);
2302
- const metadata = this._type.metadata;
2303
- const forRef = metadata.get("db.view.for");
2304
- const entryType = typeof forRef === "function" ? forRef : forRef.type;
2305
- const entryTypeResolved = entryType();
2306
- const entryTable = entryTypeResolved?.metadata?.get("db.table") || entryTypeResolved?.id || "";
2307
- const rawJoins = metadata.get("db.view.joins");
2308
- const joins = [];
2309
- if (rawJoins) for (const join of rawJoins) {
2310
- const targetRef = join.target;
2311
- const targetType = typeof targetRef === "function" ? targetRef : targetRef.type;
2312
- const targetTypeResolved = targetType();
2313
- const targetTable = targetTypeResolved?.metadata?.get("db.table") || targetTypeResolved?.id || "";
2314
- joins.push({
2315
- targetType,
2316
- targetTable,
2317
- condition: join.condition
2318
- });
2319
- }
2320
- const filter = metadata.get("db.view.filter");
2321
- const having = metadata.get("db.view.having");
2322
- const materialized = metadata.has("db.view.materialized");
2323
- this._viewPlan = {
2324
- entryType,
2325
- entryTable,
2326
- joins,
2327
- filter,
2328
- having,
2329
- materialized
2330
- };
2331
- return this._viewPlan;
2332
- }
2333
- /**
2334
- * Resolves a query field ref to a quoted `table.column` SQL fragment.
2335
- *
2336
- * @param ref - The field reference from the query tree.
2337
- * @param qi - Identifier quoting function (e.g. backtick for MySQL, double-quote for SQLite).
2338
- * Defaults to double-quote wrapping for backwards compatibility.
2339
- */ resolveFieldRef(ref, qi = (n) => `"${n}"`) {
2340
- if (!ref.type) {
2341
- const plan = this.viewPlan;
2342
- return `${qi(plan.entryTable)}.${qi(ref.field)}`;
2343
- }
2344
- const resolved = ref.type();
2345
- const table = resolved?.metadata?.get("db.table") || resolved?.id || "";
2346
- return `${qi(table)}.${qi(ref.field)}`;
2347
- }
2348
- /**
2349
- * Maps each view field to its source table and column via ref chain.
2350
- * Fields without refs (inline definitions) map to the entry table with the same name.
2351
- */ getViewColumnMappings() {
2352
- const plan = this.viewPlan;
2353
- const mappings = [];
2354
- if (this._type.type.kind !== "object") return mappings;
2355
- const aggKeys = [
2356
- "db.agg.sum",
2357
- "db.agg.avg",
2358
- "db.agg.count",
2359
- "db.agg.min",
2360
- "db.agg.max"
2361
- ];
2362
- for (const [fieldName, fieldType] of this._type.type.props.entries()) {
2363
- let aggFn;
2364
- let aggField;
2365
- for (const key of aggKeys) {
2366
- const val = fieldType.metadata?.get(key);
2367
- if (val !== undefined) {
2368
- aggFn = key.split(".")[2];
2369
- aggField = typeof val === "string" ? val : "*";
2370
- break;
2371
- }
2372
- }
2373
- if (fieldType.ref) {
2374
- const resolved = fieldType.ref.type();
2375
- const sourceTable = resolved?.metadata?.get("db.table") || resolved?.id || "";
2376
- const sourceColumn = fieldType.ref.field || fieldName;
2377
- mappings.push({
2378
- viewColumn: fieldName,
2379
- sourceTable,
2380
- sourceColumn,
2381
- aggFn,
2382
- aggField
2383
- });
2384
- } else {
2385
- const sourceColumn = aggField && aggField !== "*" ? aggField : fieldName;
2386
- mappings.push({
2387
- viewColumn: fieldName,
2388
- sourceTable: plan.entryTable,
2389
- sourceColumn,
2390
- aggFn,
2391
- aggField
2392
- });
2393
- }
2394
- }
2395
- return mappings;
2396
- }
2397
- constructor(...args) {
2398
- super(...args), _define_property$2(this, "_viewPlan", void 0);
2399
- }
2400
- };
2401
-
2402
- //#endregion
2403
- //#region packages/db/src/base-adapter.ts
2404
- function _define_property$1(obj, key, value) {
2405
- if (key in obj) Object.defineProperty(obj, key, {
2406
- value,
2407
- enumerable: true,
2408
- configurable: true,
2409
- writable: true
2410
- });
2411
- else obj[key] = value;
2412
- return obj;
2413
- }
2414
- const EMPTY_DEFAULT_FNS = new Set();
2415
- const txStorage = new node_async_hooks.AsyncLocalStorage();
2416
- var BaseDbAdapter = class {
2417
- /**
2418
- * Returns the physical column name of the single @meta.id field (if any).
2419
- * Used to return the user's logical ID instead of the DB-generated ID on insert.
2420
- */ _getMetaIdPhysical() {
2421
- if (this._metaIdPhysical === undefined) {
2422
- const fields = this._table.originalMetaIdFields;
2423
- if (fields.length === 1) {
2424
- const field = fields[0];
2425
- this._metaIdPhysical = this._table.columnMap.get(field) ?? field;
2426
- } else this._metaIdPhysical = null;
2427
- }
2428
- return this._metaIdPhysical;
2429
- }
2430
- /**
2431
- * Resolves the correct insertedId: prefers the user-supplied PK value
2432
- * from the data over the DB-generated fallback (e.g. rowid, _id).
2433
- */ _resolveInsertedId(data, dbGeneratedId) {
2434
- const metaIdPhysical = this._getMetaIdPhysical();
2435
- return metaIdPhysical ? data[metaIdPhysical] ?? dbGeneratedId : dbGeneratedId;
2436
- }
2437
- /**
2438
- * Called by {@link AtscriptDbReadable} constructor. Gives the adapter access
2439
- * to the readable's computed metadata for internal use in query rendering,
2440
- * index sync, etc.
2441
- */ registerReadable(readable, logger) {
2442
- this._table = readable;
2443
- if (logger) this.logger = logger;
2444
- }
2445
- /**
2446
- * Enables or disables verbose (debug-level) logging for this adapter.
2447
- * When disabled, no log strings are constructed — zero overhead.
2448
- */ setVerbose(enabled) {
2449
- this._verbose = enabled;
2450
- }
2451
- /**
2452
- * Logs a debug message if verbose mode is enabled.
2453
- * Adapters call this to log DB operations with zero overhead when disabled.
2454
- */ _log(...args) {
2455
- if (!this._verbose) return;
2456
- this.logger.debug(...args);
2457
- }
2458
- /**
2459
- * Runs `fn` inside a database transaction. Nested calls (from related tables
2460
- * within the same async chain) reuse the existing transaction automatically.
2461
- *
2462
- * The generic layer handles nesting detection via `AsyncLocalStorage`.
2463
- * Adapters override `_beginTransaction`, `_commitTransaction`, and
2464
- * `_rollbackTransaction` to provide raw DB-specific transaction primitives.
2465
- */ async withTransaction(fn) {
2466
- if (txStorage.getStore()) return fn();
2467
- const ctx = { state: undefined };
2468
- ctx.state = await this._beginTransaction();
2469
- return txStorage.run(ctx, async () => {
2470
- try {
2471
- const result = await fn();
2472
- await this._commitTransaction(ctx.state);
2473
- return result;
2474
- } catch (error) {
2475
- try {
2476
- await this._rollbackTransaction(ctx.state);
2477
- } catch {}
2478
- throw error;
2479
- }
2480
- });
2481
- }
2482
- /**
2483
- * Returns the opaque transaction state from the current async context.
2484
- * Adapters use this to retrieve DB-specific state (e.g., MongoDB `ClientSession`).
2485
- */ _getTransactionState() {
2486
- return txStorage.getStore()?.state;
2487
- }
2488
- /**
2489
- * Runs `fn` inside the transaction ALS context with the given state.
2490
- * Adapters that override `withTransaction` (e.g., to use MongoDB's
2491
- * `session.withTransaction()` Convenient API) use this to set up the
2492
- * shared context so that nested adapters see the same session.
2493
- * If a context already exists (nesting), it's reused.
2494
- */ _runInTransactionContext(state, fn) {
2495
- if (txStorage.getStore()) return fn();
2496
- return txStorage.run({ state }, fn);
2497
- }
2498
- /**
2499
- * Starts a raw transaction. Returns opaque state stored in the async context.
2500
- * Override in adapters that support transactions.
2501
- */ async _beginTransaction() {
2502
- return undefined;
2503
- }
2504
- /** Commits the raw transaction. Override in adapters that support transactions. */ async _commitTransaction(_state) {}
2505
- /** Rolls back the raw transaction. Override in adapters that support transactions. */ async _rollbackTransaction(_state) {}
2506
- /**
2507
- * Returns additional validator plugins for this adapter.
2508
- * These are merged with the built-in Atscript validators.
2509
- *
2510
- * Example: MongoDB adapter returns ObjectId validation plugin.
2511
- */ getValidatorPlugins() {
2512
- return [];
2513
- }
2514
- /**
2515
- * Transforms an ID value for the database.
2516
- * Override to convert string → ObjectId, parse numeric IDs, etc.
2517
- *
2518
- * @param id - The raw ID value.
2519
- * @param fieldType - The annotated type of the ID field.
2520
- * @returns The transformed ID value.
2521
- */ prepareId(id, fieldType) {
2522
- return id;
2523
- }
2524
- /**
2525
- * Whether this adapter supports native patch operations.
2526
- * When `true`, {@link AtscriptDbTable} delegates patch payloads to
2527
- * {@link nativePatch} instead of using the generic decomposition.
2528
- */ supportsNativePatch() {
2529
- return false;
2530
- }
2531
- /**
2532
- * Whether this adapter handles nested objects natively.
2533
- * When `true`, the generic layer skips flattening and
2534
- * passes nested objects as-is to the adapter.
2535
- * MongoDB returns `true`; relational adapters return `false` (default).
2536
- */ supportsNestedObjects() {
2537
- return false;
2538
- }
2539
- /**
2540
- * Whether the DB engine handles static `@db.default "value"` natively
2541
- * via column-level DEFAULT clauses in CREATE TABLE.
2542
- * When `true`, `_applyDefaults()` skips client-side value defaults,
2543
- * letting the DB apply its own DEFAULT. SQL adapters return `true`;
2544
- * document stores (MongoDB) return `false` and apply defaults client-side.
2545
- */ supportsNativeValueDefaults() {
2546
- return false;
2547
- }
2548
- /**
2549
- * Function default names handled natively by this adapter's DB engine.
2550
- * Fields with these defaults are omitted from INSERT when no value is provided,
2551
- * letting the DB apply its own DEFAULT expression (e.g. CURRENT_TIMESTAMP, UUID()).
2552
- *
2553
- * Override in adapters whose DB engine supports function defaults.
2554
- * The generic layer checks this in `_applyDefaults()` to decide whether
2555
- * to generate the value client-side or leave it for the DB.
2556
- */ nativeDefaultFns() {
2557
- return EMPTY_DEFAULT_FNS;
2558
- }
2559
- /**
2560
- * Whether this adapter enforces foreign key constraints natively.
2561
- * When `true`, the generic layer skips application-level cascade/setNull
2562
- * on delete — the DB engine handles it (e.g. SQLite `ON DELETE CASCADE`).
2563
- * When `false` (default), the generic layer implements cascade logic
2564
- * by finding child records and deleting/nullifying them before the parent.
2565
- */ supportsNativeForeignKeys() {
2566
- return false;
2567
- }
2568
- /**
2569
- * Whether this adapter handles `$with` relation loading natively.
2570
- * When `true`, the table layer delegates to {@link loadRelations}
2571
- * instead of using the generic batch-loading strategy.
2572
- *
2573
- * Adapters can use this to implement SQL JOINs, MongoDB `$lookup`,
2574
- * or other DB-native relation loading optimizations.
2575
- *
2576
- * Default: `false` — the table layer uses application-level batch loading.
2577
- */ supportsNativeRelations() {
2578
- return false;
2579
- }
2580
- /**
2581
- * Loads relations onto result rows using adapter-native operations.
2582
- * Only called when {@link supportsNativeRelations} returns `true`.
2583
- *
2584
- * The adapter receives the rows to enrich, the `$with` relation specs,
2585
- * and the table's relation/FK metadata for resolution.
2586
- *
2587
- * @param rows - The result rows to enrich (mutable — add relation properties in place).
2588
- * @param withRelations - The `$with` specs from the query.
2589
- * @param relations - This table's relation metadata (from `@db.rel.to`/`@db.rel.from`).
2590
- * @param foreignKeys - This table's FK metadata (from `@db.rel.FK`).
2591
- * @param tableResolver - Optional callback to resolve annotated types to table metadata (needed for FROM/VIA relations).
2592
- */ async loadRelations(rows, withRelations, relations, foreignKeys, tableResolver) {
2593
- throw new Error("Native relation loading not supported by this adapter");
2594
- }
2595
- /**
2596
- * Applies a patch payload using native database operations.
2597
- * Only called when {@link supportsNativePatch} returns `true`.
2598
- *
2599
- * @param filter - Filter identifying the record to patch.
2600
- * @param patch - The patch payload with array operations.
2601
- * @returns Update result.
2602
- */ async nativePatch(filter, patch) {
2603
- throw new Error("Native patch not supported by this adapter");
2604
- }
2605
- /**
2606
- * Resolves the full table name, optionally including the schema prefix.
2607
- * Override for databases that don't support schemas (e.g., SQLite).
2608
- *
2609
- * @param includeSchema - Whether to prepend `schema.` prefix (default: true).
2610
- */ resolveTableName(includeSchema = true) {
2611
- const schema = this._table.schema;
2612
- const name = this._table.tableName;
2613
- return includeSchema && schema ? `${schema}.${name}` : name;
2614
- }
2615
- /**
2616
- * Template method for index synchronization.
2617
- * Implements the diff algorithm (list → compare → create/drop).
2618
- * Adapters provide the three DB-specific primitives.
2619
- *
2620
- * @example
2621
- * ```typescript
2622
- * async syncIndexes() {
2623
- * await this.syncIndexesWithDiff({
2624
- * listExisting: async () => this.driver.all('PRAGMA index_list(...)'),
2625
- * createIndex: async (index) => this.driver.exec('CREATE INDEX ...'),
2626
- * dropIndex: async (name) => this.driver.exec('DROP INDEX ...'),
2627
- * shouldSkipType: (type) => type === 'fulltext',
2628
- * })
2629
- * }
2630
- * ```
2631
- */ async syncIndexesWithDiff(opts) {
2632
- const prefix = opts.prefix ?? "atscript__";
2633
- const existing = await opts.listExisting();
2634
- const existingNames = new Set(existing.filter((i) => i.name.startsWith(prefix)).map((i) => i.name));
2635
- const desiredNames = new Set();
2636
- for (const index of this._table.indexes.values()) {
2637
- if (opts.shouldSkipType?.(index.type)) continue;
2638
- desiredNames.add(index.key);
2639
- if (!existingNames.has(index.key)) await opts.createIndex(index);
2640
- }
2641
- for (const name of existingNames) if (!desiredNames.has(name)) await opts.dropIndex(name);
2642
- }
2643
- /**
2644
- * Returns available search indexes for this adapter.
2645
- * UI uses this to show index picker. Override in adapters that support search.
2646
- */ getSearchIndexes() {
2647
- return [];
2648
- }
2649
- /**
2650
- * Whether this adapter supports text search.
2651
- * Default: `true` when {@link getSearchIndexes} returns any entries.
2652
- */ isSearchable() {
2653
- return this.getSearchIndexes().length > 0;
2654
- }
2655
- /**
2656
- * Whether this adapter supports vector similarity search.
2657
- * Override in adapters that support vector search.
2658
- */ isVectorSearchable() {
2659
- return false;
2660
- }
2661
- /**
2662
- * Full-text search. Override in adapters that support search.
2663
- *
2664
- * @param text - Search text.
2665
- * @param query - Filter, sort, limit, etc.
2666
- * @param indexName - Optional search index to target.
2667
- */ async search(text, query, indexName) {
2668
- throw new Error("Search not supported by this adapter");
2669
- }
2670
- /**
2671
- * Full-text search with count (for paginated search results).
2672
- *
2673
- * @param text - Search text.
2674
- * @param query - Filter, sort, limit, etc.
2675
- * @param indexName - Optional search index to target.
2676
- */ async searchWithCount(text, query, indexName) {
2677
- throw new Error("Search not supported by this adapter");
2678
- }
2679
- /**
2680
- * Vector similarity search. Override in adapters that support vector search.
2681
- *
2682
- * @param vector - Pre-computed embedding vector.
2683
- * @param query - Filter, sort, limit, etc.
2684
- * @param indexName - Optional vector index to target (for multi-vector documents).
2685
- */ async vectorSearch(vector, query, indexName) {
2686
- throw new Error("Vector search not supported by this adapter");
2687
- }
2688
- /**
2689
- * Vector similarity search with count (for paginated results).
2690
- *
2691
- * @param vector - Pre-computed embedding vector.
2692
- * @param query - Filter, sort, limit, etc.
2693
- * @param indexName - Optional vector index to target (for multi-vector documents).
2694
- */ async vectorSearchWithCount(vector, query, indexName) {
2695
- throw new Error("Vector search not supported by this adapter");
2696
- }
2697
- /**
2698
- * Fetches records and total count in one call.
2699
- * Default: two parallel calls. Adapters may override for single-query optimization.
2700
- */ async findManyWithCount(query) {
2701
- const [data, count] = await Promise.all([this.findMany(query), this.count(query)]);
2702
- return {
2703
- data,
2704
- count
2705
- };
2706
- }
2707
- /**
2708
- * Executes an aggregate query (GROUP BY + aggregate functions).
2709
- * Default throws — override in adapters that support aggregation.
2710
- */ async aggregate(_query) {
2711
- throw new Error("Aggregation not supported by this adapter");
2712
- }
2713
- constructor() {
2714
- _define_property$1(this, "_table", void 0);
2715
- _define_property$1(this, "_metaIdPhysical", void 0);
2716
- /** Logger instance — set via {@link registerReadable} from the readable's logger. */ _define_property$1(this, "logger", require_logger.NoopLogger);
2717
- /** When true, adapter logs DB calls via `logger.debug`. Off by default. */ _define_property$1(this, "_verbose", false);
2718
- /**
2719
- * When true, the adapter can handle column type changes in-place
2720
- * (e.g. MySQL's ALTER TABLE MODIFY COLUMN) without requiring table recreation.
2721
- * The generic sync layer will delegate type changes to {@link syncColumns}
2722
- * instead of requiring `@db.sync.method "recreate"` or `"drop"`.
2723
- */ _define_property$1(this, "supportsColumnModify", void 0);
2724
- }
2725
- };
2726
-
2727
- //#endregion
2728
- //#region packages/db/src/table/db-space.ts
2729
- function _define_property(obj, key, value) {
2730
- if (key in obj) Object.defineProperty(obj, key, {
2731
- value,
2732
- enumerable: true,
2733
- configurable: true,
2734
- writable: true
2735
- });
2736
- else obj[key] = value;
2737
- return obj;
2738
- }
17
+ * ```typescript
18
+ * // SQLite
19
+ * const driver = new BetterSqlite3Driver(':memory:')
20
+ * const db = new DbSpace(() => new SqliteAdapter(driver))
21
+ * const users = db.getTable(UsersType)
22
+ * const activeUsers = db.getView(ActiveUsersType)
23
+ * ```
24
+ */
2739
25
  var DbSpace = class {
26
+ _readables = /* @__PURE__ */ new WeakMap();
27
+ /** All tables created in this space — used for reverse FK lookup during cascade. */
28
+ _allTables = /* @__PURE__ */ new Set();
29
+ /** Lazily created adapter for administrative ops (drop table/view) that don't need a registered readable. */
30
+ _adminAdapter;
31
+ constructor(adapterFactory, logger = require_db_view.NoopLogger) {
32
+ this.adapterFactory = adapterFactory;
33
+ this.logger = logger;
34
+ }
2740
35
  /**
2741
36
  * Auto-detects whether the type is a table or view and returns the
2742
37
  * appropriate instance. Uses `@db.view` or `@db.view.for` presence to distinguish.
2743
- */ get(type, logger) {
38
+ */
39
+ get(type, logger) {
2744
40
  if (type.metadata.has("db.view") || type.metadata.has("db.view.for")) return this.getView(type, logger);
2745
41
  return this.getTable(type, logger);
2746
42
  }
2747
43
  /**
2748
44
  * Returns the table for the given annotated type.
2749
45
  * Creates the table + adapter on first access, caches for subsequent calls.
2750
- */ getTable(type, logger) {
46
+ */
47
+ getTable(type, logger) {
2751
48
  let readable = this._readables.get(type);
2752
49
  if (!readable) {
2753
- const adapter = this.adapterFactory();
2754
- readable = new AtscriptDbTable(type, adapter, logger || this.logger, (t) => this.get(t), (t) => {
50
+ readable = new require_db_view.AtscriptDbTable(type, this.adapterFactory(), logger || this.logger, (t) => this.get(t), (t) => {
2755
51
  const resolved = this.get(t);
2756
- return resolved instanceof AtscriptDbTable ? resolved : undefined;
52
+ return resolved instanceof require_db_view.AtscriptDbTable ? resolved : void 0;
2757
53
  });
2758
54
  this._allTables.add(readable);
2759
55
  readable.setCascadeResolver((tableName) => this._getCascadeTargets(tableName));
@@ -2765,11 +61,11 @@ var DbSpace = class {
2765
61
  /**
2766
62
  * Returns the view for the given annotated type.
2767
63
  * Creates the view + adapter on first access, caches for subsequent calls.
2768
- */ getView(type, logger) {
64
+ */
65
+ getView(type, logger) {
2769
66
  let readable = this._readables.get(type);
2770
67
  if (!readable) {
2771
- const adapter = this.adapterFactory();
2772
- readable = new AtscriptDbView(type, adapter, logger || this.logger, (t) => this.get(t));
68
+ readable = new require_db_view.AtscriptDbView(type, this.adapterFactory(), logger || this.logger, (t) => this.get(t));
2773
69
  this._readables.set(type, readable);
2774
70
  }
2775
71
  return readable;
@@ -2777,29 +73,32 @@ var DbSpace = class {
2777
73
  /**
2778
74
  * Returns the adapter for the given annotated type.
2779
75
  * Creates the table/view + adapter on first access if needed.
2780
- */ getAdapter(type) {
2781
- const readable = this.get(type);
2782
- return readable.dbAdapter;
76
+ */
77
+ getAdapter(type) {
78
+ return this.get(type).dbAdapter;
2783
79
  }
2784
80
  /**
2785
81
  * Drops a table by name. Used by schema sync to remove tables no longer in the schema.
2786
- */ async dropTableByName(tableName) {
82
+ */
83
+ async dropTableByName(tableName) {
2787
84
  const adapter = this._getAdminAdapter();
2788
85
  if (adapter.dropTableByName) await adapter.dropTableByName(tableName);
2789
86
  }
2790
87
  /**
2791
88
  * Drops a view by name. Used by schema sync to remove views no longer in the schema.
2792
- */ async dropViewByName(viewName) {
89
+ */
90
+ async dropViewByName(viewName) {
2793
91
  const adapter = this._getAdminAdapter();
2794
92
  if (adapter.dropViewByName) await adapter.dropViewByName(viewName);
2795
93
  }
2796
94
  _getAdminAdapter() {
2797
- return this._adminAdapter ?? (this._adminAdapter = this.adapterFactory());
95
+ return this._adminAdapter ??= this.adapterFactory();
2798
96
  }
2799
97
  /**
2800
98
  * Finds all child tables with FKs pointing to the given parent table name.
2801
99
  * Accesses `table.foreignKeys` which triggers `_flatten()` if needed.
2802
- */ _getCascadeTargets(tableName) {
100
+ */
101
+ _getCascadeTargets(tableName) {
2803
102
  const targets = [];
2804
103
  for (const table of this._allTables) for (const fk of table.foreignKeys.values()) if (fk.targetTable === tableName && fk.onDelete) targets.push({
2805
104
  fk,
@@ -2813,25 +112,18 @@ var DbSpace = class {
2813
112
  /**
2814
113
  * Resolves a table name to a queryable target for FK validation.
2815
114
  * Searches all registered tables for one with the matching table name.
2816
- */ _getFkLookupTarget(tableName) {
115
+ */
116
+ _getFkLookupTarget(tableName) {
2817
117
  for (const table of this._allTables) if (table.tableName === tableName) return { count: (filter) => table.count({ filter }) };
2818
- return undefined;
2819
- }
2820
- constructor(adapterFactory, logger = require_logger.NoopLogger) {
2821
- _define_property(this, "adapterFactory", void 0);
2822
- _define_property(this, "logger", void 0);
2823
- _define_property(this, "_readables", void 0);
2824
- /** All tables created in this space — used for reverse FK lookup during cascade. */ _define_property(this, "_allTables", void 0);
2825
- /** Lazily created adapter for administrative ops (drop table/view) that don't need a registered readable. */ _define_property(this, "_adminAdapter", void 0);
2826
- this.adapterFactory = adapterFactory;
2827
- this.logger = logger;
2828
- this._readables = new WeakMap();
2829
- this._allTables = new Set();
2830
118
  }
2831
119
  };
2832
-
2833
120
  //#endregion
2834
- //#region packages/db/src/query/query-tree.ts
121
+ //#region src/query/query-tree.ts
122
+ /**
123
+ * Translates a JS-emitted query tree into a FilterExpr.
124
+ * Resolves field references (type + field path) to physical column names
125
+ * via the provided resolver function.
126
+ */
2835
127
  function translateQueryTree(node, resolveField) {
2836
128
  if ("$and" in node) return { $and: node.$and.map((n) => translateQueryTree(n, resolveField)) };
2837
129
  if ("$or" in node) return { $or: node.$or.map((n) => translateQueryTree(n, resolveField)) };
@@ -2845,43 +137,45 @@ function translateQueryTree(node, resolveField) {
2845
137
  if (comp.op === "$exists") return { [leftField]: { $exists: true } };
2846
138
  return { [leftField]: { [comp.op]: comp.right } };
2847
139
  }
2848
-
2849
140
  //#endregion
2850
- exports.ApplicationIntegrity = ApplicationIntegrity
2851
- exports.AtscriptDbReadable = AtscriptDbReadable
2852
- exports.AtscriptDbTable = AtscriptDbTable
2853
- exports.AtscriptDbView = AtscriptDbView
2854
- exports.BaseDbAdapter = BaseDbAdapter
2855
- exports.DbError = require_nested_writer.DbError
2856
- exports.DbSpace = DbSpace
2857
- exports.DocumentFieldMapper = DocumentFieldMapper
2858
- exports.FieldMappingStrategy = FieldMappingStrategy
2859
- exports.IntegrityStrategy = IntegrityStrategy
2860
- exports.NativeIntegrity = NativeIntegrity
2861
- exports.NoopLogger = require_logger.NoopLogger
2862
- exports.RelationalFieldMapper = RelationalFieldMapper
2863
- exports.TableMetadata = TableMetadata
2864
- exports.UniquSelect = UniquSelect
2865
- Object.defineProperty(exports, 'computeInsights', {
2866
- enumerable: true,
2867
- get: function () {
2868
- return __uniqu_core.computeInsights;
2869
- }
141
+ exports.ApplicationIntegrity = require_db_view.ApplicationIntegrity;
142
+ exports.AtscriptDbReadable = require_db_view.AtscriptDbReadable;
143
+ exports.AtscriptDbTable = require_db_view.AtscriptDbTable;
144
+ exports.AtscriptDbView = require_db_view.AtscriptDbView;
145
+ exports.BaseDbAdapter = require_db_view.BaseDbAdapter;
146
+ exports.DbError = require_nested_writer.DbError;
147
+ exports.DbSpace = DbSpace;
148
+ exports.DocumentFieldMapper = require_db_view.DocumentFieldMapper;
149
+ exports.FieldMappingStrategy = require_db_view.FieldMappingStrategy;
150
+ exports.IntegrityStrategy = require_db_view.IntegrityStrategy;
151
+ exports.NativeIntegrity = require_db_view.NativeIntegrity;
152
+ exports.NoopLogger = require_db_view.NoopLogger;
153
+ exports.RelationalFieldMapper = require_db_view.RelationalFieldMapper;
154
+ exports.TableMetadata = require_db_view.TableMetadata;
155
+ exports.UniquSelect = require_db_view.UniquSelect;
156
+ Object.defineProperty(exports, "computeInsights", {
157
+ enumerable: true,
158
+ get: function() {
159
+ return _uniqu_core.computeInsights;
160
+ }
2870
161
  });
2871
- exports.createDbValidatorPlugin = createDbValidatorPlugin
2872
- exports.decomposePatch = decomposePatch
2873
- exports.getKeyProps = getKeyProps
2874
- Object.defineProperty(exports, 'isPrimitive', {
2875
- enumerable: true,
2876
- get: function () {
2877
- return __uniqu_core.isPrimitive;
2878
- }
162
+ exports.createDbValidatorPlugin = require_db_view.createDbValidatorPlugin;
163
+ exports.decomposePatch = require_db_view.decomposePatch;
164
+ exports.getDbFieldOp = require_ops.getDbFieldOp;
165
+ exports.getKeyProps = require_db_view.getKeyProps;
166
+ exports.isDbFieldOp = require_ops.isDbFieldOp;
167
+ Object.defineProperty(exports, "isPrimitive", {
168
+ enumerable: true,
169
+ get: function() {
170
+ return _uniqu_core.isPrimitive;
171
+ }
172
+ });
173
+ exports.resolveDesignType = require_db_view.resolveDesignType;
174
+ exports.separateFieldOps = require_ops.separateFieldOps;
175
+ exports.translateQueryTree = translateQueryTree;
176
+ Object.defineProperty(exports, "walkFilter", {
177
+ enumerable: true,
178
+ get: function() {
179
+ return _uniqu_core.walkFilter;
180
+ }
2879
181
  });
2880
- exports.resolveDesignType = resolveDesignType
2881
- exports.translateQueryTree = translateQueryTree
2882
- Object.defineProperty(exports, 'walkFilter', {
2883
- enumerable: true,
2884
- get: function () {
2885
- return __uniqu_core.walkFilter;
2886
- }
2887
- });