@atscript/utils-db 0.1.31 → 0.1.32
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/dist/index.cjs +373 -121
- package/dist/index.d.ts +205 -34
- package/dist/index.mjs +373 -122
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -101,6 +101,78 @@ else update[key] = value;
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
//#endregion
|
|
105
|
+
//#region packages/utils-db/src/uniqu-select.ts
|
|
106
|
+
function _define_property$2(obj, key, value) {
|
|
107
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
108
|
+
value,
|
|
109
|
+
enumerable: true,
|
|
110
|
+
configurable: true,
|
|
111
|
+
writable: true
|
|
112
|
+
});
|
|
113
|
+
else obj[key] = value;
|
|
114
|
+
return obj;
|
|
115
|
+
}
|
|
116
|
+
var UniquSelect = class {
|
|
117
|
+
/**
|
|
118
|
+
* Resolved inclusion array of field names.
|
|
119
|
+
* For exclusion form, inverts using `allFields` from constructor.
|
|
120
|
+
*/ get asArray() {
|
|
121
|
+
if (this._arrayResolved) return this._array;
|
|
122
|
+
this._arrayResolved = true;
|
|
123
|
+
if (Array.isArray(this._raw)) {
|
|
124
|
+
this._array = this._raw;
|
|
125
|
+
return this._array;
|
|
126
|
+
}
|
|
127
|
+
const raw = this._raw;
|
|
128
|
+
const entries = Object.entries(raw);
|
|
129
|
+
if (entries.length === 0) return undefined;
|
|
130
|
+
if (entries[0][1] === 1) {
|
|
131
|
+
const result = [];
|
|
132
|
+
for (const entry of entries) if (entry[1] === 1) result.push(entry[0]);
|
|
133
|
+
this._array = result;
|
|
134
|
+
} else {
|
|
135
|
+
if (!this._allFields) return undefined;
|
|
136
|
+
const excluded = new Set();
|
|
137
|
+
for (const entry of entries) if (entry[1] === 0) excluded.add(entry[0]);
|
|
138
|
+
const result = [];
|
|
139
|
+
for (const field of this._allFields) if (!excluded.has(field)) result.push(field);
|
|
140
|
+
this._array = result;
|
|
141
|
+
}
|
|
142
|
+
return this._array;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Record projection preserving original semantics.
|
|
146
|
+
* Returns original object as-is if raw was object.
|
|
147
|
+
* Converts `string[]` to `{field: 1}` inclusion object.
|
|
148
|
+
*/ get asProjection() {
|
|
149
|
+
if (this._projectionResolved) return this._projection;
|
|
150
|
+
this._projectionResolved = true;
|
|
151
|
+
if (!Array.isArray(this._raw)) {
|
|
152
|
+
const raw = this._raw;
|
|
153
|
+
if (Object.keys(raw).length === 0) return undefined;
|
|
154
|
+
this._projection = raw;
|
|
155
|
+
return this._projection;
|
|
156
|
+
}
|
|
157
|
+
const arr = this._raw;
|
|
158
|
+
if (arr.length === 0) return undefined;
|
|
159
|
+
const result = {};
|
|
160
|
+
for (const item of arr) result[item] = 1;
|
|
161
|
+
this._projection = result;
|
|
162
|
+
return this._projection;
|
|
163
|
+
}
|
|
164
|
+
constructor(raw, allFields) {
|
|
165
|
+
_define_property$2(this, "_raw", void 0);
|
|
166
|
+
_define_property$2(this, "_allFields", void 0);
|
|
167
|
+
_define_property$2(this, "_arrayResolved", false);
|
|
168
|
+
_define_property$2(this, "_array", void 0);
|
|
169
|
+
_define_property$2(this, "_projectionResolved", false);
|
|
170
|
+
_define_property$2(this, "_projection", void 0);
|
|
171
|
+
this._raw = raw;
|
|
172
|
+
this._allFields = allFields;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
104
176
|
//#endregion
|
|
105
177
|
//#region packages/utils-db/src/db-table.ts
|
|
106
178
|
function _define_property$1(obj, key, value) {
|
|
@@ -120,11 +192,18 @@ function resolveDesignType(fieldType) {
|
|
|
120
192
|
if (fieldType.type.kind === "array") return "array";
|
|
121
193
|
return "string";
|
|
122
194
|
}
|
|
195
|
+
/** Coerces a storage value (0/1/null) back to a JS boolean. */ function toBool(value) {
|
|
196
|
+
if (value === null || value === undefined) return value;
|
|
197
|
+
return !!value;
|
|
198
|
+
}
|
|
123
199
|
function indexKey(type, name) {
|
|
124
200
|
const cleanName = name.replace(/[^a-z0-9_.-]/gi, "_").replace(/_+/g, "_").slice(0, 127 - INDEX_PREFIX.length - type.length - 2);
|
|
125
201
|
return `${INDEX_PREFIX}${type}__${cleanName}`;
|
|
126
202
|
}
|
|
127
203
|
var AtscriptDbTable = class {
|
|
204
|
+
/** Returns the underlying adapter with its concrete type preserved. */ getAdapter() {
|
|
205
|
+
return this.adapter;
|
|
206
|
+
}
|
|
128
207
|
/** The raw annotated type. */ get type() {
|
|
129
208
|
return this._type;
|
|
130
209
|
}
|
|
@@ -140,6 +219,29 @@ var AtscriptDbTable = class {
|
|
|
140
219
|
this._flatten();
|
|
141
220
|
return this._primaryKeys;
|
|
142
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* Registers an additional primary key field.
|
|
224
|
+
* Useful for adapters (e.g., MongoDB) where `_id` is always the primary key
|
|
225
|
+
* even without an explicit `@meta.id` annotation.
|
|
226
|
+
*
|
|
227
|
+
* Typically called from {@link BaseDbAdapter.onFieldScanned}.
|
|
228
|
+
*/ addPrimaryKey(field) {
|
|
229
|
+
if (!this._primaryKeys.includes(field)) this._primaryKeys.push(field);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Removes a field from the primary key list.
|
|
233
|
+
* Useful for adapters (e.g., MongoDB) where `@meta.id` fields should be
|
|
234
|
+
* unique indexes rather than part of the primary key.
|
|
235
|
+
*/ removePrimaryKey(field) {
|
|
236
|
+
const idx = this._primaryKeys.indexOf(field);
|
|
237
|
+
if (idx >= 0) this._primaryKeys.splice(idx, 1);
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Registers a field as having a unique constraint.
|
|
241
|
+
* Used by adapters to ensure `findById` falls back to this field.
|
|
242
|
+
*/ addUniqueField(field) {
|
|
243
|
+
this._uniqueProps.add(field);
|
|
244
|
+
}
|
|
143
245
|
/** Logical → physical column name mapping from `@db.column`. */ get columnMap() {
|
|
144
246
|
this._flatten();
|
|
145
247
|
return this._columnMap;
|
|
@@ -179,13 +281,13 @@ var AtscriptDbTable = class {
|
|
|
179
281
|
this._flatten();
|
|
180
282
|
if (!this._fieldDescriptors) {
|
|
181
283
|
this._fieldDescriptors = [];
|
|
182
|
-
const skipFlattening = this.
|
|
284
|
+
const skipFlattening = this._nestedObjects;
|
|
183
285
|
for (const [path, type] of this._flatMap.entries()) {
|
|
184
286
|
if (!path) continue;
|
|
185
287
|
if (!skipFlattening && this._flattenedParents.has(path)) continue;
|
|
186
|
-
if (!skipFlattening && this.
|
|
288
|
+
if (!skipFlattening && this._findAncestorInSet(path, this._jsonFields) !== undefined) continue;
|
|
187
289
|
const isJson = this._jsonFields.has(path);
|
|
188
|
-
const isFlattened = !skipFlattening && this.
|
|
290
|
+
const isFlattened = !skipFlattening && this._findAncestorInSet(path, this._flattenedParents) !== undefined;
|
|
189
291
|
const designType = isJson ? "json" : resolveDesignType(type);
|
|
190
292
|
let storage;
|
|
191
293
|
if (skipFlattening) storage = "column";
|
|
@@ -206,33 +308,11 @@ else storage = "column";
|
|
|
206
308
|
flattenedFrom: isFlattened ? path : undefined
|
|
207
309
|
});
|
|
208
310
|
}
|
|
311
|
+
Object.freeze(this._fieldDescriptors);
|
|
209
312
|
}
|
|
210
313
|
return this._fieldDescriptors;
|
|
211
314
|
}
|
|
212
315
|
/**
|
|
213
|
-
* Resolves `$select` from {@link UniqueryControls} to a list of field names.
|
|
214
|
-
* - `undefined` → `undefined` (all fields)
|
|
215
|
-
* - `string[]` → pass through
|
|
216
|
-
* - `Record<K, 1>` → extract included keys
|
|
217
|
-
* - `Record<K, 0>` → invert using known field names
|
|
218
|
-
*/ resolveProjection(select) {
|
|
219
|
-
if (!select) return undefined;
|
|
220
|
-
if (Array.isArray(select)) return select.length > 0 ? select : undefined;
|
|
221
|
-
const selectObj = select;
|
|
222
|
-
const keys = Object.keys(selectObj);
|
|
223
|
-
if (keys.length === 0) return undefined;
|
|
224
|
-
if (selectObj[keys[0]] === 1) {
|
|
225
|
-
const result$1 = [];
|
|
226
|
-
for (const k of keys) if (selectObj[k] === 1) result$1.push(k);
|
|
227
|
-
return result$1;
|
|
228
|
-
}
|
|
229
|
-
const excluded = new Set();
|
|
230
|
-
for (const k of keys) if (selectObj[k] === 0) excluded.add(k);
|
|
231
|
-
const result = [];
|
|
232
|
-
for (const f of this.fieldDescriptors) if (!f.ignored && !excluded.has(f.path)) result.push(f.path);
|
|
233
|
-
return result;
|
|
234
|
-
}
|
|
235
|
-
/**
|
|
236
316
|
* Creates a new validator with custom options.
|
|
237
317
|
* Adapter plugins are NOT automatically included — use {@link getValidator}
|
|
238
318
|
* for the standard validator with adapter plugins.
|
|
@@ -345,6 +425,106 @@ else storage = "column";
|
|
|
345
425
|
});
|
|
346
426
|
return this.adapter.count(this._translateQuery(query));
|
|
347
427
|
}
|
|
428
|
+
/**
|
|
429
|
+
* Finds records and total count in a single logical call.
|
|
430
|
+
* Adapters may optimize into a single query (e.g., MongoDB `$facet`).
|
|
431
|
+
*/ async findManyWithCount(query) {
|
|
432
|
+
this._flatten();
|
|
433
|
+
const translated = this._translateQuery(query);
|
|
434
|
+
const result = await this.adapter.findManyWithCount(translated);
|
|
435
|
+
return {
|
|
436
|
+
data: result.data.map((row) => this._reconstructFromRead(row)),
|
|
437
|
+
count: result.count
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
/** Whether the underlying adapter supports text search. */ isSearchable() {
|
|
441
|
+
return this.adapter.isSearchable();
|
|
442
|
+
}
|
|
443
|
+
/** Returns available search indexes from the adapter. */ getSearchIndexes() {
|
|
444
|
+
return this.adapter.getSearchIndexes();
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Full-text search with query translation and result reconstruction.
|
|
448
|
+
*
|
|
449
|
+
* @param text - Search text.
|
|
450
|
+
* @param query - Filter, sort, limit, etc.
|
|
451
|
+
* @param indexName - Optional search index to target.
|
|
452
|
+
*/ async search(text, query, indexName) {
|
|
453
|
+
this._flatten();
|
|
454
|
+
const translated = this._translateQuery(query);
|
|
455
|
+
const results = await this.adapter.search(text, translated, indexName);
|
|
456
|
+
return results.map((row) => this._reconstructFromRead(row));
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Full-text search with count for paginated search results.
|
|
460
|
+
*
|
|
461
|
+
* @param text - Search text.
|
|
462
|
+
* @param query - Filter, sort, limit, etc.
|
|
463
|
+
* @param indexName - Optional search index to target.
|
|
464
|
+
*/ async searchWithCount(text, query, indexName) {
|
|
465
|
+
this._flatten();
|
|
466
|
+
const translated = this._translateQuery(query);
|
|
467
|
+
const result = await this.adapter.searchWithCount(text, translated, indexName);
|
|
468
|
+
return {
|
|
469
|
+
data: result.data.map((row) => this._reconstructFromRead(row)),
|
|
470
|
+
count: result.count
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Finds a single record by primary key or unique property.
|
|
475
|
+
*
|
|
476
|
+
* 1. Tries primary key lookup (single or composite).
|
|
477
|
+
* 2. Falls back to unique properties if PK validation fails.
|
|
478
|
+
*
|
|
479
|
+
* @param id - Primary key value (scalar for single PK, object for composite).
|
|
480
|
+
* @param controls - Optional query controls ($select, etc.).
|
|
481
|
+
*/ async findById(id, controls) {
|
|
482
|
+
this._flatten();
|
|
483
|
+
const pkFields = this.primaryKeys;
|
|
484
|
+
if (pkFields.length === 0) throw new Error("No primary key defined — cannot find by ID");
|
|
485
|
+
const filter = {};
|
|
486
|
+
let pkValid = true;
|
|
487
|
+
if (pkFields.length === 1) {
|
|
488
|
+
const field = pkFields[0];
|
|
489
|
+
const fieldType = this.flatMap.get(field);
|
|
490
|
+
try {
|
|
491
|
+
filter[field] = fieldType ? this.adapter.prepareId(id, fieldType) : id;
|
|
492
|
+
} catch {
|
|
493
|
+
pkValid = false;
|
|
494
|
+
}
|
|
495
|
+
} else if (typeof id !== "object" || id === null) pkValid = false;
|
|
496
|
+
else {
|
|
497
|
+
const idObj = id;
|
|
498
|
+
for (const field of pkFields) {
|
|
499
|
+
const fieldType = this.flatMap.get(field);
|
|
500
|
+
try {
|
|
501
|
+
filter[field] = fieldType ? this.adapter.prepareId(idObj[field], fieldType) : idObj[field];
|
|
502
|
+
} catch {
|
|
503
|
+
pkValid = false;
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
if (pkValid) return await this.findOne({
|
|
509
|
+
filter,
|
|
510
|
+
controls: controls || {}
|
|
511
|
+
});
|
|
512
|
+
if (this.uniqueProps.size > 0) {
|
|
513
|
+
const orFilters = [];
|
|
514
|
+
for (const prop of this.uniqueProps) {
|
|
515
|
+
const fieldType = this.flatMap.get(prop);
|
|
516
|
+
try {
|
|
517
|
+
const prepared = fieldType ? this.adapter.prepareId(id, fieldType) : id;
|
|
518
|
+
orFilters.push({ [prop]: prepared });
|
|
519
|
+
} catch {}
|
|
520
|
+
}
|
|
521
|
+
return await this.findOne({
|
|
522
|
+
filter: { $or: orFilters },
|
|
523
|
+
controls: controls || {}
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
348
528
|
async updateMany(filter, data) {
|
|
349
529
|
this._flatten();
|
|
350
530
|
return this.adapter.updateMany(this._translateFilter(filter), this._prepareForWrite({ ...data }));
|
|
@@ -381,9 +561,12 @@ else storage = "column";
|
|
|
381
561
|
this.adapter.onFieldScanned?.(path, type, metadata);
|
|
382
562
|
}
|
|
383
563
|
});
|
|
384
|
-
if (!this.
|
|
564
|
+
if (!this._nestedObjects) this._classifyFields();
|
|
385
565
|
this._finalizeIndexes();
|
|
386
566
|
this.adapter.onAfterFlatten?.();
|
|
567
|
+
if (this._nestedObjects && this._flatMap) {
|
|
568
|
+
for (const path of this._flatMap.keys()) if (path && !this._ignoredFields.has(path)) this._allPhysicalFields.push(path);
|
|
569
|
+
} else for (const physical of this._pathToPhysical.values()) this._allPhysicalFields.push(physical);
|
|
387
570
|
}
|
|
388
571
|
/**
|
|
389
572
|
* Scans `@db.*` and `@meta.id` annotations on a field during flattening.
|
|
@@ -405,7 +588,7 @@ else if (defaultFn !== undefined) this._defaults.set(fieldName, {
|
|
|
405
588
|
for (const index of metadata.get("db.index.plain") || []) {
|
|
406
589
|
const name = index === true ? fieldName : index?.name || fieldName;
|
|
407
590
|
const sort = (index === true ? undefined : index?.sort) || "asc";
|
|
408
|
-
this._addIndexField("plain", name, fieldName, sort);
|
|
591
|
+
this._addIndexField("plain", name, fieldName, { sort });
|
|
409
592
|
}
|
|
410
593
|
for (const index of metadata.get("db.index.unique") || []) {
|
|
411
594
|
const name = index === true ? fieldName : typeof index === "string" ? index : index?.name || fieldName;
|
|
@@ -413,7 +596,8 @@ else if (defaultFn !== undefined) this._defaults.set(fieldName, {
|
|
|
413
596
|
}
|
|
414
597
|
for (const index of metadata.get("db.index.fulltext") || []) {
|
|
415
598
|
const name = index === true ? fieldName : typeof index === "string" ? index : index?.name || fieldName;
|
|
416
|
-
|
|
599
|
+
const weight = index !== true && typeof index === "object" ? index?.weight : undefined;
|
|
600
|
+
this._addIndexField("fulltext", name, fieldName, { weight });
|
|
417
601
|
}
|
|
418
602
|
if (metadata.has("db.json")) {
|
|
419
603
|
this._jsonFields.add(fieldName);
|
|
@@ -421,13 +605,14 @@ else if (defaultFn !== undefined) this._defaults.set(fieldName, {
|
|
|
421
605
|
if (hasIndex) this.logger.warn(`@db.index on a @db.json field "${fieldName}" — most databases cannot index into JSON columns`);
|
|
422
606
|
}
|
|
423
607
|
}
|
|
424
|
-
_addIndexField(type, name, field,
|
|
608
|
+
_addIndexField(type, name, field, opts) {
|
|
425
609
|
const key = indexKey(type, name);
|
|
426
610
|
const index = this._indexes.get(key);
|
|
427
611
|
const indexField = {
|
|
428
612
|
name: field,
|
|
429
|
-
sort
|
|
613
|
+
sort: opts?.sort ?? "asc"
|
|
430
614
|
};
|
|
615
|
+
if (opts?.weight !== undefined) indexField.weight = opts.weight;
|
|
431
616
|
if (index) index.fields.push(indexField);
|
|
432
617
|
else this._indexes.set(key, {
|
|
433
618
|
key,
|
|
@@ -458,11 +643,13 @@ else if (isObject && isJson) {} else if (isObject && !isJson) this._flattenedPar
|
|
|
458
643
|
for (const [path] of this._flatMap.entries()) {
|
|
459
644
|
if (!path) continue;
|
|
460
645
|
if (this._flattenedParents.has(path)) continue;
|
|
461
|
-
if (this.
|
|
462
|
-
const isFlattened = this.
|
|
646
|
+
if (this._findAncestorInSet(path, this._jsonFields) !== undefined) continue;
|
|
647
|
+
const isFlattened = this._findAncestorInSet(path, this._flattenedParents) !== undefined;
|
|
463
648
|
const physicalName = this._columnMap.get(path) ?? (isFlattened ? path.replace(/\./g, "__") : path);
|
|
464
649
|
this._pathToPhysical.set(path, physicalName);
|
|
465
650
|
this._physicalToPath.set(physicalName, path);
|
|
651
|
+
const fieldType = this._flatMap?.get(path);
|
|
652
|
+
if (fieldType && resolveDesignType(fieldType) === "boolean") this._booleanFields.add(physicalName);
|
|
466
653
|
}
|
|
467
654
|
for (const parentPath of this._flattenedParents) {
|
|
468
655
|
const prefix = `${parentPath}.`;
|
|
@@ -473,24 +660,13 @@ else if (isObject && isJson) {} else if (isObject && !isJson) this._flattenedPar
|
|
|
473
660
|
this._requiresMappings = this._flattenedParents.size > 0 || this._jsonFields.size > 0;
|
|
474
661
|
}
|
|
475
662
|
/**
|
|
476
|
-
* Finds the nearest ancestor
|
|
477
|
-
*
|
|
478
|
-
*/
|
|
663
|
+
* Finds the nearest ancestor of `path` that belongs to `set`.
|
|
664
|
+
* Used to locate flattened parents and @db.json ancestors.
|
|
665
|
+
*/ _findAncestorInSet(path, set) {
|
|
479
666
|
let pos = path.length;
|
|
480
667
|
while ((pos = path.lastIndexOf(".", pos - 1)) !== -1) {
|
|
481
668
|
const ancestor = path.slice(0, pos);
|
|
482
|
-
if (
|
|
483
|
-
}
|
|
484
|
-
return undefined;
|
|
485
|
-
}
|
|
486
|
-
/**
|
|
487
|
-
* Finds the nearest ancestor that is a @db.json field.
|
|
488
|
-
* Children of JSON fields don't get their own columns.
|
|
489
|
-
*/ _findJsonParent(path) {
|
|
490
|
-
let pos = path.length;
|
|
491
|
-
while ((pos = path.lastIndexOf(".", pos - 1)) !== -1) {
|
|
492
|
-
const ancestor = path.slice(0, pos);
|
|
493
|
-
if (this._jsonFields.has(ancestor)) return ancestor;
|
|
669
|
+
if (set.has(ancestor)) return ancestor;
|
|
494
670
|
}
|
|
495
671
|
return undefined;
|
|
496
672
|
}
|
|
@@ -503,7 +679,20 @@ else if (isObject && isJson) {} else if (isObject && !isJson) this._flattenedPar
|
|
|
503
679
|
* Called before validation so that defaults satisfy required field constraints.
|
|
504
680
|
*/ _applyDefaults(data) {
|
|
505
681
|
for (const [field, def] of this._defaults.entries()) if (data[field] === undefined) {
|
|
506
|
-
if (def.kind === "value")
|
|
682
|
+
if (def.kind === "value") {
|
|
683
|
+
const fieldType = this._flatMap?.get(field);
|
|
684
|
+
const designType = fieldType?.type.kind === "" && fieldType.type.designType;
|
|
685
|
+
data[field] = designType === "string" ? def.value : JSON.parse(def.value);
|
|
686
|
+
} else if (def.kind === "fn") switch (def.fn) {
|
|
687
|
+
case "now": {
|
|
688
|
+
data[field] = Date.now();
|
|
689
|
+
break;
|
|
690
|
+
}
|
|
691
|
+
case "uuid": {
|
|
692
|
+
data[field] = crypto.randomUUID();
|
|
693
|
+
break;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
507
696
|
}
|
|
508
697
|
return data;
|
|
509
698
|
}
|
|
@@ -518,7 +707,7 @@ else if (isObject && isJson) {} else if (isObject && !isJson) this._flattenedPar
|
|
|
518
707
|
if (fieldType) data[pk] = this.adapter.prepareId(data[pk], fieldType);
|
|
519
708
|
}
|
|
520
709
|
for (const field of this._ignoredFields) if (!field.includes(".")) delete data[field];
|
|
521
|
-
if (!this._requiresMappings || this.
|
|
710
|
+
if (!this._requiresMappings || this._nestedObjects) {
|
|
522
711
|
for (const [logical, physical] of this._columnMap.entries()) if (logical in data) {
|
|
523
712
|
data[physical] = data[logical];
|
|
524
713
|
delete data[logical];
|
|
@@ -533,40 +722,26 @@ else if (isObject && isJson) {} else if (isObject && !isJson) this._flattenedPar
|
|
|
533
722
|
* Uses _pathToPhysical for final key names.
|
|
534
723
|
*/ _flattenPayload(data) {
|
|
535
724
|
const result = {};
|
|
536
|
-
const
|
|
537
|
-
for (const key of dataKeys) {
|
|
538
|
-
const value = data[key];
|
|
539
|
-
if (this._flattenedParents.has(key)) {
|
|
540
|
-
if (value === null || value === undefined) this._setFlattenedChildrenNull(key, result);
|
|
541
|
-
else if (typeof value === "object" && !Array.isArray(value)) this._flattenObject(key, value, result);
|
|
542
|
-
} else if (this._jsonFields.has(key)) {
|
|
543
|
-
const physical = this._pathToPhysical.get(key) ?? key;
|
|
544
|
-
result[physical] = value !== undefined && value !== null ? JSON.stringify(value) : value;
|
|
545
|
-
} else {
|
|
546
|
-
const physical = this._pathToPhysical.get(key) ?? key;
|
|
547
|
-
result[physical] = value;
|
|
548
|
-
}
|
|
549
|
-
}
|
|
725
|
+
for (const key of Object.keys(data)) this._writeFlattenedField(key, data[key], result);
|
|
550
726
|
return result;
|
|
551
727
|
}
|
|
552
728
|
/**
|
|
553
|
-
*
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
else if (typeof value === "object" && !Array.isArray(value)) this._flattenObject(dotPath, value, result);
|
|
563
|
-
} else if (this._jsonFields.has(dotPath)) {
|
|
564
|
-
const physical = this._pathToPhysical.get(dotPath) ?? dotPath.replace(/\./g, "__");
|
|
565
|
-
result[physical] = value !== undefined && value !== null ? JSON.stringify(value) : value;
|
|
566
|
-
} else {
|
|
567
|
-
const physical = this._pathToPhysical.get(dotPath) ?? dotPath.replace(/\./g, "__");
|
|
568
|
-
result[physical] = value;
|
|
729
|
+
* Classifies and writes a single field to the result object.
|
|
730
|
+
* Recurses into nested objects that should be flattened.
|
|
731
|
+
*/ _writeFlattenedField(path, value, result) {
|
|
732
|
+
if (this._ignoredFields.has(path)) return;
|
|
733
|
+
if (this._flattenedParents.has(path)) {
|
|
734
|
+
if (value === null || value === undefined) this._setFlattenedChildrenNull(path, result);
|
|
735
|
+
else if (typeof value === "object" && !Array.isArray(value)) {
|
|
736
|
+
const obj = value;
|
|
737
|
+
for (const key of Object.keys(obj)) this._writeFlattenedField(`${path}.${key}`, obj[key], result);
|
|
569
738
|
}
|
|
739
|
+
} else if (this._jsonFields.has(path)) {
|
|
740
|
+
const physical = this._pathToPhysical.get(path) ?? path.replace(/\./g, "__");
|
|
741
|
+
result[physical] = value !== undefined && value !== null ? JSON.stringify(value) : value;
|
|
742
|
+
} else {
|
|
743
|
+
const physical = this._pathToPhysical.get(path) ?? path.replace(/\./g, "__");
|
|
744
|
+
result[physical] = value;
|
|
570
745
|
}
|
|
571
746
|
}
|
|
572
747
|
/**
|
|
@@ -579,11 +754,11 @@ else if (typeof value === "object" && !Array.isArray(value)) this._flattenObject
|
|
|
579
754
|
* Reconstructs nested objects from flat __-separated column values.
|
|
580
755
|
* JSON fields are parsed from strings back to objects/arrays.
|
|
581
756
|
*/ _reconstructFromRead(row) {
|
|
582
|
-
if (!this._requiresMappings || this.
|
|
757
|
+
if (!this._requiresMappings || this._nestedObjects) return this._coerceBooleans(row);
|
|
583
758
|
const result = {};
|
|
584
759
|
const rowKeys = Object.keys(row);
|
|
585
760
|
for (const physical of rowKeys) {
|
|
586
|
-
const value = row[physical];
|
|
761
|
+
const value = this._booleanFields.has(physical) ? toBool(row[physical]) : row[physical];
|
|
587
762
|
const logicalPath = this._physicalToPath.get(physical);
|
|
588
763
|
if (!logicalPath) {
|
|
589
764
|
result[physical] = value;
|
|
@@ -599,6 +774,14 @@ else result[logicalPath] = value;
|
|
|
599
774
|
return result;
|
|
600
775
|
}
|
|
601
776
|
/**
|
|
777
|
+
* Coerces boolean fields from storage representation (0/1) to JS booleans.
|
|
778
|
+
* Used on the fast-path when no column mapping is needed.
|
|
779
|
+
*/ _coerceBooleans(row) {
|
|
780
|
+
if (this._booleanFields.size === 0) return row;
|
|
781
|
+
for (const field of this._booleanFields) if (field in row) row[field] = toBool(row[field]);
|
|
782
|
+
return row;
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
602
785
|
* Sets a value at a dot-notation path, creating intermediate objects as needed.
|
|
603
786
|
*/ _setNestedValue(obj, dotPath, value) {
|
|
604
787
|
const parts = dotPath.split(".");
|
|
@@ -639,12 +822,21 @@ else result[logicalPath] = value;
|
|
|
639
822
|
/**
|
|
640
823
|
* Translates a Uniquery's filter, sort, and projection from logical
|
|
641
824
|
* dot-notation paths to physical column names.
|
|
825
|
+
* Always wraps `$select` in {@link UniquSelect}.
|
|
642
826
|
*/ _translateQuery(query) {
|
|
643
|
-
if (!this._requiresMappings || this.
|
|
827
|
+
if (!this._requiresMappings || this._nestedObjects) {
|
|
828
|
+
const controls = query.controls;
|
|
829
|
+
return {
|
|
830
|
+
filter: query.filter,
|
|
831
|
+
controls: {
|
|
832
|
+
...controls,
|
|
833
|
+
$select: controls?.$select ? new UniquSelect(controls.$select, this._allPhysicalFields) : undefined
|
|
834
|
+
}
|
|
835
|
+
};
|
|
836
|
+
}
|
|
644
837
|
return {
|
|
645
838
|
filter: this._translateFilter(query.filter),
|
|
646
|
-
controls: query.controls ? this._translateControls(query.controls) :
|
|
647
|
-
insights: query.insights
|
|
839
|
+
controls: query.controls ? this._translateControls(query.controls) : {}
|
|
648
840
|
};
|
|
649
841
|
}
|
|
650
842
|
/**
|
|
@@ -668,9 +860,13 @@ else {
|
|
|
668
860
|
}
|
|
669
861
|
/**
|
|
670
862
|
* Translates field names in sort and projection controls.
|
|
863
|
+
* Wraps `$select` in {@link UniquSelect} after path translation.
|
|
671
864
|
*/ _translateControls(controls) {
|
|
672
|
-
if (!controls) return
|
|
673
|
-
const result = {
|
|
865
|
+
if (!controls) return {};
|
|
866
|
+
const result = {
|
|
867
|
+
...controls,
|
|
868
|
+
$select: undefined
|
|
869
|
+
};
|
|
674
870
|
if (controls.$sort) {
|
|
675
871
|
const translated = {};
|
|
676
872
|
const sortObj = controls.$sort;
|
|
@@ -682,35 +878,39 @@ else {
|
|
|
682
878
|
}
|
|
683
879
|
result.$sort = translated;
|
|
684
880
|
}
|
|
685
|
-
if (controls.$select)
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
const
|
|
689
|
-
|
|
881
|
+
if (controls.$select) {
|
|
882
|
+
let translatedRaw;
|
|
883
|
+
if (Array.isArray(controls.$select)) {
|
|
884
|
+
const expanded = [];
|
|
885
|
+
for (const key of controls.$select) {
|
|
886
|
+
const expansion = this._selectExpansion.get(key);
|
|
887
|
+
if (expansion) expanded.push(...expansion);
|
|
690
888
|
else expanded.push(this._pathToPhysical.get(key) ?? key);
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
889
|
+
}
|
|
890
|
+
translatedRaw = expanded;
|
|
891
|
+
} else {
|
|
892
|
+
const translated = {};
|
|
893
|
+
const selectObj = controls.$select;
|
|
894
|
+
const selectKeys = Object.keys(selectObj);
|
|
895
|
+
for (const key of selectKeys) {
|
|
896
|
+
const val = selectObj[key];
|
|
897
|
+
const expansion = this._selectExpansion.get(key);
|
|
898
|
+
if (expansion) for (const leaf of expansion) translated[leaf] = val;
|
|
701
899
|
else {
|
|
702
|
-
|
|
703
|
-
|
|
900
|
+
const physical = this._pathToPhysical.get(key) ?? key;
|
|
901
|
+
translated[physical] = val;
|
|
902
|
+
}
|
|
704
903
|
}
|
|
904
|
+
translatedRaw = translated;
|
|
705
905
|
}
|
|
706
|
-
result.$select =
|
|
906
|
+
result.$select = new UniquSelect(translatedRaw, this._allPhysicalFields);
|
|
707
907
|
}
|
|
708
908
|
return result;
|
|
709
909
|
}
|
|
710
910
|
/**
|
|
711
911
|
* Translates dot-notation keys in a decomposed patch to physical column names.
|
|
712
912
|
*/ _translatePatchKeys(update) {
|
|
713
|
-
if (!this._requiresMappings || this.
|
|
913
|
+
if (!this._requiresMappings || this._nestedObjects) return update;
|
|
714
914
|
const result = {};
|
|
715
915
|
const updateKeys = Object.keys(update);
|
|
716
916
|
for (const key of updateKeys) {
|
|
@@ -743,21 +943,26 @@ else result[finalKey] = value;
|
|
|
743
943
|
*/ _buildValidator(purpose) {
|
|
744
944
|
const plugins = this.adapter.getValidatorPlugins();
|
|
745
945
|
switch (purpose) {
|
|
746
|
-
case "insert":
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
946
|
+
case "insert": {
|
|
947
|
+
if (this.adapter.buildInsertValidator) return this.adapter.buildInsertValidator(this);
|
|
948
|
+
return this.createValidator({
|
|
949
|
+
plugins,
|
|
950
|
+
replace: (type, path) => {
|
|
951
|
+
if (this._primaryKeys.includes(path) || this._defaults.has(path)) return {
|
|
952
|
+
...type,
|
|
953
|
+
optional: true
|
|
954
|
+
};
|
|
955
|
+
return type;
|
|
956
|
+
}
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
case "patch": {
|
|
960
|
+
if (this.adapter.buildPatchValidator) return this.adapter.buildPatchValidator(this);
|
|
961
|
+
return this.createValidator({
|
|
962
|
+
plugins,
|
|
963
|
+
partial: true
|
|
964
|
+
});
|
|
965
|
+
}
|
|
761
966
|
default: return this.createValidator({ plugins });
|
|
762
967
|
}
|
|
763
968
|
}
|
|
@@ -780,7 +985,10 @@ else result[finalKey] = value;
|
|
|
780
985
|
/** Object paths being flattened into __-separated columns (no column themselves). */ _define_property$1(this, "_flattenedParents", void 0);
|
|
781
986
|
/** Fields stored as JSON (@db.json + array fields). */ _define_property$1(this, "_jsonFields", void 0);
|
|
782
987
|
/** Intermediate paths → their leaf physical column names (for $select expansion in relational DBs). */ _define_property$1(this, "_selectExpansion", void 0);
|
|
988
|
+
/** Physical column names of boolean fields (for storage coercion on read). */ _define_property$1(this, "_booleanFields", void 0);
|
|
783
989
|
/** Fast-path flag: skip all mapping when no nested/json fields exist. */ _define_property$1(this, "_requiresMappings", void 0);
|
|
990
|
+
/** All non-ignored physical field names (for UniquSelect exclusion inversion). */ _define_property$1(this, "_allPhysicalFields", void 0);
|
|
991
|
+
/** Cached result of adapter.supportsNestedObjects(). */ _define_property$1(this, "_nestedObjects", void 0);
|
|
784
992
|
_define_property$1(this, "validators", void 0);
|
|
785
993
|
this._type = _type;
|
|
786
994
|
this.adapter = adapter;
|
|
@@ -796,7 +1004,9 @@ else result[finalKey] = value;
|
|
|
796
1004
|
this._flattenedParents = new Set();
|
|
797
1005
|
this._jsonFields = new Set();
|
|
798
1006
|
this._selectExpansion = new Map();
|
|
1007
|
+
this._booleanFields = new Set();
|
|
799
1008
|
this._requiresMappings = false;
|
|
1009
|
+
this._allPhysicalFields = [];
|
|
800
1010
|
this.validators = new Map();
|
|
801
1011
|
if (!(0, __atscript_typescript_utils.isAnnotatedType)(_type)) throw new Error("Atscript Annotated Type expected");
|
|
802
1012
|
if (_type.type.kind !== "object") throw new Error("Database table type must be an object type");
|
|
@@ -806,6 +1016,7 @@ else result[finalKey] = value;
|
|
|
806
1016
|
this.tableName = adapterName || dbTable || fallbackName;
|
|
807
1017
|
if (!this.tableName) throw new Error("@db.table annotation or adapter-specific table name expected");
|
|
808
1018
|
this.schema = _type.metadata.get("db.schema");
|
|
1019
|
+
this._nestedObjects = adapter.supportsNestedObjects();
|
|
809
1020
|
adapter.registerTable(this);
|
|
810
1021
|
}
|
|
811
1022
|
};
|
|
@@ -911,6 +1122,46 @@ var BaseDbAdapter = class {
|
|
|
911
1122
|
}
|
|
912
1123
|
for (const name of existingNames) if (!desiredNames.has(name)) await opts.dropIndex(name);
|
|
913
1124
|
}
|
|
1125
|
+
/**
|
|
1126
|
+
* Returns available search indexes for this adapter.
|
|
1127
|
+
* UI uses this to show index picker. Override in adapters that support search.
|
|
1128
|
+
*/ getSearchIndexes() {
|
|
1129
|
+
return [];
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* Whether this adapter supports text search.
|
|
1133
|
+
* Default: `true` when {@link getSearchIndexes} returns any entries.
|
|
1134
|
+
*/ isSearchable() {
|
|
1135
|
+
return this.getSearchIndexes().length > 0;
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Full-text search. Override in adapters that support search.
|
|
1139
|
+
*
|
|
1140
|
+
* @param text - Search text.
|
|
1141
|
+
* @param query - Filter, sort, limit, etc.
|
|
1142
|
+
* @param indexName - Optional search index to target.
|
|
1143
|
+
*/ async search(text, query, indexName) {
|
|
1144
|
+
throw new Error("Search not supported by this adapter");
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
1147
|
+
* Full-text search with count (for paginated search results).
|
|
1148
|
+
*
|
|
1149
|
+
* @param text - Search text.
|
|
1150
|
+
* @param query - Filter, sort, limit, etc.
|
|
1151
|
+
* @param indexName - Optional search index to target.
|
|
1152
|
+
*/ async searchWithCount(text, query, indexName) {
|
|
1153
|
+
throw new Error("Search not supported by this adapter");
|
|
1154
|
+
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Fetches records and total count in one call.
|
|
1157
|
+
* Default: two parallel calls. Adapters may override for single-query optimization.
|
|
1158
|
+
*/ async findManyWithCount(query) {
|
|
1159
|
+
const [data, count] = await Promise.all([this.findMany(query), this.count(query)]);
|
|
1160
|
+
return {
|
|
1161
|
+
data,
|
|
1162
|
+
count
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
914
1165
|
constructor() {
|
|
915
1166
|
_define_property(this, "_table", void 0);
|
|
916
1167
|
}
|
|
@@ -920,6 +1171,7 @@ var BaseDbAdapter = class {
|
|
|
920
1171
|
exports.AtscriptDbTable = AtscriptDbTable
|
|
921
1172
|
exports.BaseDbAdapter = BaseDbAdapter
|
|
922
1173
|
exports.NoopLogger = NoopLogger
|
|
1174
|
+
exports.UniquSelect = UniquSelect
|
|
923
1175
|
exports.decomposePatch = decomposePatch
|
|
924
1176
|
exports.getKeyProps = getKeyProps
|
|
925
1177
|
Object.defineProperty(exports, 'isPrimitive', {
|