@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.mjs
CHANGED
|
@@ -77,6 +77,78 @@ else update[key] = value;
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
//#endregion
|
|
81
|
+
//#region packages/utils-db/src/uniqu-select.ts
|
|
82
|
+
function _define_property$2(obj, key, value) {
|
|
83
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
84
|
+
value,
|
|
85
|
+
enumerable: true,
|
|
86
|
+
configurable: true,
|
|
87
|
+
writable: true
|
|
88
|
+
});
|
|
89
|
+
else obj[key] = value;
|
|
90
|
+
return obj;
|
|
91
|
+
}
|
|
92
|
+
var UniquSelect = class {
|
|
93
|
+
/**
|
|
94
|
+
* Resolved inclusion array of field names.
|
|
95
|
+
* For exclusion form, inverts using `allFields` from constructor.
|
|
96
|
+
*/ get asArray() {
|
|
97
|
+
if (this._arrayResolved) return this._array;
|
|
98
|
+
this._arrayResolved = true;
|
|
99
|
+
if (Array.isArray(this._raw)) {
|
|
100
|
+
this._array = this._raw;
|
|
101
|
+
return this._array;
|
|
102
|
+
}
|
|
103
|
+
const raw = this._raw;
|
|
104
|
+
const entries = Object.entries(raw);
|
|
105
|
+
if (entries.length === 0) return undefined;
|
|
106
|
+
if (entries[0][1] === 1) {
|
|
107
|
+
const result = [];
|
|
108
|
+
for (const entry of entries) if (entry[1] === 1) result.push(entry[0]);
|
|
109
|
+
this._array = result;
|
|
110
|
+
} else {
|
|
111
|
+
if (!this._allFields) return undefined;
|
|
112
|
+
const excluded = new Set();
|
|
113
|
+
for (const entry of entries) if (entry[1] === 0) excluded.add(entry[0]);
|
|
114
|
+
const result = [];
|
|
115
|
+
for (const field of this._allFields) if (!excluded.has(field)) result.push(field);
|
|
116
|
+
this._array = result;
|
|
117
|
+
}
|
|
118
|
+
return this._array;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Record projection preserving original semantics.
|
|
122
|
+
* Returns original object as-is if raw was object.
|
|
123
|
+
* Converts `string[]` to `{field: 1}` inclusion object.
|
|
124
|
+
*/ get asProjection() {
|
|
125
|
+
if (this._projectionResolved) return this._projection;
|
|
126
|
+
this._projectionResolved = true;
|
|
127
|
+
if (!Array.isArray(this._raw)) {
|
|
128
|
+
const raw = this._raw;
|
|
129
|
+
if (Object.keys(raw).length === 0) return undefined;
|
|
130
|
+
this._projection = raw;
|
|
131
|
+
return this._projection;
|
|
132
|
+
}
|
|
133
|
+
const arr = this._raw;
|
|
134
|
+
if (arr.length === 0) return undefined;
|
|
135
|
+
const result = {};
|
|
136
|
+
for (const item of arr) result[item] = 1;
|
|
137
|
+
this._projection = result;
|
|
138
|
+
return this._projection;
|
|
139
|
+
}
|
|
140
|
+
constructor(raw, allFields) {
|
|
141
|
+
_define_property$2(this, "_raw", void 0);
|
|
142
|
+
_define_property$2(this, "_allFields", void 0);
|
|
143
|
+
_define_property$2(this, "_arrayResolved", false);
|
|
144
|
+
_define_property$2(this, "_array", void 0);
|
|
145
|
+
_define_property$2(this, "_projectionResolved", false);
|
|
146
|
+
_define_property$2(this, "_projection", void 0);
|
|
147
|
+
this._raw = raw;
|
|
148
|
+
this._allFields = allFields;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
80
152
|
//#endregion
|
|
81
153
|
//#region packages/utils-db/src/db-table.ts
|
|
82
154
|
function _define_property$1(obj, key, value) {
|
|
@@ -96,11 +168,18 @@ function resolveDesignType(fieldType) {
|
|
|
96
168
|
if (fieldType.type.kind === "array") return "array";
|
|
97
169
|
return "string";
|
|
98
170
|
}
|
|
171
|
+
/** Coerces a storage value (0/1/null) back to a JS boolean. */ function toBool(value) {
|
|
172
|
+
if (value === null || value === undefined) return value;
|
|
173
|
+
return !!value;
|
|
174
|
+
}
|
|
99
175
|
function indexKey(type, name) {
|
|
100
176
|
const cleanName = name.replace(/[^a-z0-9_.-]/gi, "_").replace(/_+/g, "_").slice(0, 127 - INDEX_PREFIX.length - type.length - 2);
|
|
101
177
|
return `${INDEX_PREFIX}${type}__${cleanName}`;
|
|
102
178
|
}
|
|
103
179
|
var AtscriptDbTable = class {
|
|
180
|
+
/** Returns the underlying adapter with its concrete type preserved. */ getAdapter() {
|
|
181
|
+
return this.adapter;
|
|
182
|
+
}
|
|
104
183
|
/** The raw annotated type. */ get type() {
|
|
105
184
|
return this._type;
|
|
106
185
|
}
|
|
@@ -116,6 +195,29 @@ var AtscriptDbTable = class {
|
|
|
116
195
|
this._flatten();
|
|
117
196
|
return this._primaryKeys;
|
|
118
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Registers an additional primary key field.
|
|
200
|
+
* Useful for adapters (e.g., MongoDB) where `_id` is always the primary key
|
|
201
|
+
* even without an explicit `@meta.id` annotation.
|
|
202
|
+
*
|
|
203
|
+
* Typically called from {@link BaseDbAdapter.onFieldScanned}.
|
|
204
|
+
*/ addPrimaryKey(field) {
|
|
205
|
+
if (!this._primaryKeys.includes(field)) this._primaryKeys.push(field);
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Removes a field from the primary key list.
|
|
209
|
+
* Useful for adapters (e.g., MongoDB) where `@meta.id` fields should be
|
|
210
|
+
* unique indexes rather than part of the primary key.
|
|
211
|
+
*/ removePrimaryKey(field) {
|
|
212
|
+
const idx = this._primaryKeys.indexOf(field);
|
|
213
|
+
if (idx >= 0) this._primaryKeys.splice(idx, 1);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Registers a field as having a unique constraint.
|
|
217
|
+
* Used by adapters to ensure `findById` falls back to this field.
|
|
218
|
+
*/ addUniqueField(field) {
|
|
219
|
+
this._uniqueProps.add(field);
|
|
220
|
+
}
|
|
119
221
|
/** Logical → physical column name mapping from `@db.column`. */ get columnMap() {
|
|
120
222
|
this._flatten();
|
|
121
223
|
return this._columnMap;
|
|
@@ -155,13 +257,13 @@ var AtscriptDbTable = class {
|
|
|
155
257
|
this._flatten();
|
|
156
258
|
if (!this._fieldDescriptors) {
|
|
157
259
|
this._fieldDescriptors = [];
|
|
158
|
-
const skipFlattening = this.
|
|
260
|
+
const skipFlattening = this._nestedObjects;
|
|
159
261
|
for (const [path, type] of this._flatMap.entries()) {
|
|
160
262
|
if (!path) continue;
|
|
161
263
|
if (!skipFlattening && this._flattenedParents.has(path)) continue;
|
|
162
|
-
if (!skipFlattening && this.
|
|
264
|
+
if (!skipFlattening && this._findAncestorInSet(path, this._jsonFields) !== undefined) continue;
|
|
163
265
|
const isJson = this._jsonFields.has(path);
|
|
164
|
-
const isFlattened = !skipFlattening && this.
|
|
266
|
+
const isFlattened = !skipFlattening && this._findAncestorInSet(path, this._flattenedParents) !== undefined;
|
|
165
267
|
const designType = isJson ? "json" : resolveDesignType(type);
|
|
166
268
|
let storage;
|
|
167
269
|
if (skipFlattening) storage = "column";
|
|
@@ -182,33 +284,11 @@ else storage = "column";
|
|
|
182
284
|
flattenedFrom: isFlattened ? path : undefined
|
|
183
285
|
});
|
|
184
286
|
}
|
|
287
|
+
Object.freeze(this._fieldDescriptors);
|
|
185
288
|
}
|
|
186
289
|
return this._fieldDescriptors;
|
|
187
290
|
}
|
|
188
291
|
/**
|
|
189
|
-
* Resolves `$select` from {@link UniqueryControls} to a list of field names.
|
|
190
|
-
* - `undefined` → `undefined` (all fields)
|
|
191
|
-
* - `string[]` → pass through
|
|
192
|
-
* - `Record<K, 1>` → extract included keys
|
|
193
|
-
* - `Record<K, 0>` → invert using known field names
|
|
194
|
-
*/ resolveProjection(select) {
|
|
195
|
-
if (!select) return undefined;
|
|
196
|
-
if (Array.isArray(select)) return select.length > 0 ? select : undefined;
|
|
197
|
-
const selectObj = select;
|
|
198
|
-
const keys = Object.keys(selectObj);
|
|
199
|
-
if (keys.length === 0) return undefined;
|
|
200
|
-
if (selectObj[keys[0]] === 1) {
|
|
201
|
-
const result$1 = [];
|
|
202
|
-
for (const k of keys) if (selectObj[k] === 1) result$1.push(k);
|
|
203
|
-
return result$1;
|
|
204
|
-
}
|
|
205
|
-
const excluded = new Set();
|
|
206
|
-
for (const k of keys) if (selectObj[k] === 0) excluded.add(k);
|
|
207
|
-
const result = [];
|
|
208
|
-
for (const f of this.fieldDescriptors) if (!f.ignored && !excluded.has(f.path)) result.push(f.path);
|
|
209
|
-
return result;
|
|
210
|
-
}
|
|
211
|
-
/**
|
|
212
292
|
* Creates a new validator with custom options.
|
|
213
293
|
* Adapter plugins are NOT automatically included — use {@link getValidator}
|
|
214
294
|
* for the standard validator with adapter plugins.
|
|
@@ -321,6 +401,106 @@ else storage = "column";
|
|
|
321
401
|
});
|
|
322
402
|
return this.adapter.count(this._translateQuery(query));
|
|
323
403
|
}
|
|
404
|
+
/**
|
|
405
|
+
* Finds records and total count in a single logical call.
|
|
406
|
+
* Adapters may optimize into a single query (e.g., MongoDB `$facet`).
|
|
407
|
+
*/ async findManyWithCount(query) {
|
|
408
|
+
this._flatten();
|
|
409
|
+
const translated = this._translateQuery(query);
|
|
410
|
+
const result = await this.adapter.findManyWithCount(translated);
|
|
411
|
+
return {
|
|
412
|
+
data: result.data.map((row) => this._reconstructFromRead(row)),
|
|
413
|
+
count: result.count
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
/** Whether the underlying adapter supports text search. */ isSearchable() {
|
|
417
|
+
return this.adapter.isSearchable();
|
|
418
|
+
}
|
|
419
|
+
/** Returns available search indexes from the adapter. */ getSearchIndexes() {
|
|
420
|
+
return this.adapter.getSearchIndexes();
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Full-text search with query translation and result reconstruction.
|
|
424
|
+
*
|
|
425
|
+
* @param text - Search text.
|
|
426
|
+
* @param query - Filter, sort, limit, etc.
|
|
427
|
+
* @param indexName - Optional search index to target.
|
|
428
|
+
*/ async search(text, query, indexName) {
|
|
429
|
+
this._flatten();
|
|
430
|
+
const translated = this._translateQuery(query);
|
|
431
|
+
const results = await this.adapter.search(text, translated, indexName);
|
|
432
|
+
return results.map((row) => this._reconstructFromRead(row));
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Full-text search with count for paginated search results.
|
|
436
|
+
*
|
|
437
|
+
* @param text - Search text.
|
|
438
|
+
* @param query - Filter, sort, limit, etc.
|
|
439
|
+
* @param indexName - Optional search index to target.
|
|
440
|
+
*/ async searchWithCount(text, query, indexName) {
|
|
441
|
+
this._flatten();
|
|
442
|
+
const translated = this._translateQuery(query);
|
|
443
|
+
const result = await this.adapter.searchWithCount(text, translated, indexName);
|
|
444
|
+
return {
|
|
445
|
+
data: result.data.map((row) => this._reconstructFromRead(row)),
|
|
446
|
+
count: result.count
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Finds a single record by primary key or unique property.
|
|
451
|
+
*
|
|
452
|
+
* 1. Tries primary key lookup (single or composite).
|
|
453
|
+
* 2. Falls back to unique properties if PK validation fails.
|
|
454
|
+
*
|
|
455
|
+
* @param id - Primary key value (scalar for single PK, object for composite).
|
|
456
|
+
* @param controls - Optional query controls ($select, etc.).
|
|
457
|
+
*/ async findById(id, controls) {
|
|
458
|
+
this._flatten();
|
|
459
|
+
const pkFields = this.primaryKeys;
|
|
460
|
+
if (pkFields.length === 0) throw new Error("No primary key defined — cannot find by ID");
|
|
461
|
+
const filter = {};
|
|
462
|
+
let pkValid = true;
|
|
463
|
+
if (pkFields.length === 1) {
|
|
464
|
+
const field = pkFields[0];
|
|
465
|
+
const fieldType = this.flatMap.get(field);
|
|
466
|
+
try {
|
|
467
|
+
filter[field] = fieldType ? this.adapter.prepareId(id, fieldType) : id;
|
|
468
|
+
} catch {
|
|
469
|
+
pkValid = false;
|
|
470
|
+
}
|
|
471
|
+
} else if (typeof id !== "object" || id === null) pkValid = false;
|
|
472
|
+
else {
|
|
473
|
+
const idObj = id;
|
|
474
|
+
for (const field of pkFields) {
|
|
475
|
+
const fieldType = this.flatMap.get(field);
|
|
476
|
+
try {
|
|
477
|
+
filter[field] = fieldType ? this.adapter.prepareId(idObj[field], fieldType) : idObj[field];
|
|
478
|
+
} catch {
|
|
479
|
+
pkValid = false;
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
if (pkValid) return await this.findOne({
|
|
485
|
+
filter,
|
|
486
|
+
controls: controls || {}
|
|
487
|
+
});
|
|
488
|
+
if (this.uniqueProps.size > 0) {
|
|
489
|
+
const orFilters = [];
|
|
490
|
+
for (const prop of this.uniqueProps) {
|
|
491
|
+
const fieldType = this.flatMap.get(prop);
|
|
492
|
+
try {
|
|
493
|
+
const prepared = fieldType ? this.adapter.prepareId(id, fieldType) : id;
|
|
494
|
+
orFilters.push({ [prop]: prepared });
|
|
495
|
+
} catch {}
|
|
496
|
+
}
|
|
497
|
+
return await this.findOne({
|
|
498
|
+
filter: { $or: orFilters },
|
|
499
|
+
controls: controls || {}
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
324
504
|
async updateMany(filter, data) {
|
|
325
505
|
this._flatten();
|
|
326
506
|
return this.adapter.updateMany(this._translateFilter(filter), this._prepareForWrite({ ...data }));
|
|
@@ -357,9 +537,12 @@ else storage = "column";
|
|
|
357
537
|
this.adapter.onFieldScanned?.(path, type, metadata);
|
|
358
538
|
}
|
|
359
539
|
});
|
|
360
|
-
if (!this.
|
|
540
|
+
if (!this._nestedObjects) this._classifyFields();
|
|
361
541
|
this._finalizeIndexes();
|
|
362
542
|
this.adapter.onAfterFlatten?.();
|
|
543
|
+
if (this._nestedObjects && this._flatMap) {
|
|
544
|
+
for (const path of this._flatMap.keys()) if (path && !this._ignoredFields.has(path)) this._allPhysicalFields.push(path);
|
|
545
|
+
} else for (const physical of this._pathToPhysical.values()) this._allPhysicalFields.push(physical);
|
|
363
546
|
}
|
|
364
547
|
/**
|
|
365
548
|
* Scans `@db.*` and `@meta.id` annotations on a field during flattening.
|
|
@@ -381,7 +564,7 @@ else if (defaultFn !== undefined) this._defaults.set(fieldName, {
|
|
|
381
564
|
for (const index of metadata.get("db.index.plain") || []) {
|
|
382
565
|
const name = index === true ? fieldName : index?.name || fieldName;
|
|
383
566
|
const sort = (index === true ? undefined : index?.sort) || "asc";
|
|
384
|
-
this._addIndexField("plain", name, fieldName, sort);
|
|
567
|
+
this._addIndexField("plain", name, fieldName, { sort });
|
|
385
568
|
}
|
|
386
569
|
for (const index of metadata.get("db.index.unique") || []) {
|
|
387
570
|
const name = index === true ? fieldName : typeof index === "string" ? index : index?.name || fieldName;
|
|
@@ -389,7 +572,8 @@ else if (defaultFn !== undefined) this._defaults.set(fieldName, {
|
|
|
389
572
|
}
|
|
390
573
|
for (const index of metadata.get("db.index.fulltext") || []) {
|
|
391
574
|
const name = index === true ? fieldName : typeof index === "string" ? index : index?.name || fieldName;
|
|
392
|
-
|
|
575
|
+
const weight = index !== true && typeof index === "object" ? index?.weight : undefined;
|
|
576
|
+
this._addIndexField("fulltext", name, fieldName, { weight });
|
|
393
577
|
}
|
|
394
578
|
if (metadata.has("db.json")) {
|
|
395
579
|
this._jsonFields.add(fieldName);
|
|
@@ -397,13 +581,14 @@ else if (defaultFn !== undefined) this._defaults.set(fieldName, {
|
|
|
397
581
|
if (hasIndex) this.logger.warn(`@db.index on a @db.json field "${fieldName}" — most databases cannot index into JSON columns`);
|
|
398
582
|
}
|
|
399
583
|
}
|
|
400
|
-
_addIndexField(type, name, field,
|
|
584
|
+
_addIndexField(type, name, field, opts) {
|
|
401
585
|
const key = indexKey(type, name);
|
|
402
586
|
const index = this._indexes.get(key);
|
|
403
587
|
const indexField = {
|
|
404
588
|
name: field,
|
|
405
|
-
sort
|
|
589
|
+
sort: opts?.sort ?? "asc"
|
|
406
590
|
};
|
|
591
|
+
if (opts?.weight !== undefined) indexField.weight = opts.weight;
|
|
407
592
|
if (index) index.fields.push(indexField);
|
|
408
593
|
else this._indexes.set(key, {
|
|
409
594
|
key,
|
|
@@ -434,11 +619,13 @@ else if (isObject && isJson) {} else if (isObject && !isJson) this._flattenedPar
|
|
|
434
619
|
for (const [path] of this._flatMap.entries()) {
|
|
435
620
|
if (!path) continue;
|
|
436
621
|
if (this._flattenedParents.has(path)) continue;
|
|
437
|
-
if (this.
|
|
438
|
-
const isFlattened = this.
|
|
622
|
+
if (this._findAncestorInSet(path, this._jsonFields) !== undefined) continue;
|
|
623
|
+
const isFlattened = this._findAncestorInSet(path, this._flattenedParents) !== undefined;
|
|
439
624
|
const physicalName = this._columnMap.get(path) ?? (isFlattened ? path.replace(/\./g, "__") : path);
|
|
440
625
|
this._pathToPhysical.set(path, physicalName);
|
|
441
626
|
this._physicalToPath.set(physicalName, path);
|
|
627
|
+
const fieldType = this._flatMap?.get(path);
|
|
628
|
+
if (fieldType && resolveDesignType(fieldType) === "boolean") this._booleanFields.add(physicalName);
|
|
442
629
|
}
|
|
443
630
|
for (const parentPath of this._flattenedParents) {
|
|
444
631
|
const prefix = `${parentPath}.`;
|
|
@@ -449,24 +636,13 @@ else if (isObject && isJson) {} else if (isObject && !isJson) this._flattenedPar
|
|
|
449
636
|
this._requiresMappings = this._flattenedParents.size > 0 || this._jsonFields.size > 0;
|
|
450
637
|
}
|
|
451
638
|
/**
|
|
452
|
-
* Finds the nearest ancestor
|
|
453
|
-
*
|
|
454
|
-
*/
|
|
639
|
+
* Finds the nearest ancestor of `path` that belongs to `set`.
|
|
640
|
+
* Used to locate flattened parents and @db.json ancestors.
|
|
641
|
+
*/ _findAncestorInSet(path, set) {
|
|
455
642
|
let pos = path.length;
|
|
456
643
|
while ((pos = path.lastIndexOf(".", pos - 1)) !== -1) {
|
|
457
644
|
const ancestor = path.slice(0, pos);
|
|
458
|
-
if (
|
|
459
|
-
}
|
|
460
|
-
return undefined;
|
|
461
|
-
}
|
|
462
|
-
/**
|
|
463
|
-
* Finds the nearest ancestor that is a @db.json field.
|
|
464
|
-
* Children of JSON fields don't get their own columns.
|
|
465
|
-
*/ _findJsonParent(path) {
|
|
466
|
-
let pos = path.length;
|
|
467
|
-
while ((pos = path.lastIndexOf(".", pos - 1)) !== -1) {
|
|
468
|
-
const ancestor = path.slice(0, pos);
|
|
469
|
-
if (this._jsonFields.has(ancestor)) return ancestor;
|
|
645
|
+
if (set.has(ancestor)) return ancestor;
|
|
470
646
|
}
|
|
471
647
|
return undefined;
|
|
472
648
|
}
|
|
@@ -479,7 +655,20 @@ else if (isObject && isJson) {} else if (isObject && !isJson) this._flattenedPar
|
|
|
479
655
|
* Called before validation so that defaults satisfy required field constraints.
|
|
480
656
|
*/ _applyDefaults(data) {
|
|
481
657
|
for (const [field, def] of this._defaults.entries()) if (data[field] === undefined) {
|
|
482
|
-
if (def.kind === "value")
|
|
658
|
+
if (def.kind === "value") {
|
|
659
|
+
const fieldType = this._flatMap?.get(field);
|
|
660
|
+
const designType = fieldType?.type.kind === "" && fieldType.type.designType;
|
|
661
|
+
data[field] = designType === "string" ? def.value : JSON.parse(def.value);
|
|
662
|
+
} else if (def.kind === "fn") switch (def.fn) {
|
|
663
|
+
case "now": {
|
|
664
|
+
data[field] = Date.now();
|
|
665
|
+
break;
|
|
666
|
+
}
|
|
667
|
+
case "uuid": {
|
|
668
|
+
data[field] = crypto.randomUUID();
|
|
669
|
+
break;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
483
672
|
}
|
|
484
673
|
return data;
|
|
485
674
|
}
|
|
@@ -494,7 +683,7 @@ else if (isObject && isJson) {} else if (isObject && !isJson) this._flattenedPar
|
|
|
494
683
|
if (fieldType) data[pk] = this.adapter.prepareId(data[pk], fieldType);
|
|
495
684
|
}
|
|
496
685
|
for (const field of this._ignoredFields) if (!field.includes(".")) delete data[field];
|
|
497
|
-
if (!this._requiresMappings || this.
|
|
686
|
+
if (!this._requiresMappings || this._nestedObjects) {
|
|
498
687
|
for (const [logical, physical] of this._columnMap.entries()) if (logical in data) {
|
|
499
688
|
data[physical] = data[logical];
|
|
500
689
|
delete data[logical];
|
|
@@ -509,40 +698,26 @@ else if (isObject && isJson) {} else if (isObject && !isJson) this._flattenedPar
|
|
|
509
698
|
* Uses _pathToPhysical for final key names.
|
|
510
699
|
*/ _flattenPayload(data) {
|
|
511
700
|
const result = {};
|
|
512
|
-
const
|
|
513
|
-
for (const key of dataKeys) {
|
|
514
|
-
const value = data[key];
|
|
515
|
-
if (this._flattenedParents.has(key)) {
|
|
516
|
-
if (value === null || value === undefined) this._setFlattenedChildrenNull(key, result);
|
|
517
|
-
else if (typeof value === "object" && !Array.isArray(value)) this._flattenObject(key, value, result);
|
|
518
|
-
} else if (this._jsonFields.has(key)) {
|
|
519
|
-
const physical = this._pathToPhysical.get(key) ?? key;
|
|
520
|
-
result[physical] = value !== undefined && value !== null ? JSON.stringify(value) : value;
|
|
521
|
-
} else {
|
|
522
|
-
const physical = this._pathToPhysical.get(key) ?? key;
|
|
523
|
-
result[physical] = value;
|
|
524
|
-
}
|
|
525
|
-
}
|
|
701
|
+
for (const key of Object.keys(data)) this._writeFlattenedField(key, data[key], result);
|
|
526
702
|
return result;
|
|
527
703
|
}
|
|
528
704
|
/**
|
|
529
|
-
*
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
else if (typeof value === "object" && !Array.isArray(value)) this._flattenObject(dotPath, value, result);
|
|
539
|
-
} else if (this._jsonFields.has(dotPath)) {
|
|
540
|
-
const physical = this._pathToPhysical.get(dotPath) ?? dotPath.replace(/\./g, "__");
|
|
541
|
-
result[physical] = value !== undefined && value !== null ? JSON.stringify(value) : value;
|
|
542
|
-
} else {
|
|
543
|
-
const physical = this._pathToPhysical.get(dotPath) ?? dotPath.replace(/\./g, "__");
|
|
544
|
-
result[physical] = value;
|
|
705
|
+
* Classifies and writes a single field to the result object.
|
|
706
|
+
* Recurses into nested objects that should be flattened.
|
|
707
|
+
*/ _writeFlattenedField(path, value, result) {
|
|
708
|
+
if (this._ignoredFields.has(path)) return;
|
|
709
|
+
if (this._flattenedParents.has(path)) {
|
|
710
|
+
if (value === null || value === undefined) this._setFlattenedChildrenNull(path, result);
|
|
711
|
+
else if (typeof value === "object" && !Array.isArray(value)) {
|
|
712
|
+
const obj = value;
|
|
713
|
+
for (const key of Object.keys(obj)) this._writeFlattenedField(`${path}.${key}`, obj[key], result);
|
|
545
714
|
}
|
|
715
|
+
} else if (this._jsonFields.has(path)) {
|
|
716
|
+
const physical = this._pathToPhysical.get(path) ?? path.replace(/\./g, "__");
|
|
717
|
+
result[physical] = value !== undefined && value !== null ? JSON.stringify(value) : value;
|
|
718
|
+
} else {
|
|
719
|
+
const physical = this._pathToPhysical.get(path) ?? path.replace(/\./g, "__");
|
|
720
|
+
result[physical] = value;
|
|
546
721
|
}
|
|
547
722
|
}
|
|
548
723
|
/**
|
|
@@ -555,11 +730,11 @@ else if (typeof value === "object" && !Array.isArray(value)) this._flattenObject
|
|
|
555
730
|
* Reconstructs nested objects from flat __-separated column values.
|
|
556
731
|
* JSON fields are parsed from strings back to objects/arrays.
|
|
557
732
|
*/ _reconstructFromRead(row) {
|
|
558
|
-
if (!this._requiresMappings || this.
|
|
733
|
+
if (!this._requiresMappings || this._nestedObjects) return this._coerceBooleans(row);
|
|
559
734
|
const result = {};
|
|
560
735
|
const rowKeys = Object.keys(row);
|
|
561
736
|
for (const physical of rowKeys) {
|
|
562
|
-
const value = row[physical];
|
|
737
|
+
const value = this._booleanFields.has(physical) ? toBool(row[physical]) : row[physical];
|
|
563
738
|
const logicalPath = this._physicalToPath.get(physical);
|
|
564
739
|
if (!logicalPath) {
|
|
565
740
|
result[physical] = value;
|
|
@@ -575,6 +750,14 @@ else result[logicalPath] = value;
|
|
|
575
750
|
return result;
|
|
576
751
|
}
|
|
577
752
|
/**
|
|
753
|
+
* Coerces boolean fields from storage representation (0/1) to JS booleans.
|
|
754
|
+
* Used on the fast-path when no column mapping is needed.
|
|
755
|
+
*/ _coerceBooleans(row) {
|
|
756
|
+
if (this._booleanFields.size === 0) return row;
|
|
757
|
+
for (const field of this._booleanFields) if (field in row) row[field] = toBool(row[field]);
|
|
758
|
+
return row;
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
578
761
|
* Sets a value at a dot-notation path, creating intermediate objects as needed.
|
|
579
762
|
*/ _setNestedValue(obj, dotPath, value) {
|
|
580
763
|
const parts = dotPath.split(".");
|
|
@@ -615,12 +798,21 @@ else result[logicalPath] = value;
|
|
|
615
798
|
/**
|
|
616
799
|
* Translates a Uniquery's filter, sort, and projection from logical
|
|
617
800
|
* dot-notation paths to physical column names.
|
|
801
|
+
* Always wraps `$select` in {@link UniquSelect}.
|
|
618
802
|
*/ _translateQuery(query) {
|
|
619
|
-
if (!this._requiresMappings || this.
|
|
803
|
+
if (!this._requiresMappings || this._nestedObjects) {
|
|
804
|
+
const controls = query.controls;
|
|
805
|
+
return {
|
|
806
|
+
filter: query.filter,
|
|
807
|
+
controls: {
|
|
808
|
+
...controls,
|
|
809
|
+
$select: controls?.$select ? new UniquSelect(controls.$select, this._allPhysicalFields) : undefined
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
}
|
|
620
813
|
return {
|
|
621
814
|
filter: this._translateFilter(query.filter),
|
|
622
|
-
controls: query.controls ? this._translateControls(query.controls) :
|
|
623
|
-
insights: query.insights
|
|
815
|
+
controls: query.controls ? this._translateControls(query.controls) : {}
|
|
624
816
|
};
|
|
625
817
|
}
|
|
626
818
|
/**
|
|
@@ -644,9 +836,13 @@ else {
|
|
|
644
836
|
}
|
|
645
837
|
/**
|
|
646
838
|
* Translates field names in sort and projection controls.
|
|
839
|
+
* Wraps `$select` in {@link UniquSelect} after path translation.
|
|
647
840
|
*/ _translateControls(controls) {
|
|
648
|
-
if (!controls) return
|
|
649
|
-
const result = {
|
|
841
|
+
if (!controls) return {};
|
|
842
|
+
const result = {
|
|
843
|
+
...controls,
|
|
844
|
+
$select: undefined
|
|
845
|
+
};
|
|
650
846
|
if (controls.$sort) {
|
|
651
847
|
const translated = {};
|
|
652
848
|
const sortObj = controls.$sort;
|
|
@@ -658,35 +854,39 @@ else {
|
|
|
658
854
|
}
|
|
659
855
|
result.$sort = translated;
|
|
660
856
|
}
|
|
661
|
-
if (controls.$select)
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
const
|
|
665
|
-
|
|
857
|
+
if (controls.$select) {
|
|
858
|
+
let translatedRaw;
|
|
859
|
+
if (Array.isArray(controls.$select)) {
|
|
860
|
+
const expanded = [];
|
|
861
|
+
for (const key of controls.$select) {
|
|
862
|
+
const expansion = this._selectExpansion.get(key);
|
|
863
|
+
if (expansion) expanded.push(...expansion);
|
|
666
864
|
else expanded.push(this._pathToPhysical.get(key) ?? key);
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
865
|
+
}
|
|
866
|
+
translatedRaw = expanded;
|
|
867
|
+
} else {
|
|
868
|
+
const translated = {};
|
|
869
|
+
const selectObj = controls.$select;
|
|
870
|
+
const selectKeys = Object.keys(selectObj);
|
|
871
|
+
for (const key of selectKeys) {
|
|
872
|
+
const val = selectObj[key];
|
|
873
|
+
const expansion = this._selectExpansion.get(key);
|
|
874
|
+
if (expansion) for (const leaf of expansion) translated[leaf] = val;
|
|
677
875
|
else {
|
|
678
|
-
|
|
679
|
-
|
|
876
|
+
const physical = this._pathToPhysical.get(key) ?? key;
|
|
877
|
+
translated[physical] = val;
|
|
878
|
+
}
|
|
680
879
|
}
|
|
880
|
+
translatedRaw = translated;
|
|
681
881
|
}
|
|
682
|
-
result.$select =
|
|
882
|
+
result.$select = new UniquSelect(translatedRaw, this._allPhysicalFields);
|
|
683
883
|
}
|
|
684
884
|
return result;
|
|
685
885
|
}
|
|
686
886
|
/**
|
|
687
887
|
* Translates dot-notation keys in a decomposed patch to physical column names.
|
|
688
888
|
*/ _translatePatchKeys(update) {
|
|
689
|
-
if (!this._requiresMappings || this.
|
|
889
|
+
if (!this._requiresMappings || this._nestedObjects) return update;
|
|
690
890
|
const result = {};
|
|
691
891
|
const updateKeys = Object.keys(update);
|
|
692
892
|
for (const key of updateKeys) {
|
|
@@ -719,21 +919,26 @@ else result[finalKey] = value;
|
|
|
719
919
|
*/ _buildValidator(purpose) {
|
|
720
920
|
const plugins = this.adapter.getValidatorPlugins();
|
|
721
921
|
switch (purpose) {
|
|
722
|
-
case "insert":
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
922
|
+
case "insert": {
|
|
923
|
+
if (this.adapter.buildInsertValidator) return this.adapter.buildInsertValidator(this);
|
|
924
|
+
return this.createValidator({
|
|
925
|
+
plugins,
|
|
926
|
+
replace: (type, path) => {
|
|
927
|
+
if (this._primaryKeys.includes(path) || this._defaults.has(path)) return {
|
|
928
|
+
...type,
|
|
929
|
+
optional: true
|
|
930
|
+
};
|
|
931
|
+
return type;
|
|
932
|
+
}
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
case "patch": {
|
|
936
|
+
if (this.adapter.buildPatchValidator) return this.adapter.buildPatchValidator(this);
|
|
937
|
+
return this.createValidator({
|
|
938
|
+
plugins,
|
|
939
|
+
partial: true
|
|
940
|
+
});
|
|
941
|
+
}
|
|
737
942
|
default: return this.createValidator({ plugins });
|
|
738
943
|
}
|
|
739
944
|
}
|
|
@@ -756,7 +961,10 @@ else result[finalKey] = value;
|
|
|
756
961
|
/** Object paths being flattened into __-separated columns (no column themselves). */ _define_property$1(this, "_flattenedParents", void 0);
|
|
757
962
|
/** Fields stored as JSON (@db.json + array fields). */ _define_property$1(this, "_jsonFields", void 0);
|
|
758
963
|
/** Intermediate paths → their leaf physical column names (for $select expansion in relational DBs). */ _define_property$1(this, "_selectExpansion", void 0);
|
|
964
|
+
/** Physical column names of boolean fields (for storage coercion on read). */ _define_property$1(this, "_booleanFields", void 0);
|
|
759
965
|
/** Fast-path flag: skip all mapping when no nested/json fields exist. */ _define_property$1(this, "_requiresMappings", void 0);
|
|
966
|
+
/** All non-ignored physical field names (for UniquSelect exclusion inversion). */ _define_property$1(this, "_allPhysicalFields", void 0);
|
|
967
|
+
/** Cached result of adapter.supportsNestedObjects(). */ _define_property$1(this, "_nestedObjects", void 0);
|
|
760
968
|
_define_property$1(this, "validators", void 0);
|
|
761
969
|
this._type = _type;
|
|
762
970
|
this.adapter = adapter;
|
|
@@ -772,7 +980,9 @@ else result[finalKey] = value;
|
|
|
772
980
|
this._flattenedParents = new Set();
|
|
773
981
|
this._jsonFields = new Set();
|
|
774
982
|
this._selectExpansion = new Map();
|
|
983
|
+
this._booleanFields = new Set();
|
|
775
984
|
this._requiresMappings = false;
|
|
985
|
+
this._allPhysicalFields = [];
|
|
776
986
|
this.validators = new Map();
|
|
777
987
|
if (!isAnnotatedType(_type)) throw new Error("Atscript Annotated Type expected");
|
|
778
988
|
if (_type.type.kind !== "object") throw new Error("Database table type must be an object type");
|
|
@@ -782,6 +992,7 @@ else result[finalKey] = value;
|
|
|
782
992
|
this.tableName = adapterName || dbTable || fallbackName;
|
|
783
993
|
if (!this.tableName) throw new Error("@db.table annotation or adapter-specific table name expected");
|
|
784
994
|
this.schema = _type.metadata.get("db.schema");
|
|
995
|
+
this._nestedObjects = adapter.supportsNestedObjects();
|
|
785
996
|
adapter.registerTable(this);
|
|
786
997
|
}
|
|
787
998
|
};
|
|
@@ -887,10 +1098,50 @@ var BaseDbAdapter = class {
|
|
|
887
1098
|
}
|
|
888
1099
|
for (const name of existingNames) if (!desiredNames.has(name)) await opts.dropIndex(name);
|
|
889
1100
|
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Returns available search indexes for this adapter.
|
|
1103
|
+
* UI uses this to show index picker. Override in adapters that support search.
|
|
1104
|
+
*/ getSearchIndexes() {
|
|
1105
|
+
return [];
|
|
1106
|
+
}
|
|
1107
|
+
/**
|
|
1108
|
+
* Whether this adapter supports text search.
|
|
1109
|
+
* Default: `true` when {@link getSearchIndexes} returns any entries.
|
|
1110
|
+
*/ isSearchable() {
|
|
1111
|
+
return this.getSearchIndexes().length > 0;
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* Full-text search. Override in adapters that support search.
|
|
1115
|
+
*
|
|
1116
|
+
* @param text - Search text.
|
|
1117
|
+
* @param query - Filter, sort, limit, etc.
|
|
1118
|
+
* @param indexName - Optional search index to target.
|
|
1119
|
+
*/ async search(text, query, indexName) {
|
|
1120
|
+
throw new Error("Search not supported by this adapter");
|
|
1121
|
+
}
|
|
1122
|
+
/**
|
|
1123
|
+
* Full-text search with count (for paginated search results).
|
|
1124
|
+
*
|
|
1125
|
+
* @param text - Search text.
|
|
1126
|
+
* @param query - Filter, sort, limit, etc.
|
|
1127
|
+
* @param indexName - Optional search index to target.
|
|
1128
|
+
*/ async searchWithCount(text, query, indexName) {
|
|
1129
|
+
throw new Error("Search not supported by this adapter");
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* Fetches records and total count in one call.
|
|
1133
|
+
* Default: two parallel calls. Adapters may override for single-query optimization.
|
|
1134
|
+
*/ async findManyWithCount(query) {
|
|
1135
|
+
const [data, count] = await Promise.all([this.findMany(query), this.count(query)]);
|
|
1136
|
+
return {
|
|
1137
|
+
data,
|
|
1138
|
+
count
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
890
1141
|
constructor() {
|
|
891
1142
|
_define_property(this, "_table", void 0);
|
|
892
1143
|
}
|
|
893
1144
|
};
|
|
894
1145
|
|
|
895
1146
|
//#endregion
|
|
896
|
-
export { AtscriptDbTable, BaseDbAdapter, NoopLogger, decomposePatch, getKeyProps, isPrimitive, resolveDesignType, walkFilter };
|
|
1147
|
+
export { AtscriptDbTable, BaseDbAdapter, NoopLogger, UniquSelect, decomposePatch, getKeyProps, isPrimitive, resolveDesignType, walkFilter };
|