@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.
- package/README.md +18 -18
- package/dist/agg.cjs +8 -3
- package/dist/agg.d.cts +7 -0
- package/dist/agg.d.mts +7 -0
- package/dist/agg.mjs +7 -3
- package/dist/control-DRgryKeg.cjs +14 -0
- package/dist/{control_as-bjmwe24C.mjs → control-IANbnfjG.mjs} +6 -18
- package/dist/db-readable-BQQzfguJ.d.cts +1249 -0
- package/dist/db-readable-Bbr4CjMb.d.mts +1249 -0
- package/dist/db-space-BUrQ5BFm.d.mts +309 -0
- package/dist/db-space-Vxpcnyt5.d.cts +309 -0
- package/dist/db-validator-plugin-07kDiis2.d.cts +22 -0
- package/dist/db-validator-plugin-CiqsHTI_.d.mts +22 -0
- package/dist/db-view-BntnAmXO.cjs +3071 -0
- package/dist/db-view-ZsoN91-q.mjs +2970 -0
- package/dist/index.cjs +95 -2801
- package/dist/index.d.cts +137 -0
- package/dist/index.d.mts +137 -0
- package/dist/index.mjs +55 -2761
- package/dist/{nested-writer-BkqL7cp3.cjs → nested-writer-BDXsDMPP.cjs} +196 -150
- package/dist/{nested-writer-NEN51mnR.mjs → nested-writer-Dmm1gbZV.mjs} +118 -70
- package/dist/ops-BdRAFLKY.d.mts +67 -0
- package/dist/ops-DXJ4Zw0P.d.cts +67 -0
- package/dist/ops.cjs +123 -0
- package/dist/ops.d.cts +2 -0
- package/dist/ops.d.mts +2 -0
- package/dist/ops.mjs +112 -0
- package/dist/plugin.cjs +90 -109
- package/dist/plugin.d.cts +6 -0
- package/dist/plugin.d.mts +6 -0
- package/dist/plugin.mjs +29 -49
- package/dist/rel.cjs +20 -20
- package/dist/rel.d.cts +119 -0
- package/dist/rel.d.mts +119 -0
- package/dist/rel.mjs +4 -5
- package/dist/{relation-helpers-guFL_oRf.cjs → relation-helpers-BYvsE1tR.cjs} +26 -22
- package/dist/{relation-helpers-DyBIlQnB.mjs → relation-helpers-CLasawQq.mjs} +11 -6
- package/dist/{relation-loader-Dv7qXYq7.mjs → relation-loader-BEOTXNcq.mjs} +63 -43
- package/dist/{relation-loader-CpnDRf9k.cjs → relation-loader-CRC5LcqM.cjs} +74 -49
- package/dist/shared.cjs +13 -13
- package/dist/{shared.d.ts → shared.d.cts} +14 -13
- package/dist/shared.d.mts +71 -0
- package/dist/shared.mjs +2 -3
- package/dist/sync.cjs +300 -252
- package/dist/sync.d.cts +369 -0
- package/dist/sync.d.mts +369 -0
- package/dist/sync.mjs +284 -233
- package/dist/{validation-utils-DEoCMmEb.cjs → validation-utils-DVJDijnB.cjs} +141 -109
- package/dist/{validation-utils-DhR_mtKa.mjs → validation-utils-DhjIjP1-.mjs} +71 -37
- package/package.json +30 -29
- package/LICENSE +0 -21
- package/dist/agg-BJFJ3dFQ.mjs +0 -8
- package/dist/agg-DnUWAOK8.cjs +0 -14
- package/dist/agg.d.ts +0 -13
- package/dist/chunk-CrpGerW8.cjs +0 -31
- package/dist/control_as-BFPERAF_.cjs +0 -28
- package/dist/index.d.ts +0 -1706
- package/dist/logger-B7oxCfLQ.mjs +0 -12
- package/dist/logger-Dt2v_-wb.cjs +0 -18
- package/dist/plugin.d.ts +0 -5
- package/dist/rel.d.ts +0 -1305
- package/dist/relation-loader-D4mTw6yH.cjs +0 -4
- package/dist/relation-loader-Ggy1ujwR.mjs +0 -4
- package/dist/sync.d.ts +0 -1878
package/dist/index.cjs
CHANGED
|
@@ -1,2759 +1,55 @@
|
|
|
1
|
-
"
|
|
2
|
-
const
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
1659
|
-
*
|
|
1660
|
-
*
|
|
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
|
-
*
|
|
1882
|
-
*
|
|
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
|
-
*
|
|
1890
|
-
*
|
|
1891
|
-
*
|
|
1892
|
-
*
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
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
|
-
*/
|
|
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
|
-
*/
|
|
46
|
+
*/
|
|
47
|
+
getTable(type, logger) {
|
|
2751
48
|
let readable = this._readables.get(type);
|
|
2752
49
|
if (!readable) {
|
|
2753
|
-
|
|
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 :
|
|
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
|
-
*/
|
|
64
|
+
*/
|
|
65
|
+
getView(type, logger) {
|
|
2769
66
|
let readable = this._readables.get(type);
|
|
2770
67
|
if (!readable) {
|
|
2771
|
-
|
|
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
|
-
*/
|
|
2781
|
-
|
|
2782
|
-
return
|
|
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
|
-
*/
|
|
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
|
-
*/
|
|
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
|
|
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
|
-
*/
|
|
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
|
-
*/
|
|
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
|
|
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 =
|
|
2862
|
-
exports.RelationalFieldMapper = RelationalFieldMapper
|
|
2863
|
-
exports.TableMetadata = TableMetadata
|
|
2864
|
-
exports.UniquSelect = UniquSelect
|
|
2865
|
-
Object.defineProperty(exports,
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
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.
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
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
|
-
});
|