@atscript/utils-db 0.1.30 → 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/README.md +21 -0
- package/dist/index.cjs +631 -54
- package/dist/index.d.ts +299 -32
- package/dist/index.mjs +631 -55
- 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;
|
|
@@ -132,6 +234,14 @@ var AtscriptDbTable = class {
|
|
|
132
234
|
this._flatten();
|
|
133
235
|
return this._uniqueProps;
|
|
134
236
|
}
|
|
237
|
+
/** Precomputed logical dot-path → physical column name map. */ get pathToPhysical() {
|
|
238
|
+
this._flatten();
|
|
239
|
+
return this._pathToPhysical;
|
|
240
|
+
}
|
|
241
|
+
/** Precomputed physical column name → logical dot-path map (inverse). */ get physicalToPath() {
|
|
242
|
+
this._flatten();
|
|
243
|
+
return this._physicalToPath;
|
|
244
|
+
}
|
|
135
245
|
/** Descriptor for the primary ID field(s). */ getIdDescriptor() {
|
|
136
246
|
this._flatten();
|
|
137
247
|
return {
|
|
@@ -147,39 +257,38 @@ var AtscriptDbTable = class {
|
|
|
147
257
|
this._flatten();
|
|
148
258
|
if (!this._fieldDescriptors) {
|
|
149
259
|
this._fieldDescriptors = [];
|
|
260
|
+
const skipFlattening = this._nestedObjects;
|
|
150
261
|
for (const [path, type] of this._flatMap.entries()) {
|
|
151
262
|
if (!path) continue;
|
|
263
|
+
if (!skipFlattening && this._flattenedParents.has(path)) continue;
|
|
264
|
+
if (!skipFlattening && this._findAncestorInSet(path, this._jsonFields) !== undefined) continue;
|
|
265
|
+
const isJson = this._jsonFields.has(path);
|
|
266
|
+
const isFlattened = !skipFlattening && this._findAncestorInSet(path, this._flattenedParents) !== undefined;
|
|
267
|
+
const designType = isJson ? "json" : resolveDesignType(type);
|
|
268
|
+
let storage;
|
|
269
|
+
if (skipFlattening) storage = "column";
|
|
270
|
+
else if (isJson) storage = "json";
|
|
271
|
+
else if (isFlattened) storage = "flattened";
|
|
272
|
+
else storage = "column";
|
|
273
|
+
const physicalName = skipFlattening ? this._columnMap.get(path) ?? path : this._pathToPhysical.get(path) ?? this._columnMap.get(path) ?? path;
|
|
152
274
|
this._fieldDescriptors.push({
|
|
153
275
|
path,
|
|
154
276
|
type,
|
|
155
|
-
physicalName
|
|
156
|
-
designType
|
|
277
|
+
physicalName,
|
|
278
|
+
designType,
|
|
157
279
|
optional: type.optional === true,
|
|
158
280
|
isPrimaryKey: this._primaryKeys.includes(path),
|
|
159
281
|
ignored: this._ignoredFields.has(path),
|
|
160
|
-
defaultValue: this._defaults.get(path)
|
|
282
|
+
defaultValue: this._defaults.get(path),
|
|
283
|
+
storage,
|
|
284
|
+
flattenedFrom: isFlattened ? path : undefined
|
|
161
285
|
});
|
|
162
286
|
}
|
|
287
|
+
Object.freeze(this._fieldDescriptors);
|
|
163
288
|
}
|
|
164
289
|
return this._fieldDescriptors;
|
|
165
290
|
}
|
|
166
291
|
/**
|
|
167
|
-
* Resolves `$select` from {@link UniqueryControls} to a list of field names.
|
|
168
|
-
* - `undefined` → `undefined` (all fields)
|
|
169
|
-
* - `string[]` → pass through
|
|
170
|
-
* - `Record<K, 1>` → extract included keys
|
|
171
|
-
* - `Record<K, 0>` → invert using known field names
|
|
172
|
-
*/ resolveProjection(select) {
|
|
173
|
-
if (!select) return undefined;
|
|
174
|
-
if (Array.isArray(select)) return select.length > 0 ? select : undefined;
|
|
175
|
-
const entries = Object.entries(select);
|
|
176
|
-
if (entries.length === 0) return undefined;
|
|
177
|
-
const firstVal = entries[0][1];
|
|
178
|
-
if (firstVal === 1) return entries.filter(([, v]) => v === 1).map(([k]) => k);
|
|
179
|
-
const excluded = new Set(entries.filter(([, v]) => v === 0).map(([k]) => k));
|
|
180
|
-
return this.fieldDescriptors.filter((f) => !f.ignored && !excluded.has(f.path)).map((f) => f.path);
|
|
181
|
-
}
|
|
182
|
-
/**
|
|
183
292
|
* Creates a new validator with custom options.
|
|
184
293
|
* Adapter plugins are NOT automatically included — use {@link getValidator}
|
|
185
294
|
* for the standard validator with adapter plugins.
|
|
@@ -242,9 +351,9 @@ var AtscriptDbTable = class {
|
|
|
242
351
|
const validator = this.getValidator("patch");
|
|
243
352
|
if (!validator.validate(payload)) throw new Error("Validation failed for update");
|
|
244
353
|
const filter = this._extractPrimaryKeyFilter(payload);
|
|
245
|
-
if (this.adapter.supportsNativePatch()) return this.adapter.nativePatch(filter, payload);
|
|
354
|
+
if (this.adapter.supportsNativePatch()) return this.adapter.nativePatch(this._translateFilter(filter), payload);
|
|
246
355
|
const update = decomposePatch(payload, this);
|
|
247
|
-
return this.adapter.updateOne(filter, update);
|
|
356
|
+
return this.adapter.updateOne(this._translateFilter(filter), this._translatePatchKeys(update));
|
|
248
357
|
}
|
|
249
358
|
/**
|
|
250
359
|
* Deletes a single record by primary key value.
|
|
@@ -264,35 +373,145 @@ var AtscriptDbTable = class {
|
|
|
264
373
|
filter[field] = fieldType ? this.adapter.prepareId(idObj[field], fieldType) : idObj[field];
|
|
265
374
|
}
|
|
266
375
|
}
|
|
267
|
-
return this.adapter.deleteOne(filter);
|
|
376
|
+
return this.adapter.deleteOne(this._translateFilter(filter));
|
|
268
377
|
}
|
|
269
378
|
/**
|
|
270
379
|
* Finds a single record matching the query.
|
|
271
380
|
*/ async findOne(query) {
|
|
272
|
-
|
|
381
|
+
this._flatten();
|
|
382
|
+
const translatedQuery = this._translateQuery(query);
|
|
383
|
+
const result = await this.adapter.findOne(translatedQuery);
|
|
384
|
+
return result ? this._reconstructFromRead(result) : null;
|
|
273
385
|
}
|
|
274
386
|
/**
|
|
275
387
|
* Finds all records matching the query.
|
|
276
388
|
*/ async findMany(query) {
|
|
277
|
-
|
|
389
|
+
this._flatten();
|
|
390
|
+
const translatedQuery = this._translateQuery(query);
|
|
391
|
+
const results = await this.adapter.findMany(translatedQuery);
|
|
392
|
+
return results.map((row) => this._reconstructFromRead(row));
|
|
278
393
|
}
|
|
279
394
|
/**
|
|
280
395
|
* Counts records matching the query.
|
|
281
396
|
*/ async count(query) {
|
|
397
|
+
this._flatten();
|
|
282
398
|
query ?? (query = {
|
|
283
399
|
filter: {},
|
|
284
400
|
controls: {}
|
|
285
401
|
});
|
|
286
|
-
return this.adapter.count(query);
|
|
402
|
+
return this.adapter.count(this._translateQuery(query));
|
|
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;
|
|
287
503
|
}
|
|
288
504
|
async updateMany(filter, data) {
|
|
289
|
-
|
|
505
|
+
this._flatten();
|
|
506
|
+
return this.adapter.updateMany(this._translateFilter(filter), this._prepareForWrite({ ...data }));
|
|
290
507
|
}
|
|
291
508
|
async replaceMany(filter, data) {
|
|
292
|
-
|
|
509
|
+
this._flatten();
|
|
510
|
+
return this.adapter.replaceMany(this._translateFilter(filter), this._prepareForWrite({ ...data }));
|
|
293
511
|
}
|
|
294
512
|
async deleteMany(filter) {
|
|
295
|
-
|
|
513
|
+
this._flatten();
|
|
514
|
+
return this.adapter.deleteMany(this._translateFilter(filter));
|
|
296
515
|
}
|
|
297
516
|
/**
|
|
298
517
|
* Synchronizes indexes between Atscript definitions and the database.
|
|
@@ -318,8 +537,12 @@ var AtscriptDbTable = class {
|
|
|
318
537
|
this.adapter.onFieldScanned?.(path, type, metadata);
|
|
319
538
|
}
|
|
320
539
|
});
|
|
540
|
+
if (!this._nestedObjects) this._classifyFields();
|
|
321
541
|
this._finalizeIndexes();
|
|
322
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);
|
|
323
546
|
}
|
|
324
547
|
/**
|
|
325
548
|
* Scans `@db.*` and `@meta.id` annotations on a field during flattening.
|
|
@@ -341,7 +564,7 @@ else if (defaultFn !== undefined) this._defaults.set(fieldName, {
|
|
|
341
564
|
for (const index of metadata.get("db.index.plain") || []) {
|
|
342
565
|
const name = index === true ? fieldName : index?.name || fieldName;
|
|
343
566
|
const sort = (index === true ? undefined : index?.sort) || "asc";
|
|
344
|
-
this._addIndexField("plain", name, fieldName, sort);
|
|
567
|
+
this._addIndexField("plain", name, fieldName, { sort });
|
|
345
568
|
}
|
|
346
569
|
for (const index of metadata.get("db.index.unique") || []) {
|
|
347
570
|
const name = index === true ? fieldName : typeof index === "string" ? index : index?.name || fieldName;
|
|
@@ -349,16 +572,23 @@ else if (defaultFn !== undefined) this._defaults.set(fieldName, {
|
|
|
349
572
|
}
|
|
350
573
|
for (const index of metadata.get("db.index.fulltext") || []) {
|
|
351
574
|
const name = index === true ? fieldName : typeof index === "string" ? index : index?.name || fieldName;
|
|
352
|
-
|
|
575
|
+
const weight = index !== true && typeof index === "object" ? index?.weight : undefined;
|
|
576
|
+
this._addIndexField("fulltext", name, fieldName, { weight });
|
|
577
|
+
}
|
|
578
|
+
if (metadata.has("db.json")) {
|
|
579
|
+
this._jsonFields.add(fieldName);
|
|
580
|
+
const hasIndex = metadata.has("db.index.plain") || metadata.has("db.index.unique") || metadata.has("db.index.fulltext");
|
|
581
|
+
if (hasIndex) this.logger.warn(`@db.index on a @db.json field "${fieldName}" — most databases cannot index into JSON columns`);
|
|
353
582
|
}
|
|
354
583
|
}
|
|
355
|
-
_addIndexField(type, name, field,
|
|
584
|
+
_addIndexField(type, name, field, opts) {
|
|
356
585
|
const key = indexKey(type, name);
|
|
357
586
|
const index = this._indexes.get(key);
|
|
358
587
|
const indexField = {
|
|
359
588
|
name: field,
|
|
360
|
-
sort
|
|
589
|
+
sort: opts?.sort ?? "asc"
|
|
361
590
|
};
|
|
591
|
+
if (opts?.weight !== undefined) indexField.weight = opts.weight;
|
|
362
592
|
if (index) index.fields.push(indexField);
|
|
363
593
|
else this._indexes.set(key, {
|
|
364
594
|
key,
|
|
@@ -367,7 +597,57 @@ else this._indexes.set(key, {
|
|
|
367
597
|
fields: [indexField]
|
|
368
598
|
});
|
|
369
599
|
}
|
|
600
|
+
/**
|
|
601
|
+
* Classifies each field as column, flattened, json, or parent-object.
|
|
602
|
+
* Builds the bidirectional _pathToPhysical / _physicalToPath maps.
|
|
603
|
+
* Only called when the adapter does NOT support nested objects natively.
|
|
604
|
+
*/ _classifyFields() {
|
|
605
|
+
for (const [path, type] of this._flatMap.entries()) {
|
|
606
|
+
if (!path) continue;
|
|
607
|
+
const designType = resolveDesignType(type);
|
|
608
|
+
const isJson = this._jsonFields.has(path);
|
|
609
|
+
const isArray = designType === "array";
|
|
610
|
+
const isObject = designType === "object";
|
|
611
|
+
if (isArray) this._jsonFields.add(path);
|
|
612
|
+
else if (isObject && isJson) {} else if (isObject && !isJson) this._flattenedParents.add(path);
|
|
613
|
+
}
|
|
614
|
+
for (const ignoredField of this._ignoredFields) if (this._flattenedParents.has(ignoredField)) {
|
|
615
|
+
const prefix = `${ignoredField}.`;
|
|
616
|
+
for (const path of this._flatMap.keys()) if (path.startsWith(prefix)) this._ignoredFields.add(path);
|
|
617
|
+
}
|
|
618
|
+
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`);
|
|
619
|
+
for (const [path] of this._flatMap.entries()) {
|
|
620
|
+
if (!path) continue;
|
|
621
|
+
if (this._flattenedParents.has(path)) continue;
|
|
622
|
+
if (this._findAncestorInSet(path, this._jsonFields) !== undefined) continue;
|
|
623
|
+
const isFlattened = this._findAncestorInSet(path, this._flattenedParents) !== undefined;
|
|
624
|
+
const physicalName = this._columnMap.get(path) ?? (isFlattened ? path.replace(/\./g, "__") : path);
|
|
625
|
+
this._pathToPhysical.set(path, physicalName);
|
|
626
|
+
this._physicalToPath.set(physicalName, path);
|
|
627
|
+
const fieldType = this._flatMap?.get(path);
|
|
628
|
+
if (fieldType && resolveDesignType(fieldType) === "boolean") this._booleanFields.add(physicalName);
|
|
629
|
+
}
|
|
630
|
+
for (const parentPath of this._flattenedParents) {
|
|
631
|
+
const prefix = `${parentPath}.`;
|
|
632
|
+
const leaves = [];
|
|
633
|
+
for (const [path, physical] of this._pathToPhysical) if (path.startsWith(prefix)) leaves.push(physical);
|
|
634
|
+
if (leaves.length > 0) this._selectExpansion.set(parentPath, leaves);
|
|
635
|
+
}
|
|
636
|
+
this._requiresMappings = this._flattenedParents.size > 0 || this._jsonFields.size > 0;
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
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) {
|
|
642
|
+
let pos = path.length;
|
|
643
|
+
while ((pos = path.lastIndexOf(".", pos - 1)) !== -1) {
|
|
644
|
+
const ancestor = path.slice(0, pos);
|
|
645
|
+
if (set.has(ancestor)) return ancestor;
|
|
646
|
+
}
|
|
647
|
+
return undefined;
|
|
648
|
+
}
|
|
370
649
|
_finalizeIndexes() {
|
|
650
|
+
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;
|
|
371
651
|
for (const index of this._indexes.values()) if (index.type === "unique" && index.fields.length === 1) this._uniqueProps.add(index.fields[0].name);
|
|
372
652
|
}
|
|
373
653
|
/**
|
|
@@ -375,13 +655,26 @@ else this._indexes.set(key, {
|
|
|
375
655
|
* Called before validation so that defaults satisfy required field constraints.
|
|
376
656
|
*/ _applyDefaults(data) {
|
|
377
657
|
for (const [field, def] of this._defaults.entries()) if (data[field] === undefined) {
|
|
378
|
-
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
|
+
}
|
|
379
672
|
}
|
|
380
673
|
return data;
|
|
381
674
|
}
|
|
382
675
|
/**
|
|
383
676
|
* Prepares a payload for writing to the database:
|
|
384
|
-
* prepares IDs, strips ignored fields, maps column names.
|
|
677
|
+
* prepares IDs, strips ignored fields, flattens nested objects, maps column names.
|
|
385
678
|
* Defaults should be applied before this via `_applyDefaults`.
|
|
386
679
|
*/ _prepareForWrite(payload) {
|
|
387
680
|
const data = { ...payload };
|
|
@@ -389,12 +682,224 @@ else this._indexes.set(key, {
|
|
|
389
682
|
const fieldType = this._flatMap?.get(pk);
|
|
390
683
|
if (fieldType) data[pk] = this.adapter.prepareId(data[pk], fieldType);
|
|
391
684
|
}
|
|
392
|
-
for (const field of this._ignoredFields) delete data[field];
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
685
|
+
for (const field of this._ignoredFields) if (!field.includes(".")) delete data[field];
|
|
686
|
+
if (!this._requiresMappings || this._nestedObjects) {
|
|
687
|
+
for (const [logical, physical] of this._columnMap.entries()) if (logical in data) {
|
|
688
|
+
data[physical] = data[logical];
|
|
689
|
+
delete data[logical];
|
|
690
|
+
}
|
|
691
|
+
return data;
|
|
396
692
|
}
|
|
397
|
-
return data;
|
|
693
|
+
return this._flattenPayload(data);
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Flattens nested object fields into __-separated keys and
|
|
697
|
+
* JSON-stringifies @db.json / array fields.
|
|
698
|
+
* Uses _pathToPhysical for final key names.
|
|
699
|
+
*/ _flattenPayload(data) {
|
|
700
|
+
const result = {};
|
|
701
|
+
for (const key of Object.keys(data)) this._writeFlattenedField(key, data[key], result);
|
|
702
|
+
return result;
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
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);
|
|
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;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* When a parent object is null/undefined, set all its flattened children to null.
|
|
725
|
+
*/ _setFlattenedChildrenNull(parentPath, result) {
|
|
726
|
+
const prefix = `${parentPath}.`;
|
|
727
|
+
for (const [path, physical] of this._pathToPhysical.entries()) if (path.startsWith(prefix)) result[physical] = null;
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Reconstructs nested objects from flat __-separated column values.
|
|
731
|
+
* JSON fields are parsed from strings back to objects/arrays.
|
|
732
|
+
*/ _reconstructFromRead(row) {
|
|
733
|
+
if (!this._requiresMappings || this._nestedObjects) return this._coerceBooleans(row);
|
|
734
|
+
const result = {};
|
|
735
|
+
const rowKeys = Object.keys(row);
|
|
736
|
+
for (const physical of rowKeys) {
|
|
737
|
+
const value = this._booleanFields.has(physical) ? toBool(row[physical]) : row[physical];
|
|
738
|
+
const logicalPath = this._physicalToPath.get(physical);
|
|
739
|
+
if (!logicalPath) {
|
|
740
|
+
result[physical] = value;
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
if (this._jsonFields.has(logicalPath)) {
|
|
744
|
+
const parsed = typeof value === "string" ? JSON.parse(value) : value;
|
|
745
|
+
this._setNestedValue(result, logicalPath, parsed);
|
|
746
|
+
} else if (logicalPath.includes(".")) this._setNestedValue(result, logicalPath, value);
|
|
747
|
+
else result[logicalPath] = value;
|
|
748
|
+
}
|
|
749
|
+
for (const parentPath of this._flattenedParents) this._reconstructNullParent(result, parentPath);
|
|
750
|
+
return result;
|
|
751
|
+
}
|
|
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
|
+
/**
|
|
761
|
+
* Sets a value at a dot-notation path, creating intermediate objects as needed.
|
|
762
|
+
*/ _setNestedValue(obj, dotPath, value) {
|
|
763
|
+
const parts = dotPath.split(".");
|
|
764
|
+
let current = obj;
|
|
765
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
766
|
+
const part = parts[i];
|
|
767
|
+
if (current[part] === undefined || current[part] === null) current[part] = {};
|
|
768
|
+
current = current[part];
|
|
769
|
+
}
|
|
770
|
+
current[parts[parts.length - 1]] = value;
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* If all children of a flattened parent are null, collapse the parent to null.
|
|
774
|
+
*/ _reconstructNullParent(obj, parentPath) {
|
|
775
|
+
const parts = parentPath.split(".");
|
|
776
|
+
let current = obj;
|
|
777
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
778
|
+
if (current[parts[i]] === undefined) return;
|
|
779
|
+
current = current[parts[i]];
|
|
780
|
+
}
|
|
781
|
+
const lastPart = parts[parts.length - 1];
|
|
782
|
+
const parentObj = current[lastPart];
|
|
783
|
+
if (typeof parentObj !== "object" || parentObj === null) return;
|
|
784
|
+
let allNull = true;
|
|
785
|
+
const parentKeys = Object.keys(parentObj);
|
|
786
|
+
for (const k of parentKeys) {
|
|
787
|
+
const v = parentObj[k];
|
|
788
|
+
if (v !== null && v !== undefined) {
|
|
789
|
+
allNull = false;
|
|
790
|
+
break;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (allNull) {
|
|
794
|
+
const parentType = this._flatMap?.get(parentPath);
|
|
795
|
+
current[lastPart] = parentType?.optional ? null : {};
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Translates a Uniquery's filter, sort, and projection from logical
|
|
800
|
+
* dot-notation paths to physical column names.
|
|
801
|
+
* Always wraps `$select` in {@link UniquSelect}.
|
|
802
|
+
*/ _translateQuery(query) {
|
|
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
|
+
}
|
|
813
|
+
return {
|
|
814
|
+
filter: this._translateFilter(query.filter),
|
|
815
|
+
controls: query.controls ? this._translateControls(query.controls) : {}
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Recursively translates field names in a filter expression.
|
|
820
|
+
*/ _translateFilter(filter) {
|
|
821
|
+
if (!filter || typeof filter !== "object") return filter;
|
|
822
|
+
if (!this._requiresMappings) return filter;
|
|
823
|
+
const result = {};
|
|
824
|
+
const filterKeys = Object.keys(filter);
|
|
825
|
+
for (const key of filterKeys) {
|
|
826
|
+
const value = filter[key];
|
|
827
|
+
if (key === "$and" || key === "$or") result[key] = value.map((f) => this._translateFilter(f));
|
|
828
|
+
else if (key === "$not") result[key] = this._translateFilter(value);
|
|
829
|
+
else if (key.startsWith("$")) result[key] = value;
|
|
830
|
+
else {
|
|
831
|
+
const physical = this._pathToPhysical.get(key) ?? key;
|
|
832
|
+
result[physical] = value;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
return result;
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Translates field names in sort and projection controls.
|
|
839
|
+
* Wraps `$select` in {@link UniquSelect} after path translation.
|
|
840
|
+
*/ _translateControls(controls) {
|
|
841
|
+
if (!controls) return {};
|
|
842
|
+
const result = {
|
|
843
|
+
...controls,
|
|
844
|
+
$select: undefined
|
|
845
|
+
};
|
|
846
|
+
if (controls.$sort) {
|
|
847
|
+
const translated = {};
|
|
848
|
+
const sortObj = controls.$sort;
|
|
849
|
+
const sortKeys = Object.keys(sortObj);
|
|
850
|
+
for (const key of sortKeys) {
|
|
851
|
+
if (this._flattenedParents.has(key)) continue;
|
|
852
|
+
const physical = this._pathToPhysical.get(key) ?? key;
|
|
853
|
+
translated[physical] = sortObj[key];
|
|
854
|
+
}
|
|
855
|
+
result.$sort = translated;
|
|
856
|
+
}
|
|
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);
|
|
864
|
+
else expanded.push(this._pathToPhysical.get(key) ?? key);
|
|
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;
|
|
875
|
+
else {
|
|
876
|
+
const physical = this._pathToPhysical.get(key) ?? key;
|
|
877
|
+
translated[physical] = val;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
translatedRaw = translated;
|
|
881
|
+
}
|
|
882
|
+
result.$select = new UniquSelect(translatedRaw, this._allPhysicalFields);
|
|
883
|
+
}
|
|
884
|
+
return result;
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Translates dot-notation keys in a decomposed patch to physical column names.
|
|
888
|
+
*/ _translatePatchKeys(update) {
|
|
889
|
+
if (!this._requiresMappings || this._nestedObjects) return update;
|
|
890
|
+
const result = {};
|
|
891
|
+
const updateKeys = Object.keys(update);
|
|
892
|
+
for (const key of updateKeys) {
|
|
893
|
+
const value = update[key];
|
|
894
|
+
const operatorMatch = key.match(/^(.+?)(\.__\$.+)$/);
|
|
895
|
+
const basePath = operatorMatch ? operatorMatch[1] : key;
|
|
896
|
+
const suffix = operatorMatch ? operatorMatch[2] : "";
|
|
897
|
+
const physical = this._pathToPhysical.get(basePath) ?? basePath;
|
|
898
|
+
const finalKey = physical + suffix;
|
|
899
|
+
if (this._jsonFields.has(basePath) && typeof value === "object" && value !== null && !suffix) result[finalKey] = JSON.stringify(value);
|
|
900
|
+
else result[finalKey] = value;
|
|
901
|
+
}
|
|
902
|
+
return result;
|
|
398
903
|
}
|
|
399
904
|
/**
|
|
400
905
|
* Extracts primary key field(s) from a payload to build a filter.
|
|
@@ -414,21 +919,26 @@ else this._indexes.set(key, {
|
|
|
414
919
|
*/ _buildValidator(purpose) {
|
|
415
920
|
const plugins = this.adapter.getValidatorPlugins();
|
|
416
921
|
switch (purpose) {
|
|
417
|
-
case "insert":
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
+
}
|
|
432
942
|
default: return this.createValidator({ plugins });
|
|
433
943
|
}
|
|
434
944
|
}
|
|
@@ -446,6 +956,15 @@ else this._indexes.set(key, {
|
|
|
446
956
|
_define_property$1(this, "_defaults", void 0);
|
|
447
957
|
_define_property$1(this, "_ignoredFields", void 0);
|
|
448
958
|
_define_property$1(this, "_uniqueProps", void 0);
|
|
959
|
+
/** Logical dot-path → physical column name. */ _define_property$1(this, "_pathToPhysical", void 0);
|
|
960
|
+
/** Physical column name → logical dot-path (inverse). */ _define_property$1(this, "_physicalToPath", void 0);
|
|
961
|
+
/** Object paths being flattened into __-separated columns (no column themselves). */ _define_property$1(this, "_flattenedParents", void 0);
|
|
962
|
+
/** Fields stored as JSON (@db.json + array fields). */ _define_property$1(this, "_jsonFields", void 0);
|
|
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);
|
|
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);
|
|
449
968
|
_define_property$1(this, "validators", void 0);
|
|
450
969
|
this._type = _type;
|
|
451
970
|
this.adapter = adapter;
|
|
@@ -456,6 +975,14 @@ else this._indexes.set(key, {
|
|
|
456
975
|
this._defaults = new Map();
|
|
457
976
|
this._ignoredFields = new Set();
|
|
458
977
|
this._uniqueProps = new Set();
|
|
978
|
+
this._pathToPhysical = new Map();
|
|
979
|
+
this._physicalToPath = new Map();
|
|
980
|
+
this._flattenedParents = new Set();
|
|
981
|
+
this._jsonFields = new Set();
|
|
982
|
+
this._selectExpansion = new Map();
|
|
983
|
+
this._booleanFields = new Set();
|
|
984
|
+
this._requiresMappings = false;
|
|
985
|
+
this._allPhysicalFields = [];
|
|
459
986
|
this.validators = new Map();
|
|
460
987
|
if (!isAnnotatedType(_type)) throw new Error("Atscript Annotated Type expected");
|
|
461
988
|
if (_type.type.kind !== "object") throw new Error("Database table type must be an object type");
|
|
@@ -465,6 +992,7 @@ else this._indexes.set(key, {
|
|
|
465
992
|
this.tableName = adapterName || dbTable || fallbackName;
|
|
466
993
|
if (!this.tableName) throw new Error("@db.table annotation or adapter-specific table name expected");
|
|
467
994
|
this.schema = _type.metadata.get("db.schema");
|
|
995
|
+
this._nestedObjects = adapter.supportsNestedObjects();
|
|
468
996
|
adapter.registerTable(this);
|
|
469
997
|
}
|
|
470
998
|
};
|
|
@@ -515,6 +1043,14 @@ var BaseDbAdapter = class {
|
|
|
515
1043
|
return false;
|
|
516
1044
|
}
|
|
517
1045
|
/**
|
|
1046
|
+
* Whether this adapter handles nested objects natively.
|
|
1047
|
+
* When `true`, the generic layer skips flattening and
|
|
1048
|
+
* passes nested objects as-is to the adapter.
|
|
1049
|
+
* MongoDB returns `true`; relational adapters return `false` (default).
|
|
1050
|
+
*/ supportsNestedObjects() {
|
|
1051
|
+
return false;
|
|
1052
|
+
}
|
|
1053
|
+
/**
|
|
518
1054
|
* Applies a patch payload using native database operations.
|
|
519
1055
|
* Only called when {@link supportsNativePatch} returns `true`.
|
|
520
1056
|
*
|
|
@@ -562,10 +1098,50 @@ var BaseDbAdapter = class {
|
|
|
562
1098
|
}
|
|
563
1099
|
for (const name of existingNames) if (!desiredNames.has(name)) await opts.dropIndex(name);
|
|
564
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
|
+
}
|
|
565
1141
|
constructor() {
|
|
566
1142
|
_define_property(this, "_table", void 0);
|
|
567
1143
|
}
|
|
568
1144
|
};
|
|
569
1145
|
|
|
570
1146
|
//#endregion
|
|
571
|
-
export { AtscriptDbTable, BaseDbAdapter, NoopLogger, decomposePatch, getKeyProps, isPrimitive, resolveDesignType, walkFilter };
|
|
1147
|
+
export { AtscriptDbTable, BaseDbAdapter, NoopLogger, UniquSelect, decomposePatch, getKeyProps, isPrimitive, resolveDesignType, walkFilter };
|