@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.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;
|
|
@@ -156,6 +258,14 @@ var AtscriptDbTable = class {
|
|
|
156
258
|
this._flatten();
|
|
157
259
|
return this._uniqueProps;
|
|
158
260
|
}
|
|
261
|
+
/** Precomputed logical dot-path → physical column name map. */ get pathToPhysical() {
|
|
262
|
+
this._flatten();
|
|
263
|
+
return this._pathToPhysical;
|
|
264
|
+
}
|
|
265
|
+
/** Precomputed physical column name → logical dot-path map (inverse). */ get physicalToPath() {
|
|
266
|
+
this._flatten();
|
|
267
|
+
return this._physicalToPath;
|
|
268
|
+
}
|
|
159
269
|
/** Descriptor for the primary ID field(s). */ getIdDescriptor() {
|
|
160
270
|
this._flatten();
|
|
161
271
|
return {
|
|
@@ -171,39 +281,38 @@ var AtscriptDbTable = class {
|
|
|
171
281
|
this._flatten();
|
|
172
282
|
if (!this._fieldDescriptors) {
|
|
173
283
|
this._fieldDescriptors = [];
|
|
284
|
+
const skipFlattening = this._nestedObjects;
|
|
174
285
|
for (const [path, type] of this._flatMap.entries()) {
|
|
175
286
|
if (!path) continue;
|
|
287
|
+
if (!skipFlattening && this._flattenedParents.has(path)) continue;
|
|
288
|
+
if (!skipFlattening && this._findAncestorInSet(path, this._jsonFields) !== undefined) continue;
|
|
289
|
+
const isJson = this._jsonFields.has(path);
|
|
290
|
+
const isFlattened = !skipFlattening && this._findAncestorInSet(path, this._flattenedParents) !== undefined;
|
|
291
|
+
const designType = isJson ? "json" : resolveDesignType(type);
|
|
292
|
+
let storage;
|
|
293
|
+
if (skipFlattening) storage = "column";
|
|
294
|
+
else if (isJson) storage = "json";
|
|
295
|
+
else if (isFlattened) storage = "flattened";
|
|
296
|
+
else storage = "column";
|
|
297
|
+
const physicalName = skipFlattening ? this._columnMap.get(path) ?? path : this._pathToPhysical.get(path) ?? this._columnMap.get(path) ?? path;
|
|
176
298
|
this._fieldDescriptors.push({
|
|
177
299
|
path,
|
|
178
300
|
type,
|
|
179
|
-
physicalName
|
|
180
|
-
designType
|
|
301
|
+
physicalName,
|
|
302
|
+
designType,
|
|
181
303
|
optional: type.optional === true,
|
|
182
304
|
isPrimaryKey: this._primaryKeys.includes(path),
|
|
183
305
|
ignored: this._ignoredFields.has(path),
|
|
184
|
-
defaultValue: this._defaults.get(path)
|
|
306
|
+
defaultValue: this._defaults.get(path),
|
|
307
|
+
storage,
|
|
308
|
+
flattenedFrom: isFlattened ? path : undefined
|
|
185
309
|
});
|
|
186
310
|
}
|
|
311
|
+
Object.freeze(this._fieldDescriptors);
|
|
187
312
|
}
|
|
188
313
|
return this._fieldDescriptors;
|
|
189
314
|
}
|
|
190
315
|
/**
|
|
191
|
-
* Resolves `$select` from {@link UniqueryControls} to a list of field names.
|
|
192
|
-
* - `undefined` → `undefined` (all fields)
|
|
193
|
-
* - `string[]` → pass through
|
|
194
|
-
* - `Record<K, 1>` → extract included keys
|
|
195
|
-
* - `Record<K, 0>` → invert using known field names
|
|
196
|
-
*/ resolveProjection(select) {
|
|
197
|
-
if (!select) return undefined;
|
|
198
|
-
if (Array.isArray(select)) return select.length > 0 ? select : undefined;
|
|
199
|
-
const entries = Object.entries(select);
|
|
200
|
-
if (entries.length === 0) return undefined;
|
|
201
|
-
const firstVal = entries[0][1];
|
|
202
|
-
if (firstVal === 1) return entries.filter(([, v]) => v === 1).map(([k]) => k);
|
|
203
|
-
const excluded = new Set(entries.filter(([, v]) => v === 0).map(([k]) => k));
|
|
204
|
-
return this.fieldDescriptors.filter((f) => !f.ignored && !excluded.has(f.path)).map((f) => f.path);
|
|
205
|
-
}
|
|
206
|
-
/**
|
|
207
316
|
* Creates a new validator with custom options.
|
|
208
317
|
* Adapter plugins are NOT automatically included — use {@link getValidator}
|
|
209
318
|
* for the standard validator with adapter plugins.
|
|
@@ -266,9 +375,9 @@ var AtscriptDbTable = class {
|
|
|
266
375
|
const validator = this.getValidator("patch");
|
|
267
376
|
if (!validator.validate(payload)) throw new Error("Validation failed for update");
|
|
268
377
|
const filter = this._extractPrimaryKeyFilter(payload);
|
|
269
|
-
if (this.adapter.supportsNativePatch()) return this.adapter.nativePatch(filter, payload);
|
|
378
|
+
if (this.adapter.supportsNativePatch()) return this.adapter.nativePatch(this._translateFilter(filter), payload);
|
|
270
379
|
const update = decomposePatch(payload, this);
|
|
271
|
-
return this.adapter.updateOne(filter, update);
|
|
380
|
+
return this.adapter.updateOne(this._translateFilter(filter), this._translatePatchKeys(update));
|
|
272
381
|
}
|
|
273
382
|
/**
|
|
274
383
|
* Deletes a single record by primary key value.
|
|
@@ -288,35 +397,145 @@ var AtscriptDbTable = class {
|
|
|
288
397
|
filter[field] = fieldType ? this.adapter.prepareId(idObj[field], fieldType) : idObj[field];
|
|
289
398
|
}
|
|
290
399
|
}
|
|
291
|
-
return this.adapter.deleteOne(filter);
|
|
400
|
+
return this.adapter.deleteOne(this._translateFilter(filter));
|
|
292
401
|
}
|
|
293
402
|
/**
|
|
294
403
|
* Finds a single record matching the query.
|
|
295
404
|
*/ async findOne(query) {
|
|
296
|
-
|
|
405
|
+
this._flatten();
|
|
406
|
+
const translatedQuery = this._translateQuery(query);
|
|
407
|
+
const result = await this.adapter.findOne(translatedQuery);
|
|
408
|
+
return result ? this._reconstructFromRead(result) : null;
|
|
297
409
|
}
|
|
298
410
|
/**
|
|
299
411
|
* Finds all records matching the query.
|
|
300
412
|
*/ async findMany(query) {
|
|
301
|
-
|
|
413
|
+
this._flatten();
|
|
414
|
+
const translatedQuery = this._translateQuery(query);
|
|
415
|
+
const results = await this.adapter.findMany(translatedQuery);
|
|
416
|
+
return results.map((row) => this._reconstructFromRead(row));
|
|
302
417
|
}
|
|
303
418
|
/**
|
|
304
419
|
* Counts records matching the query.
|
|
305
420
|
*/ async count(query) {
|
|
421
|
+
this._flatten();
|
|
306
422
|
query ?? (query = {
|
|
307
423
|
filter: {},
|
|
308
424
|
controls: {}
|
|
309
425
|
});
|
|
310
|
-
return this.adapter.count(query);
|
|
426
|
+
return this.adapter.count(this._translateQuery(query));
|
|
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;
|
|
311
527
|
}
|
|
312
528
|
async updateMany(filter, data) {
|
|
313
|
-
|
|
529
|
+
this._flatten();
|
|
530
|
+
return this.adapter.updateMany(this._translateFilter(filter), this._prepareForWrite({ ...data }));
|
|
314
531
|
}
|
|
315
532
|
async replaceMany(filter, data) {
|
|
316
|
-
|
|
533
|
+
this._flatten();
|
|
534
|
+
return this.adapter.replaceMany(this._translateFilter(filter), this._prepareForWrite({ ...data }));
|
|
317
535
|
}
|
|
318
536
|
async deleteMany(filter) {
|
|
319
|
-
|
|
537
|
+
this._flatten();
|
|
538
|
+
return this.adapter.deleteMany(this._translateFilter(filter));
|
|
320
539
|
}
|
|
321
540
|
/**
|
|
322
541
|
* Synchronizes indexes between Atscript definitions and the database.
|
|
@@ -342,8 +561,12 @@ var AtscriptDbTable = class {
|
|
|
342
561
|
this.adapter.onFieldScanned?.(path, type, metadata);
|
|
343
562
|
}
|
|
344
563
|
});
|
|
564
|
+
if (!this._nestedObjects) this._classifyFields();
|
|
345
565
|
this._finalizeIndexes();
|
|
346
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);
|
|
347
570
|
}
|
|
348
571
|
/**
|
|
349
572
|
* Scans `@db.*` and `@meta.id` annotations on a field during flattening.
|
|
@@ -365,7 +588,7 @@ else if (defaultFn !== undefined) this._defaults.set(fieldName, {
|
|
|
365
588
|
for (const index of metadata.get("db.index.plain") || []) {
|
|
366
589
|
const name = index === true ? fieldName : index?.name || fieldName;
|
|
367
590
|
const sort = (index === true ? undefined : index?.sort) || "asc";
|
|
368
|
-
this._addIndexField("plain", name, fieldName, sort);
|
|
591
|
+
this._addIndexField("plain", name, fieldName, { sort });
|
|
369
592
|
}
|
|
370
593
|
for (const index of metadata.get("db.index.unique") || []) {
|
|
371
594
|
const name = index === true ? fieldName : typeof index === "string" ? index : index?.name || fieldName;
|
|
@@ -373,16 +596,23 @@ else if (defaultFn !== undefined) this._defaults.set(fieldName, {
|
|
|
373
596
|
}
|
|
374
597
|
for (const index of metadata.get("db.index.fulltext") || []) {
|
|
375
598
|
const name = index === true ? fieldName : typeof index === "string" ? index : index?.name || fieldName;
|
|
376
|
-
|
|
599
|
+
const weight = index !== true && typeof index === "object" ? index?.weight : undefined;
|
|
600
|
+
this._addIndexField("fulltext", name, fieldName, { weight });
|
|
601
|
+
}
|
|
602
|
+
if (metadata.has("db.json")) {
|
|
603
|
+
this._jsonFields.add(fieldName);
|
|
604
|
+
const hasIndex = metadata.has("db.index.plain") || metadata.has("db.index.unique") || metadata.has("db.index.fulltext");
|
|
605
|
+
if (hasIndex) this.logger.warn(`@db.index on a @db.json field "${fieldName}" — most databases cannot index into JSON columns`);
|
|
377
606
|
}
|
|
378
607
|
}
|
|
379
|
-
_addIndexField(type, name, field,
|
|
608
|
+
_addIndexField(type, name, field, opts) {
|
|
380
609
|
const key = indexKey(type, name);
|
|
381
610
|
const index = this._indexes.get(key);
|
|
382
611
|
const indexField = {
|
|
383
612
|
name: field,
|
|
384
|
-
sort
|
|
613
|
+
sort: opts?.sort ?? "asc"
|
|
385
614
|
};
|
|
615
|
+
if (opts?.weight !== undefined) indexField.weight = opts.weight;
|
|
386
616
|
if (index) index.fields.push(indexField);
|
|
387
617
|
else this._indexes.set(key, {
|
|
388
618
|
key,
|
|
@@ -391,7 +621,57 @@ else this._indexes.set(key, {
|
|
|
391
621
|
fields: [indexField]
|
|
392
622
|
});
|
|
393
623
|
}
|
|
624
|
+
/**
|
|
625
|
+
* Classifies each field as column, flattened, json, or parent-object.
|
|
626
|
+
* Builds the bidirectional _pathToPhysical / _physicalToPath maps.
|
|
627
|
+
* Only called when the adapter does NOT support nested objects natively.
|
|
628
|
+
*/ _classifyFields() {
|
|
629
|
+
for (const [path, type] of this._flatMap.entries()) {
|
|
630
|
+
if (!path) continue;
|
|
631
|
+
const designType = resolveDesignType(type);
|
|
632
|
+
const isJson = this._jsonFields.has(path);
|
|
633
|
+
const isArray = designType === "array";
|
|
634
|
+
const isObject = designType === "object";
|
|
635
|
+
if (isArray) this._jsonFields.add(path);
|
|
636
|
+
else if (isObject && isJson) {} else if (isObject && !isJson) this._flattenedParents.add(path);
|
|
637
|
+
}
|
|
638
|
+
for (const ignoredField of this._ignoredFields) if (this._flattenedParents.has(ignoredField)) {
|
|
639
|
+
const prefix = `${ignoredField}.`;
|
|
640
|
+
for (const path of this._flatMap.keys()) if (path.startsWith(prefix)) this._ignoredFields.add(path);
|
|
641
|
+
}
|
|
642
|
+
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`);
|
|
643
|
+
for (const [path] of this._flatMap.entries()) {
|
|
644
|
+
if (!path) continue;
|
|
645
|
+
if (this._flattenedParents.has(path)) continue;
|
|
646
|
+
if (this._findAncestorInSet(path, this._jsonFields) !== undefined) continue;
|
|
647
|
+
const isFlattened = this._findAncestorInSet(path, this._flattenedParents) !== undefined;
|
|
648
|
+
const physicalName = this._columnMap.get(path) ?? (isFlattened ? path.replace(/\./g, "__") : path);
|
|
649
|
+
this._pathToPhysical.set(path, physicalName);
|
|
650
|
+
this._physicalToPath.set(physicalName, path);
|
|
651
|
+
const fieldType = this._flatMap?.get(path);
|
|
652
|
+
if (fieldType && resolveDesignType(fieldType) === "boolean") this._booleanFields.add(physicalName);
|
|
653
|
+
}
|
|
654
|
+
for (const parentPath of this._flattenedParents) {
|
|
655
|
+
const prefix = `${parentPath}.`;
|
|
656
|
+
const leaves = [];
|
|
657
|
+
for (const [path, physical] of this._pathToPhysical) if (path.startsWith(prefix)) leaves.push(physical);
|
|
658
|
+
if (leaves.length > 0) this._selectExpansion.set(parentPath, leaves);
|
|
659
|
+
}
|
|
660
|
+
this._requiresMappings = this._flattenedParents.size > 0 || this._jsonFields.size > 0;
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
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) {
|
|
666
|
+
let pos = path.length;
|
|
667
|
+
while ((pos = path.lastIndexOf(".", pos - 1)) !== -1) {
|
|
668
|
+
const ancestor = path.slice(0, pos);
|
|
669
|
+
if (set.has(ancestor)) return ancestor;
|
|
670
|
+
}
|
|
671
|
+
return undefined;
|
|
672
|
+
}
|
|
394
673
|
_finalizeIndexes() {
|
|
674
|
+
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;
|
|
395
675
|
for (const index of this._indexes.values()) if (index.type === "unique" && index.fields.length === 1) this._uniqueProps.add(index.fields[0].name);
|
|
396
676
|
}
|
|
397
677
|
/**
|
|
@@ -399,13 +679,26 @@ else this._indexes.set(key, {
|
|
|
399
679
|
* Called before validation so that defaults satisfy required field constraints.
|
|
400
680
|
*/ _applyDefaults(data) {
|
|
401
681
|
for (const [field, def] of this._defaults.entries()) if (data[field] === undefined) {
|
|
402
|
-
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
|
+
}
|
|
403
696
|
}
|
|
404
697
|
return data;
|
|
405
698
|
}
|
|
406
699
|
/**
|
|
407
700
|
* Prepares a payload for writing to the database:
|
|
408
|
-
* prepares IDs, strips ignored fields, maps column names.
|
|
701
|
+
* prepares IDs, strips ignored fields, flattens nested objects, maps column names.
|
|
409
702
|
* Defaults should be applied before this via `_applyDefaults`.
|
|
410
703
|
*/ _prepareForWrite(payload) {
|
|
411
704
|
const data = { ...payload };
|
|
@@ -413,12 +706,224 @@ else this._indexes.set(key, {
|
|
|
413
706
|
const fieldType = this._flatMap?.get(pk);
|
|
414
707
|
if (fieldType) data[pk] = this.adapter.prepareId(data[pk], fieldType);
|
|
415
708
|
}
|
|
416
|
-
for (const field of this._ignoredFields) delete data[field];
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
709
|
+
for (const field of this._ignoredFields) if (!field.includes(".")) delete data[field];
|
|
710
|
+
if (!this._requiresMappings || this._nestedObjects) {
|
|
711
|
+
for (const [logical, physical] of this._columnMap.entries()) if (logical in data) {
|
|
712
|
+
data[physical] = data[logical];
|
|
713
|
+
delete data[logical];
|
|
714
|
+
}
|
|
715
|
+
return data;
|
|
420
716
|
}
|
|
421
|
-
return data;
|
|
717
|
+
return this._flattenPayload(data);
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Flattens nested object fields into __-separated keys and
|
|
721
|
+
* JSON-stringifies @db.json / array fields.
|
|
722
|
+
* Uses _pathToPhysical for final key names.
|
|
723
|
+
*/ _flattenPayload(data) {
|
|
724
|
+
const result = {};
|
|
725
|
+
for (const key of Object.keys(data)) this._writeFlattenedField(key, data[key], result);
|
|
726
|
+
return result;
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
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);
|
|
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;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* When a parent object is null/undefined, set all its flattened children to null.
|
|
749
|
+
*/ _setFlattenedChildrenNull(parentPath, result) {
|
|
750
|
+
const prefix = `${parentPath}.`;
|
|
751
|
+
for (const [path, physical] of this._pathToPhysical.entries()) if (path.startsWith(prefix)) result[physical] = null;
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Reconstructs nested objects from flat __-separated column values.
|
|
755
|
+
* JSON fields are parsed from strings back to objects/arrays.
|
|
756
|
+
*/ _reconstructFromRead(row) {
|
|
757
|
+
if (!this._requiresMappings || this._nestedObjects) return this._coerceBooleans(row);
|
|
758
|
+
const result = {};
|
|
759
|
+
const rowKeys = Object.keys(row);
|
|
760
|
+
for (const physical of rowKeys) {
|
|
761
|
+
const value = this._booleanFields.has(physical) ? toBool(row[physical]) : row[physical];
|
|
762
|
+
const logicalPath = this._physicalToPath.get(physical);
|
|
763
|
+
if (!logicalPath) {
|
|
764
|
+
result[physical] = value;
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
if (this._jsonFields.has(logicalPath)) {
|
|
768
|
+
const parsed = typeof value === "string" ? JSON.parse(value) : value;
|
|
769
|
+
this._setNestedValue(result, logicalPath, parsed);
|
|
770
|
+
} else if (logicalPath.includes(".")) this._setNestedValue(result, logicalPath, value);
|
|
771
|
+
else result[logicalPath] = value;
|
|
772
|
+
}
|
|
773
|
+
for (const parentPath of this._flattenedParents) this._reconstructNullParent(result, parentPath);
|
|
774
|
+
return result;
|
|
775
|
+
}
|
|
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
|
+
/**
|
|
785
|
+
* Sets a value at a dot-notation path, creating intermediate objects as needed.
|
|
786
|
+
*/ _setNestedValue(obj, dotPath, value) {
|
|
787
|
+
const parts = dotPath.split(".");
|
|
788
|
+
let current = obj;
|
|
789
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
790
|
+
const part = parts[i];
|
|
791
|
+
if (current[part] === undefined || current[part] === null) current[part] = {};
|
|
792
|
+
current = current[part];
|
|
793
|
+
}
|
|
794
|
+
current[parts[parts.length - 1]] = value;
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* If all children of a flattened parent are null, collapse the parent to null.
|
|
798
|
+
*/ _reconstructNullParent(obj, parentPath) {
|
|
799
|
+
const parts = parentPath.split(".");
|
|
800
|
+
let current = obj;
|
|
801
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
802
|
+
if (current[parts[i]] === undefined) return;
|
|
803
|
+
current = current[parts[i]];
|
|
804
|
+
}
|
|
805
|
+
const lastPart = parts[parts.length - 1];
|
|
806
|
+
const parentObj = current[lastPart];
|
|
807
|
+
if (typeof parentObj !== "object" || parentObj === null) return;
|
|
808
|
+
let allNull = true;
|
|
809
|
+
const parentKeys = Object.keys(parentObj);
|
|
810
|
+
for (const k of parentKeys) {
|
|
811
|
+
const v = parentObj[k];
|
|
812
|
+
if (v !== null && v !== undefined) {
|
|
813
|
+
allNull = false;
|
|
814
|
+
break;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
if (allNull) {
|
|
818
|
+
const parentType = this._flatMap?.get(parentPath);
|
|
819
|
+
current[lastPart] = parentType?.optional ? null : {};
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Translates a Uniquery's filter, sort, and projection from logical
|
|
824
|
+
* dot-notation paths to physical column names.
|
|
825
|
+
* Always wraps `$select` in {@link UniquSelect}.
|
|
826
|
+
*/ _translateQuery(query) {
|
|
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
|
+
}
|
|
837
|
+
return {
|
|
838
|
+
filter: this._translateFilter(query.filter),
|
|
839
|
+
controls: query.controls ? this._translateControls(query.controls) : {}
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* Recursively translates field names in a filter expression.
|
|
844
|
+
*/ _translateFilter(filter) {
|
|
845
|
+
if (!filter || typeof filter !== "object") return filter;
|
|
846
|
+
if (!this._requiresMappings) return filter;
|
|
847
|
+
const result = {};
|
|
848
|
+
const filterKeys = Object.keys(filter);
|
|
849
|
+
for (const key of filterKeys) {
|
|
850
|
+
const value = filter[key];
|
|
851
|
+
if (key === "$and" || key === "$or") result[key] = value.map((f) => this._translateFilter(f));
|
|
852
|
+
else if (key === "$not") result[key] = this._translateFilter(value);
|
|
853
|
+
else if (key.startsWith("$")) result[key] = value;
|
|
854
|
+
else {
|
|
855
|
+
const physical = this._pathToPhysical.get(key) ?? key;
|
|
856
|
+
result[physical] = value;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
return result;
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Translates field names in sort and projection controls.
|
|
863
|
+
* Wraps `$select` in {@link UniquSelect} after path translation.
|
|
864
|
+
*/ _translateControls(controls) {
|
|
865
|
+
if (!controls) return {};
|
|
866
|
+
const result = {
|
|
867
|
+
...controls,
|
|
868
|
+
$select: undefined
|
|
869
|
+
};
|
|
870
|
+
if (controls.$sort) {
|
|
871
|
+
const translated = {};
|
|
872
|
+
const sortObj = controls.$sort;
|
|
873
|
+
const sortKeys = Object.keys(sortObj);
|
|
874
|
+
for (const key of sortKeys) {
|
|
875
|
+
if (this._flattenedParents.has(key)) continue;
|
|
876
|
+
const physical = this._pathToPhysical.get(key) ?? key;
|
|
877
|
+
translated[physical] = sortObj[key];
|
|
878
|
+
}
|
|
879
|
+
result.$sort = translated;
|
|
880
|
+
}
|
|
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);
|
|
888
|
+
else expanded.push(this._pathToPhysical.get(key) ?? key);
|
|
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;
|
|
899
|
+
else {
|
|
900
|
+
const physical = this._pathToPhysical.get(key) ?? key;
|
|
901
|
+
translated[physical] = val;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
translatedRaw = translated;
|
|
905
|
+
}
|
|
906
|
+
result.$select = new UniquSelect(translatedRaw, this._allPhysicalFields);
|
|
907
|
+
}
|
|
908
|
+
return result;
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Translates dot-notation keys in a decomposed patch to physical column names.
|
|
912
|
+
*/ _translatePatchKeys(update) {
|
|
913
|
+
if (!this._requiresMappings || this._nestedObjects) return update;
|
|
914
|
+
const result = {};
|
|
915
|
+
const updateKeys = Object.keys(update);
|
|
916
|
+
for (const key of updateKeys) {
|
|
917
|
+
const value = update[key];
|
|
918
|
+
const operatorMatch = key.match(/^(.+?)(\.__\$.+)$/);
|
|
919
|
+
const basePath = operatorMatch ? operatorMatch[1] : key;
|
|
920
|
+
const suffix = operatorMatch ? operatorMatch[2] : "";
|
|
921
|
+
const physical = this._pathToPhysical.get(basePath) ?? basePath;
|
|
922
|
+
const finalKey = physical + suffix;
|
|
923
|
+
if (this._jsonFields.has(basePath) && typeof value === "object" && value !== null && !suffix) result[finalKey] = JSON.stringify(value);
|
|
924
|
+
else result[finalKey] = value;
|
|
925
|
+
}
|
|
926
|
+
return result;
|
|
422
927
|
}
|
|
423
928
|
/**
|
|
424
929
|
* Extracts primary key field(s) from a payload to build a filter.
|
|
@@ -438,21 +943,26 @@ else this._indexes.set(key, {
|
|
|
438
943
|
*/ _buildValidator(purpose) {
|
|
439
944
|
const plugins = this.adapter.getValidatorPlugins();
|
|
440
945
|
switch (purpose) {
|
|
441
|
-
case "insert":
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
+
}
|
|
456
966
|
default: return this.createValidator({ plugins });
|
|
457
967
|
}
|
|
458
968
|
}
|
|
@@ -470,6 +980,15 @@ else this._indexes.set(key, {
|
|
|
470
980
|
_define_property$1(this, "_defaults", void 0);
|
|
471
981
|
_define_property$1(this, "_ignoredFields", void 0);
|
|
472
982
|
_define_property$1(this, "_uniqueProps", void 0);
|
|
983
|
+
/** Logical dot-path → physical column name. */ _define_property$1(this, "_pathToPhysical", void 0);
|
|
984
|
+
/** Physical column name → logical dot-path (inverse). */ _define_property$1(this, "_physicalToPath", void 0);
|
|
985
|
+
/** Object paths being flattened into __-separated columns (no column themselves). */ _define_property$1(this, "_flattenedParents", void 0);
|
|
986
|
+
/** Fields stored as JSON (@db.json + array fields). */ _define_property$1(this, "_jsonFields", void 0);
|
|
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);
|
|
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);
|
|
473
992
|
_define_property$1(this, "validators", void 0);
|
|
474
993
|
this._type = _type;
|
|
475
994
|
this.adapter = adapter;
|
|
@@ -480,6 +999,14 @@ else this._indexes.set(key, {
|
|
|
480
999
|
this._defaults = new Map();
|
|
481
1000
|
this._ignoredFields = new Set();
|
|
482
1001
|
this._uniqueProps = new Set();
|
|
1002
|
+
this._pathToPhysical = new Map();
|
|
1003
|
+
this._physicalToPath = new Map();
|
|
1004
|
+
this._flattenedParents = new Set();
|
|
1005
|
+
this._jsonFields = new Set();
|
|
1006
|
+
this._selectExpansion = new Map();
|
|
1007
|
+
this._booleanFields = new Set();
|
|
1008
|
+
this._requiresMappings = false;
|
|
1009
|
+
this._allPhysicalFields = [];
|
|
483
1010
|
this.validators = new Map();
|
|
484
1011
|
if (!(0, __atscript_typescript_utils.isAnnotatedType)(_type)) throw new Error("Atscript Annotated Type expected");
|
|
485
1012
|
if (_type.type.kind !== "object") throw new Error("Database table type must be an object type");
|
|
@@ -489,6 +1016,7 @@ else this._indexes.set(key, {
|
|
|
489
1016
|
this.tableName = adapterName || dbTable || fallbackName;
|
|
490
1017
|
if (!this.tableName) throw new Error("@db.table annotation or adapter-specific table name expected");
|
|
491
1018
|
this.schema = _type.metadata.get("db.schema");
|
|
1019
|
+
this._nestedObjects = adapter.supportsNestedObjects();
|
|
492
1020
|
adapter.registerTable(this);
|
|
493
1021
|
}
|
|
494
1022
|
};
|
|
@@ -539,6 +1067,14 @@ var BaseDbAdapter = class {
|
|
|
539
1067
|
return false;
|
|
540
1068
|
}
|
|
541
1069
|
/**
|
|
1070
|
+
* Whether this adapter handles nested objects natively.
|
|
1071
|
+
* When `true`, the generic layer skips flattening and
|
|
1072
|
+
* passes nested objects as-is to the adapter.
|
|
1073
|
+
* MongoDB returns `true`; relational adapters return `false` (default).
|
|
1074
|
+
*/ supportsNestedObjects() {
|
|
1075
|
+
return false;
|
|
1076
|
+
}
|
|
1077
|
+
/**
|
|
542
1078
|
* Applies a patch payload using native database operations.
|
|
543
1079
|
* Only called when {@link supportsNativePatch} returns `true`.
|
|
544
1080
|
*
|
|
@@ -586,6 +1122,46 @@ var BaseDbAdapter = class {
|
|
|
586
1122
|
}
|
|
587
1123
|
for (const name of existingNames) if (!desiredNames.has(name)) await opts.dropIndex(name);
|
|
588
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
|
+
}
|
|
589
1165
|
constructor() {
|
|
590
1166
|
_define_property(this, "_table", void 0);
|
|
591
1167
|
}
|
|
@@ -595,6 +1171,7 @@ var BaseDbAdapter = class {
|
|
|
595
1171
|
exports.AtscriptDbTable = AtscriptDbTable
|
|
596
1172
|
exports.BaseDbAdapter = BaseDbAdapter
|
|
597
1173
|
exports.NoopLogger = NoopLogger
|
|
1174
|
+
exports.UniquSelect = UniquSelect
|
|
598
1175
|
exports.decomposePatch = decomposePatch
|
|
599
1176
|
exports.getKeyProps = getKeyProps
|
|
600
1177
|
Object.defineProperty(exports, 'isPrimitive', {
|