@atscript/utils-db 0.1.29 → 0.1.31
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 +354 -28
- package/dist/index.d.ts +107 -11
- package/dist/index.mjs +354 -28
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -245,9 +245,29 @@ These `@db.*` annotations are defined in `@atscript/core` and processed by `Atsc
|
|
|
245
245
|
| `@db.index.plain "name"` | Field | B-tree index (optional sort: `"name", "desc"`) |
|
|
246
246
|
| `@db.index.unique "name"` | Field | Unique index |
|
|
247
247
|
| `@db.index.fulltext "name"` | Field | Full-text search index |
|
|
248
|
+
| `@db.json` | Field | Store as a single JSON column (skip flattening) |
|
|
248
249
|
|
|
249
250
|
Multiple fields with the same index name form a **composite index**.
|
|
250
251
|
|
|
252
|
+
## Type-Safe Queries with `__flat`
|
|
253
|
+
|
|
254
|
+
Interfaces annotated with `@db.table` get a `__flat` static property in the generated `.d.ts` file, mapping all dot-notation paths to their value types. This enables autocomplete and type checking for filter expressions and `$select`/`$sort` operations.
|
|
255
|
+
|
|
256
|
+
Use `FlatOf<T>` to extract the flat type:
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
import type { FlatOf } from '@atscript/utils-db'
|
|
260
|
+
|
|
261
|
+
type UserFlat = FlatOf<typeof User>
|
|
262
|
+
// → { id: number; name: string; contact: never; "contact.email": string; ... }
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
`AtscriptDbTable` uses `FlatOf<T>` as the type parameter for query methods (`findOne`, `findMany`, `count`, `updateMany`, `replaceMany`, `deleteMany`), giving you autocomplete on filter keys and select paths. When `__flat` is not present (no `@db.table`), `FlatOf<T>` falls back to the regular data shape — fully backward-compatible.
|
|
266
|
+
|
|
267
|
+
### `$select` Parent Path Expansion
|
|
268
|
+
|
|
269
|
+
Selecting a parent object path (e.g., `$select: ['contact']`) automatically expands to all its leaf physical columns for relational databases. Sorting by parent paths is silently ignored.
|
|
270
|
+
|
|
251
271
|
## Cross-Cutting Concerns
|
|
252
272
|
|
|
253
273
|
Since `AtscriptDbTable` is concrete, extend it for cross-cutting concerns that work with any adapter:
|
|
@@ -305,6 +325,7 @@ export { decomposePatch } from './patch-decomposer'
|
|
|
305
325
|
export { getKeyProps } from './patch-types'
|
|
306
326
|
|
|
307
327
|
// Types
|
|
328
|
+
export type { FlatOf } from '@atscript/typescript/utils'
|
|
308
329
|
export type {
|
|
309
330
|
TDbFilter, TDbFindOptions,
|
|
310
331
|
TDbInsertResult, TDbInsertManyResult, TDbUpdateResult, TDbDeleteResult,
|
package/dist/index.cjs
CHANGED
|
@@ -156,6 +156,14 @@ var AtscriptDbTable = class {
|
|
|
156
156
|
this._flatten();
|
|
157
157
|
return this._uniqueProps;
|
|
158
158
|
}
|
|
159
|
+
/** Precomputed logical dot-path → physical column name map. */ get pathToPhysical() {
|
|
160
|
+
this._flatten();
|
|
161
|
+
return this._pathToPhysical;
|
|
162
|
+
}
|
|
163
|
+
/** Precomputed physical column name → logical dot-path map (inverse). */ get physicalToPath() {
|
|
164
|
+
this._flatten();
|
|
165
|
+
return this._physicalToPath;
|
|
166
|
+
}
|
|
159
167
|
/** Descriptor for the primary ID field(s). */ getIdDescriptor() {
|
|
160
168
|
this._flatten();
|
|
161
169
|
return {
|
|
@@ -171,17 +179,31 @@ var AtscriptDbTable = class {
|
|
|
171
179
|
this._flatten();
|
|
172
180
|
if (!this._fieldDescriptors) {
|
|
173
181
|
this._fieldDescriptors = [];
|
|
182
|
+
const skipFlattening = this.adapter.supportsNestedObjects();
|
|
174
183
|
for (const [path, type] of this._flatMap.entries()) {
|
|
175
184
|
if (!path) continue;
|
|
185
|
+
if (!skipFlattening && this._flattenedParents.has(path)) continue;
|
|
186
|
+
if (!skipFlattening && this._findJsonParent(path) !== undefined) continue;
|
|
187
|
+
const isJson = this._jsonFields.has(path);
|
|
188
|
+
const isFlattened = !skipFlattening && this._findFlattenedParent(path) !== undefined;
|
|
189
|
+
const designType = isJson ? "json" : resolveDesignType(type);
|
|
190
|
+
let storage;
|
|
191
|
+
if (skipFlattening) storage = "column";
|
|
192
|
+
else if (isJson) storage = "json";
|
|
193
|
+
else if (isFlattened) storage = "flattened";
|
|
194
|
+
else storage = "column";
|
|
195
|
+
const physicalName = skipFlattening ? this._columnMap.get(path) ?? path : this._pathToPhysical.get(path) ?? this._columnMap.get(path) ?? path;
|
|
176
196
|
this._fieldDescriptors.push({
|
|
177
197
|
path,
|
|
178
198
|
type,
|
|
179
|
-
physicalName
|
|
180
|
-
designType
|
|
199
|
+
physicalName,
|
|
200
|
+
designType,
|
|
181
201
|
optional: type.optional === true,
|
|
182
202
|
isPrimaryKey: this._primaryKeys.includes(path),
|
|
183
203
|
ignored: this._ignoredFields.has(path),
|
|
184
|
-
defaultValue: this._defaults.get(path)
|
|
204
|
+
defaultValue: this._defaults.get(path),
|
|
205
|
+
storage,
|
|
206
|
+
flattenedFrom: isFlattened ? path : undefined
|
|
185
207
|
});
|
|
186
208
|
}
|
|
187
209
|
}
|
|
@@ -196,12 +218,19 @@ var AtscriptDbTable = class {
|
|
|
196
218
|
*/ resolveProjection(select) {
|
|
197
219
|
if (!select) return undefined;
|
|
198
220
|
if (Array.isArray(select)) return select.length > 0 ? select : undefined;
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if (
|
|
203
|
-
|
|
204
|
-
|
|
221
|
+
const selectObj = select;
|
|
222
|
+
const keys = Object.keys(selectObj);
|
|
223
|
+
if (keys.length === 0) return undefined;
|
|
224
|
+
if (selectObj[keys[0]] === 1) {
|
|
225
|
+
const result$1 = [];
|
|
226
|
+
for (const k of keys) if (selectObj[k] === 1) result$1.push(k);
|
|
227
|
+
return result$1;
|
|
228
|
+
}
|
|
229
|
+
const excluded = new Set();
|
|
230
|
+
for (const k of keys) if (selectObj[k] === 0) excluded.add(k);
|
|
231
|
+
const result = [];
|
|
232
|
+
for (const f of this.fieldDescriptors) if (!f.ignored && !excluded.has(f.path)) result.push(f.path);
|
|
233
|
+
return result;
|
|
205
234
|
}
|
|
206
235
|
/**
|
|
207
236
|
* Creates a new validator with custom options.
|
|
@@ -266,9 +295,9 @@ var AtscriptDbTable = class {
|
|
|
266
295
|
const validator = this.getValidator("patch");
|
|
267
296
|
if (!validator.validate(payload)) throw new Error("Validation failed for update");
|
|
268
297
|
const filter = this._extractPrimaryKeyFilter(payload);
|
|
269
|
-
if (this.adapter.supportsNativePatch()) return this.adapter.nativePatch(filter, payload);
|
|
298
|
+
if (this.adapter.supportsNativePatch()) return this.adapter.nativePatch(this._translateFilter(filter), payload);
|
|
270
299
|
const update = decomposePatch(payload, this);
|
|
271
|
-
return this.adapter.updateOne(filter, update);
|
|
300
|
+
return this.adapter.updateOne(this._translateFilter(filter), this._translatePatchKeys(update));
|
|
272
301
|
}
|
|
273
302
|
/**
|
|
274
303
|
* Deletes a single record by primary key value.
|
|
@@ -288,34 +317,45 @@ var AtscriptDbTable = class {
|
|
|
288
317
|
filter[field] = fieldType ? this.adapter.prepareId(idObj[field], fieldType) : idObj[field];
|
|
289
318
|
}
|
|
290
319
|
}
|
|
291
|
-
return this.adapter.deleteOne(filter);
|
|
320
|
+
return this.adapter.deleteOne(this._translateFilter(filter));
|
|
292
321
|
}
|
|
293
322
|
/**
|
|
294
323
|
* Finds a single record matching the query.
|
|
295
324
|
*/ async findOne(query) {
|
|
296
|
-
|
|
325
|
+
this._flatten();
|
|
326
|
+
const translatedQuery = this._translateQuery(query);
|
|
327
|
+
const result = await this.adapter.findOne(translatedQuery);
|
|
328
|
+
return result ? this._reconstructFromRead(result) : null;
|
|
297
329
|
}
|
|
298
330
|
/**
|
|
299
331
|
* Finds all records matching the query.
|
|
300
332
|
*/ async findMany(query) {
|
|
301
|
-
|
|
333
|
+
this._flatten();
|
|
334
|
+
const translatedQuery = this._translateQuery(query);
|
|
335
|
+
const results = await this.adapter.findMany(translatedQuery);
|
|
336
|
+
return results.map((row) => this._reconstructFromRead(row));
|
|
302
337
|
}
|
|
303
338
|
/**
|
|
304
339
|
* Counts records matching the query.
|
|
305
|
-
*/ async count(query
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
340
|
+
*/ async count(query) {
|
|
341
|
+
this._flatten();
|
|
342
|
+
query ?? (query = {
|
|
343
|
+
filter: {},
|
|
344
|
+
controls: {}
|
|
345
|
+
});
|
|
346
|
+
return this.adapter.count(this._translateQuery(query));
|
|
310
347
|
}
|
|
311
348
|
async updateMany(filter, data) {
|
|
312
|
-
|
|
349
|
+
this._flatten();
|
|
350
|
+
return this.adapter.updateMany(this._translateFilter(filter), this._prepareForWrite({ ...data }));
|
|
313
351
|
}
|
|
314
352
|
async replaceMany(filter, data) {
|
|
315
|
-
|
|
353
|
+
this._flatten();
|
|
354
|
+
return this.adapter.replaceMany(this._translateFilter(filter), this._prepareForWrite({ ...data }));
|
|
316
355
|
}
|
|
317
356
|
async deleteMany(filter) {
|
|
318
|
-
|
|
357
|
+
this._flatten();
|
|
358
|
+
return this.adapter.deleteMany(this._translateFilter(filter));
|
|
319
359
|
}
|
|
320
360
|
/**
|
|
321
361
|
* Synchronizes indexes between Atscript definitions and the database.
|
|
@@ -341,6 +381,7 @@ var AtscriptDbTable = class {
|
|
|
341
381
|
this.adapter.onFieldScanned?.(path, type, metadata);
|
|
342
382
|
}
|
|
343
383
|
});
|
|
384
|
+
if (!this.adapter.supportsNestedObjects()) this._classifyFields();
|
|
344
385
|
this._finalizeIndexes();
|
|
345
386
|
this.adapter.onAfterFlatten?.();
|
|
346
387
|
}
|
|
@@ -374,6 +415,11 @@ else if (defaultFn !== undefined) this._defaults.set(fieldName, {
|
|
|
374
415
|
const name = index === true ? fieldName : typeof index === "string" ? index : index?.name || fieldName;
|
|
375
416
|
this._addIndexField("fulltext", name, fieldName);
|
|
376
417
|
}
|
|
418
|
+
if (metadata.has("db.json")) {
|
|
419
|
+
this._jsonFields.add(fieldName);
|
|
420
|
+
const hasIndex = metadata.has("db.index.plain") || metadata.has("db.index.unique") || metadata.has("db.index.fulltext");
|
|
421
|
+
if (hasIndex) this.logger.warn(`@db.index on a @db.json field "${fieldName}" — most databases cannot index into JSON columns`);
|
|
422
|
+
}
|
|
377
423
|
}
|
|
378
424
|
_addIndexField(type, name, field, sort = "asc") {
|
|
379
425
|
const key = indexKey(type, name);
|
|
@@ -390,7 +436,66 @@ else this._indexes.set(key, {
|
|
|
390
436
|
fields: [indexField]
|
|
391
437
|
});
|
|
392
438
|
}
|
|
439
|
+
/**
|
|
440
|
+
* Classifies each field as column, flattened, json, or parent-object.
|
|
441
|
+
* Builds the bidirectional _pathToPhysical / _physicalToPath maps.
|
|
442
|
+
* Only called when the adapter does NOT support nested objects natively.
|
|
443
|
+
*/ _classifyFields() {
|
|
444
|
+
for (const [path, type] of this._flatMap.entries()) {
|
|
445
|
+
if (!path) continue;
|
|
446
|
+
const designType = resolveDesignType(type);
|
|
447
|
+
const isJson = this._jsonFields.has(path);
|
|
448
|
+
const isArray = designType === "array";
|
|
449
|
+
const isObject = designType === "object";
|
|
450
|
+
if (isArray) this._jsonFields.add(path);
|
|
451
|
+
else if (isObject && isJson) {} else if (isObject && !isJson) this._flattenedParents.add(path);
|
|
452
|
+
}
|
|
453
|
+
for (const ignoredField of this._ignoredFields) if (this._flattenedParents.has(ignoredField)) {
|
|
454
|
+
const prefix = `${ignoredField}.`;
|
|
455
|
+
for (const path of this._flatMap.keys()) if (path.startsWith(prefix)) this._ignoredFields.add(path);
|
|
456
|
+
}
|
|
457
|
+
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`);
|
|
458
|
+
for (const [path] of this._flatMap.entries()) {
|
|
459
|
+
if (!path) continue;
|
|
460
|
+
if (this._flattenedParents.has(path)) continue;
|
|
461
|
+
if (this._findJsonParent(path) !== undefined) continue;
|
|
462
|
+
const isFlattened = this._findFlattenedParent(path) !== undefined;
|
|
463
|
+
const physicalName = this._columnMap.get(path) ?? (isFlattened ? path.replace(/\./g, "__") : path);
|
|
464
|
+
this._pathToPhysical.set(path, physicalName);
|
|
465
|
+
this._physicalToPath.set(physicalName, path);
|
|
466
|
+
}
|
|
467
|
+
for (const parentPath of this._flattenedParents) {
|
|
468
|
+
const prefix = `${parentPath}.`;
|
|
469
|
+
const leaves = [];
|
|
470
|
+
for (const [path, physical] of this._pathToPhysical) if (path.startsWith(prefix)) leaves.push(physical);
|
|
471
|
+
if (leaves.length > 0) this._selectExpansion.set(parentPath, leaves);
|
|
472
|
+
}
|
|
473
|
+
this._requiresMappings = this._flattenedParents.size > 0 || this._jsonFields.size > 0;
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Finds the nearest ancestor that is a flattened parent object.
|
|
477
|
+
* Returns the parent path, or undefined if no ancestor is being flattened.
|
|
478
|
+
*/ _findFlattenedParent(path) {
|
|
479
|
+
let pos = path.length;
|
|
480
|
+
while ((pos = path.lastIndexOf(".", pos - 1)) !== -1) {
|
|
481
|
+
const ancestor = path.slice(0, pos);
|
|
482
|
+
if (this._flattenedParents.has(ancestor)) return ancestor;
|
|
483
|
+
}
|
|
484
|
+
return undefined;
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Finds the nearest ancestor that is a @db.json field.
|
|
488
|
+
* Children of JSON fields don't get their own columns.
|
|
489
|
+
*/ _findJsonParent(path) {
|
|
490
|
+
let pos = path.length;
|
|
491
|
+
while ((pos = path.lastIndexOf(".", pos - 1)) !== -1) {
|
|
492
|
+
const ancestor = path.slice(0, pos);
|
|
493
|
+
if (this._jsonFields.has(ancestor)) return ancestor;
|
|
494
|
+
}
|
|
495
|
+
return undefined;
|
|
496
|
+
}
|
|
393
497
|
_finalizeIndexes() {
|
|
498
|
+
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;
|
|
394
499
|
for (const index of this._indexes.values()) if (index.type === "unique" && index.fields.length === 1) this._uniqueProps.add(index.fields[0].name);
|
|
395
500
|
}
|
|
396
501
|
/**
|
|
@@ -404,7 +509,7 @@ else this._indexes.set(key, {
|
|
|
404
509
|
}
|
|
405
510
|
/**
|
|
406
511
|
* Prepares a payload for writing to the database:
|
|
407
|
-
* prepares IDs, strips ignored fields, maps column names.
|
|
512
|
+
* prepares IDs, strips ignored fields, flattens nested objects, maps column names.
|
|
408
513
|
* Defaults should be applied before this via `_applyDefaults`.
|
|
409
514
|
*/ _prepareForWrite(payload) {
|
|
410
515
|
const data = { ...payload };
|
|
@@ -412,12 +517,213 @@ else this._indexes.set(key, {
|
|
|
412
517
|
const fieldType = this._flatMap?.get(pk);
|
|
413
518
|
if (fieldType) data[pk] = this.adapter.prepareId(data[pk], fieldType);
|
|
414
519
|
}
|
|
415
|
-
for (const field of this._ignoredFields) delete data[field];
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
520
|
+
for (const field of this._ignoredFields) if (!field.includes(".")) delete data[field];
|
|
521
|
+
if (!this._requiresMappings || this.adapter.supportsNestedObjects()) {
|
|
522
|
+
for (const [logical, physical] of this._columnMap.entries()) if (logical in data) {
|
|
523
|
+
data[physical] = data[logical];
|
|
524
|
+
delete data[logical];
|
|
525
|
+
}
|
|
526
|
+
return data;
|
|
419
527
|
}
|
|
420
|
-
return data;
|
|
528
|
+
return this._flattenPayload(data);
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Flattens nested object fields into __-separated keys and
|
|
532
|
+
* JSON-stringifies @db.json / array fields.
|
|
533
|
+
* Uses _pathToPhysical for final key names.
|
|
534
|
+
*/ _flattenPayload(data) {
|
|
535
|
+
const result = {};
|
|
536
|
+
const dataKeys = Object.keys(data);
|
|
537
|
+
for (const key of dataKeys) {
|
|
538
|
+
const value = data[key];
|
|
539
|
+
if (this._flattenedParents.has(key)) {
|
|
540
|
+
if (value === null || value === undefined) this._setFlattenedChildrenNull(key, result);
|
|
541
|
+
else if (typeof value === "object" && !Array.isArray(value)) this._flattenObject(key, value, result);
|
|
542
|
+
} else if (this._jsonFields.has(key)) {
|
|
543
|
+
const physical = this._pathToPhysical.get(key) ?? key;
|
|
544
|
+
result[physical] = value !== undefined && value !== null ? JSON.stringify(value) : value;
|
|
545
|
+
} else {
|
|
546
|
+
const physical = this._pathToPhysical.get(key) ?? key;
|
|
547
|
+
result[physical] = value;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return result;
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Recursively flattens a nested object, writing physical keys to result.
|
|
554
|
+
*/ _flattenObject(prefix, obj, result) {
|
|
555
|
+
const objKeys = Object.keys(obj);
|
|
556
|
+
for (const key of objKeys) {
|
|
557
|
+
const value = obj[key];
|
|
558
|
+
const dotPath = `${prefix}.${key}`;
|
|
559
|
+
if (this._ignoredFields.has(dotPath)) continue;
|
|
560
|
+
if (this._flattenedParents.has(dotPath)) {
|
|
561
|
+
if (value === null || value === undefined) this._setFlattenedChildrenNull(dotPath, result);
|
|
562
|
+
else if (typeof value === "object" && !Array.isArray(value)) this._flattenObject(dotPath, value, result);
|
|
563
|
+
} else if (this._jsonFields.has(dotPath)) {
|
|
564
|
+
const physical = this._pathToPhysical.get(dotPath) ?? dotPath.replace(/\./g, "__");
|
|
565
|
+
result[physical] = value !== undefined && value !== null ? JSON.stringify(value) : value;
|
|
566
|
+
} else {
|
|
567
|
+
const physical = this._pathToPhysical.get(dotPath) ?? dotPath.replace(/\./g, "__");
|
|
568
|
+
result[physical] = value;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* When a parent object is null/undefined, set all its flattened children to null.
|
|
574
|
+
*/ _setFlattenedChildrenNull(parentPath, result) {
|
|
575
|
+
const prefix = `${parentPath}.`;
|
|
576
|
+
for (const [path, physical] of this._pathToPhysical.entries()) if (path.startsWith(prefix)) result[physical] = null;
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Reconstructs nested objects from flat __-separated column values.
|
|
580
|
+
* JSON fields are parsed from strings back to objects/arrays.
|
|
581
|
+
*/ _reconstructFromRead(row) {
|
|
582
|
+
if (!this._requiresMappings || this.adapter.supportsNestedObjects()) return row;
|
|
583
|
+
const result = {};
|
|
584
|
+
const rowKeys = Object.keys(row);
|
|
585
|
+
for (const physical of rowKeys) {
|
|
586
|
+
const value = row[physical];
|
|
587
|
+
const logicalPath = this._physicalToPath.get(physical);
|
|
588
|
+
if (!logicalPath) {
|
|
589
|
+
result[physical] = value;
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
if (this._jsonFields.has(logicalPath)) {
|
|
593
|
+
const parsed = typeof value === "string" ? JSON.parse(value) : value;
|
|
594
|
+
this._setNestedValue(result, logicalPath, parsed);
|
|
595
|
+
} else if (logicalPath.includes(".")) this._setNestedValue(result, logicalPath, value);
|
|
596
|
+
else result[logicalPath] = value;
|
|
597
|
+
}
|
|
598
|
+
for (const parentPath of this._flattenedParents) this._reconstructNullParent(result, parentPath);
|
|
599
|
+
return result;
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Sets a value at a dot-notation path, creating intermediate objects as needed.
|
|
603
|
+
*/ _setNestedValue(obj, dotPath, value) {
|
|
604
|
+
const parts = dotPath.split(".");
|
|
605
|
+
let current = obj;
|
|
606
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
607
|
+
const part = parts[i];
|
|
608
|
+
if (current[part] === undefined || current[part] === null) current[part] = {};
|
|
609
|
+
current = current[part];
|
|
610
|
+
}
|
|
611
|
+
current[parts[parts.length - 1]] = value;
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* If all children of a flattened parent are null, collapse the parent to null.
|
|
615
|
+
*/ _reconstructNullParent(obj, parentPath) {
|
|
616
|
+
const parts = parentPath.split(".");
|
|
617
|
+
let current = obj;
|
|
618
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
619
|
+
if (current[parts[i]] === undefined) return;
|
|
620
|
+
current = current[parts[i]];
|
|
621
|
+
}
|
|
622
|
+
const lastPart = parts[parts.length - 1];
|
|
623
|
+
const parentObj = current[lastPart];
|
|
624
|
+
if (typeof parentObj !== "object" || parentObj === null) return;
|
|
625
|
+
let allNull = true;
|
|
626
|
+
const parentKeys = Object.keys(parentObj);
|
|
627
|
+
for (const k of parentKeys) {
|
|
628
|
+
const v = parentObj[k];
|
|
629
|
+
if (v !== null && v !== undefined) {
|
|
630
|
+
allNull = false;
|
|
631
|
+
break;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
if (allNull) {
|
|
635
|
+
const parentType = this._flatMap?.get(parentPath);
|
|
636
|
+
current[lastPart] = parentType?.optional ? null : {};
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Translates a Uniquery's filter, sort, and projection from logical
|
|
641
|
+
* dot-notation paths to physical column names.
|
|
642
|
+
*/ _translateQuery(query) {
|
|
643
|
+
if (!this._requiresMappings || this.adapter.supportsNestedObjects()) return query;
|
|
644
|
+
return {
|
|
645
|
+
filter: this._translateFilter(query.filter),
|
|
646
|
+
controls: query.controls ? this._translateControls(query.controls) : query.controls,
|
|
647
|
+
insights: query.insights
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Recursively translates field names in a filter expression.
|
|
652
|
+
*/ _translateFilter(filter) {
|
|
653
|
+
if (!filter || typeof filter !== "object") return filter;
|
|
654
|
+
if (!this._requiresMappings) return filter;
|
|
655
|
+
const result = {};
|
|
656
|
+
const filterKeys = Object.keys(filter);
|
|
657
|
+
for (const key of filterKeys) {
|
|
658
|
+
const value = filter[key];
|
|
659
|
+
if (key === "$and" || key === "$or") result[key] = value.map((f) => this._translateFilter(f));
|
|
660
|
+
else if (key === "$not") result[key] = this._translateFilter(value);
|
|
661
|
+
else if (key.startsWith("$")) result[key] = value;
|
|
662
|
+
else {
|
|
663
|
+
const physical = this._pathToPhysical.get(key) ?? key;
|
|
664
|
+
result[physical] = value;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
return result;
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Translates field names in sort and projection controls.
|
|
671
|
+
*/ _translateControls(controls) {
|
|
672
|
+
if (!controls) return controls;
|
|
673
|
+
const result = { ...controls };
|
|
674
|
+
if (controls.$sort) {
|
|
675
|
+
const translated = {};
|
|
676
|
+
const sortObj = controls.$sort;
|
|
677
|
+
const sortKeys = Object.keys(sortObj);
|
|
678
|
+
for (const key of sortKeys) {
|
|
679
|
+
if (this._flattenedParents.has(key)) continue;
|
|
680
|
+
const physical = this._pathToPhysical.get(key) ?? key;
|
|
681
|
+
translated[physical] = sortObj[key];
|
|
682
|
+
}
|
|
683
|
+
result.$sort = translated;
|
|
684
|
+
}
|
|
685
|
+
if (controls.$select) if (Array.isArray(controls.$select)) {
|
|
686
|
+
const expanded = [];
|
|
687
|
+
for (const key of controls.$select) {
|
|
688
|
+
const expansion = this._selectExpansion.get(key);
|
|
689
|
+
if (expansion) expanded.push(...expansion);
|
|
690
|
+
else expanded.push(this._pathToPhysical.get(key) ?? key);
|
|
691
|
+
}
|
|
692
|
+
result.$select = expanded;
|
|
693
|
+
} else {
|
|
694
|
+
const translated = {};
|
|
695
|
+
const selectObj = controls.$select;
|
|
696
|
+
const selectKeys = Object.keys(selectObj);
|
|
697
|
+
for (const key of selectKeys) {
|
|
698
|
+
const val = selectObj[key];
|
|
699
|
+
const expansion = this._selectExpansion.get(key);
|
|
700
|
+
if (expansion) for (const leaf of expansion) translated[leaf] = val;
|
|
701
|
+
else {
|
|
702
|
+
const physical = this._pathToPhysical.get(key) ?? key;
|
|
703
|
+
translated[physical] = val;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
result.$select = translated;
|
|
707
|
+
}
|
|
708
|
+
return result;
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Translates dot-notation keys in a decomposed patch to physical column names.
|
|
712
|
+
*/ _translatePatchKeys(update) {
|
|
713
|
+
if (!this._requiresMappings || this.adapter.supportsNestedObjects()) return update;
|
|
714
|
+
const result = {};
|
|
715
|
+
const updateKeys = Object.keys(update);
|
|
716
|
+
for (const key of updateKeys) {
|
|
717
|
+
const value = update[key];
|
|
718
|
+
const operatorMatch = key.match(/^(.+?)(\.__\$.+)$/);
|
|
719
|
+
const basePath = operatorMatch ? operatorMatch[1] : key;
|
|
720
|
+
const suffix = operatorMatch ? operatorMatch[2] : "";
|
|
721
|
+
const physical = this._pathToPhysical.get(basePath) ?? basePath;
|
|
722
|
+
const finalKey = physical + suffix;
|
|
723
|
+
if (this._jsonFields.has(basePath) && typeof value === "object" && value !== null && !suffix) result[finalKey] = JSON.stringify(value);
|
|
724
|
+
else result[finalKey] = value;
|
|
725
|
+
}
|
|
726
|
+
return result;
|
|
421
727
|
}
|
|
422
728
|
/**
|
|
423
729
|
* Extracts primary key field(s) from a payload to build a filter.
|
|
@@ -469,6 +775,12 @@ else this._indexes.set(key, {
|
|
|
469
775
|
_define_property$1(this, "_defaults", void 0);
|
|
470
776
|
_define_property$1(this, "_ignoredFields", void 0);
|
|
471
777
|
_define_property$1(this, "_uniqueProps", void 0);
|
|
778
|
+
/** Logical dot-path → physical column name. */ _define_property$1(this, "_pathToPhysical", void 0);
|
|
779
|
+
/** Physical column name → logical dot-path (inverse). */ _define_property$1(this, "_physicalToPath", void 0);
|
|
780
|
+
/** Object paths being flattened into __-separated columns (no column themselves). */ _define_property$1(this, "_flattenedParents", void 0);
|
|
781
|
+
/** Fields stored as JSON (@db.json + array fields). */ _define_property$1(this, "_jsonFields", void 0);
|
|
782
|
+
/** Intermediate paths → their leaf physical column names (for $select expansion in relational DBs). */ _define_property$1(this, "_selectExpansion", void 0);
|
|
783
|
+
/** Fast-path flag: skip all mapping when no nested/json fields exist. */ _define_property$1(this, "_requiresMappings", void 0);
|
|
472
784
|
_define_property$1(this, "validators", void 0);
|
|
473
785
|
this._type = _type;
|
|
474
786
|
this.adapter = adapter;
|
|
@@ -479,6 +791,12 @@ else this._indexes.set(key, {
|
|
|
479
791
|
this._defaults = new Map();
|
|
480
792
|
this._ignoredFields = new Set();
|
|
481
793
|
this._uniqueProps = new Set();
|
|
794
|
+
this._pathToPhysical = new Map();
|
|
795
|
+
this._physicalToPath = new Map();
|
|
796
|
+
this._flattenedParents = new Set();
|
|
797
|
+
this._jsonFields = new Set();
|
|
798
|
+
this._selectExpansion = new Map();
|
|
799
|
+
this._requiresMappings = false;
|
|
482
800
|
this.validators = new Map();
|
|
483
801
|
if (!(0, __atscript_typescript_utils.isAnnotatedType)(_type)) throw new Error("Atscript Annotated Type expected");
|
|
484
802
|
if (_type.type.kind !== "object") throw new Error("Database table type must be an object type");
|
|
@@ -538,6 +856,14 @@ var BaseDbAdapter = class {
|
|
|
538
856
|
return false;
|
|
539
857
|
}
|
|
540
858
|
/**
|
|
859
|
+
* Whether this adapter handles nested objects natively.
|
|
860
|
+
* When `true`, the generic layer skips flattening and
|
|
861
|
+
* passes nested objects as-is to the adapter.
|
|
862
|
+
* MongoDB returns `true`; relational adapters return `false` (default).
|
|
863
|
+
*/ supportsNestedObjects() {
|
|
864
|
+
return false;
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
541
867
|
* Applies a patch payload using native database operations.
|
|
542
868
|
* Only called when {@link supportsNativePatch} returns `true`.
|
|
543
869
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TAtscriptAnnotatedType, TValidatorPlugin, TMetadataMap, TAtscriptDataType, Validator, TAtscriptTypeObject, TValidatorOptions, TAtscriptTypeArray } from '@atscript/typescript/utils';
|
|
1
|
+
import { TAtscriptAnnotatedType, TValidatorPlugin, TMetadataMap, TAtscriptDataType, FlatOf, Validator, TAtscriptTypeObject, TValidatorOptions, TAtscriptTypeArray } from '@atscript/typescript/utils';
|
|
2
2
|
import { FilterExpr, Uniquery, UniqueryControls } from '@uniqu/core';
|
|
3
3
|
export { FieldOpsFor, FilterExpr, FilterVisitor, Uniquery, UniqueryControls, isPrimitive, walkFilter } from '@uniqu/core';
|
|
4
4
|
|
|
@@ -48,9 +48,9 @@ interface TDbFieldMeta {
|
|
|
48
48
|
path: string;
|
|
49
49
|
/** The annotated type for this field. */
|
|
50
50
|
type: TAtscriptAnnotatedType;
|
|
51
|
-
/** Physical column/field name (from @db.column, or same as path). */
|
|
51
|
+
/** Physical column/field name (from @db.column, __-separated for flattened, or same as path). */
|
|
52
52
|
physicalName: string;
|
|
53
|
-
/** Resolved design type: 'string', 'number', 'boolean', 'object', etc. */
|
|
53
|
+
/** Resolved design type: 'string', 'number', 'boolean', 'object', 'json', etc. */
|
|
54
54
|
designType: string;
|
|
55
55
|
/** Whether the field is optional. */
|
|
56
56
|
optional: boolean;
|
|
@@ -60,6 +60,19 @@ interface TDbFieldMeta {
|
|
|
60
60
|
ignored: boolean;
|
|
61
61
|
/** Default value from @db.default.* */
|
|
62
62
|
defaultValue?: TDbDefaultValue;
|
|
63
|
+
/**
|
|
64
|
+
* How this field is stored in the database.
|
|
65
|
+
* - 'column': a standard scalar column (default for primitives)
|
|
66
|
+
* - 'flattened': a leaf scalar from a flattened nested object
|
|
67
|
+
* - 'json': stored as a single JSON column (arrays, @db.json fields)
|
|
68
|
+
*/
|
|
69
|
+
storage: 'column' | 'flattened' | 'json';
|
|
70
|
+
/**
|
|
71
|
+
* For flattened fields: the dot-notation path (same as `path`).
|
|
72
|
+
* E.g., for physicalName 'contact__email', this is 'contact.email'.
|
|
73
|
+
* Undefined for non-flattened fields.
|
|
74
|
+
*/
|
|
75
|
+
flattenedFrom?: string;
|
|
63
76
|
}
|
|
64
77
|
|
|
65
78
|
/**
|
|
@@ -114,6 +127,13 @@ declare abstract class BaseDbAdapter {
|
|
|
114
127
|
* {@link nativePatch} instead of using the generic decomposition.
|
|
115
128
|
*/
|
|
116
129
|
supportsNativePatch(): boolean;
|
|
130
|
+
/**
|
|
131
|
+
* Whether this adapter handles nested objects natively.
|
|
132
|
+
* When `true`, the generic layer skips flattening and
|
|
133
|
+
* passes nested objects as-is to the adapter.
|
|
134
|
+
* MongoDB returns `true`; relational adapters return `false` (default).
|
|
135
|
+
*/
|
|
136
|
+
supportsNestedObjects(): boolean;
|
|
117
137
|
/**
|
|
118
138
|
* Applies a patch payload using native database operations.
|
|
119
139
|
* Only called when {@link supportsNativePatch} returns `true`.
|
|
@@ -247,7 +267,7 @@ declare function resolveDesignType(fieldType: TAtscriptAnnotatedType): string;
|
|
|
247
267
|
* @typeParam T - The Atscript annotated type for this table.
|
|
248
268
|
* @typeParam DataType - The inferred data shape from the annotated type.
|
|
249
269
|
*/
|
|
250
|
-
declare class AtscriptDbTable<T extends TAtscriptAnnotatedType = TAtscriptAnnotatedType, DataType = TAtscriptDataType<T>> {
|
|
270
|
+
declare class AtscriptDbTable<T extends TAtscriptAnnotatedType = TAtscriptAnnotatedType, DataType = TAtscriptDataType<T>, FlatType = FlatOf<T>> {
|
|
251
271
|
protected readonly _type: T;
|
|
252
272
|
protected readonly adapter: BaseDbAdapter;
|
|
253
273
|
protected readonly logger: TGenericLogger;
|
|
@@ -263,6 +283,18 @@ declare class AtscriptDbTable<T extends TAtscriptAnnotatedType = TAtscriptAnnota
|
|
|
263
283
|
protected _defaults: Map<string, TDbDefaultValue>;
|
|
264
284
|
protected _ignoredFields: Set<string>;
|
|
265
285
|
protected _uniqueProps: Set<string>;
|
|
286
|
+
/** Logical dot-path → physical column name. */
|
|
287
|
+
protected _pathToPhysical: Map<string, string>;
|
|
288
|
+
/** Physical column name → logical dot-path (inverse). */
|
|
289
|
+
protected _physicalToPath: Map<string, string>;
|
|
290
|
+
/** Object paths being flattened into __-separated columns (no column themselves). */
|
|
291
|
+
protected _flattenedParents: Set<string>;
|
|
292
|
+
/** Fields stored as JSON (@db.json + array fields). */
|
|
293
|
+
protected _jsonFields: Set<string>;
|
|
294
|
+
/** Intermediate paths → their leaf physical column names (for $select expansion in relational DBs). */
|
|
295
|
+
protected _selectExpansion: Map<string, string[]>;
|
|
296
|
+
/** Fast-path flag: skip all mapping when no nested/json fields exist. */
|
|
297
|
+
protected _requiresMappings: boolean;
|
|
266
298
|
protected readonly validators: Map<string, Validator<T, DataType>>;
|
|
267
299
|
constructor(_type: T, adapter: BaseDbAdapter, logger?: TGenericLogger);
|
|
268
300
|
/** The raw annotated type. */
|
|
@@ -281,6 +313,10 @@ declare class AtscriptDbTable<T extends TAtscriptAnnotatedType = TAtscriptAnnota
|
|
|
281
313
|
get ignoredFields(): ReadonlySet<string>;
|
|
282
314
|
/** Single-field unique index properties. */
|
|
283
315
|
get uniqueProps(): ReadonlySet<string>;
|
|
316
|
+
/** Precomputed logical dot-path → physical column name map. */
|
|
317
|
+
get pathToPhysical(): ReadonlyMap<string, string>;
|
|
318
|
+
/** Precomputed physical column name → logical dot-path map (inverse). */
|
|
319
|
+
get physicalToPath(): ReadonlyMap<string, string>;
|
|
284
320
|
/** Descriptor for the primary ID field(s). */
|
|
285
321
|
getIdDescriptor(): TIdDescriptor;
|
|
286
322
|
/**
|
|
@@ -338,18 +374,18 @@ declare class AtscriptDbTable<T extends TAtscriptAnnotatedType = TAtscriptAnnota
|
|
|
338
374
|
/**
|
|
339
375
|
* Finds a single record matching the query.
|
|
340
376
|
*/
|
|
341
|
-
findOne(query: Uniquery<
|
|
377
|
+
findOne(query: Uniquery<FlatType>): Promise<DataType | null>;
|
|
342
378
|
/**
|
|
343
379
|
* Finds all records matching the query.
|
|
344
380
|
*/
|
|
345
|
-
findMany(query: Uniquery<
|
|
381
|
+
findMany(query: Uniquery<FlatType>): Promise<DataType[]>;
|
|
346
382
|
/**
|
|
347
383
|
* Counts records matching the query.
|
|
348
384
|
*/
|
|
349
|
-
count(query?: Uniquery<
|
|
350
|
-
updateMany(filter: FilterExpr<
|
|
351
|
-
replaceMany(filter: FilterExpr<
|
|
352
|
-
deleteMany(filter: FilterExpr<
|
|
385
|
+
count(query?: Uniquery<FlatType>): Promise<number>;
|
|
386
|
+
updateMany(filter: FilterExpr<FlatType>, data: Partial<DataType> & Record<string, unknown>): Promise<TDbUpdateResult>;
|
|
387
|
+
replaceMany(filter: FilterExpr<FlatType>, data: Record<string, unknown>): Promise<TDbUpdateResult>;
|
|
388
|
+
deleteMany(filter: FilterExpr<FlatType>): Promise<TDbDeleteResult>;
|
|
353
389
|
/**
|
|
354
390
|
* Synchronizes indexes between Atscript definitions and the database.
|
|
355
391
|
* Delegates to the adapter, which uses `this._table.indexes`.
|
|
@@ -365,6 +401,22 @@ declare class AtscriptDbTable<T extends TAtscriptAnnotatedType = TAtscriptAnnota
|
|
|
365
401
|
*/
|
|
366
402
|
private _scanGenericAnnotations;
|
|
367
403
|
protected _addIndexField(type: TDbIndex['type'], name: string, field: string, sort?: 'asc' | 'desc'): void;
|
|
404
|
+
/**
|
|
405
|
+
* Classifies each field as column, flattened, json, or parent-object.
|
|
406
|
+
* Builds the bidirectional _pathToPhysical / _physicalToPath maps.
|
|
407
|
+
* Only called when the adapter does NOT support nested objects natively.
|
|
408
|
+
*/
|
|
409
|
+
private _classifyFields;
|
|
410
|
+
/**
|
|
411
|
+
* Finds the nearest ancestor that is a flattened parent object.
|
|
412
|
+
* Returns the parent path, or undefined if no ancestor is being flattened.
|
|
413
|
+
*/
|
|
414
|
+
private _findFlattenedParent;
|
|
415
|
+
/**
|
|
416
|
+
* Finds the nearest ancestor that is a @db.json field.
|
|
417
|
+
* Children of JSON fields don't get their own columns.
|
|
418
|
+
*/
|
|
419
|
+
private _findJsonParent;
|
|
368
420
|
private _finalizeIndexes;
|
|
369
421
|
/**
|
|
370
422
|
* Applies default values for fields that are missing from the payload.
|
|
@@ -373,10 +425,54 @@ declare class AtscriptDbTable<T extends TAtscriptAnnotatedType = TAtscriptAnnota
|
|
|
373
425
|
protected _applyDefaults(data: Record<string, unknown>): Record<string, unknown>;
|
|
374
426
|
/**
|
|
375
427
|
* Prepares a payload for writing to the database:
|
|
376
|
-
* prepares IDs, strips ignored fields, maps column names.
|
|
428
|
+
* prepares IDs, strips ignored fields, flattens nested objects, maps column names.
|
|
377
429
|
* Defaults should be applied before this via `_applyDefaults`.
|
|
378
430
|
*/
|
|
379
431
|
protected _prepareForWrite(payload: Record<string, unknown>): Record<string, unknown>;
|
|
432
|
+
/**
|
|
433
|
+
* Flattens nested object fields into __-separated keys and
|
|
434
|
+
* JSON-stringifies @db.json / array fields.
|
|
435
|
+
* Uses _pathToPhysical for final key names.
|
|
436
|
+
*/
|
|
437
|
+
private _flattenPayload;
|
|
438
|
+
/**
|
|
439
|
+
* Recursively flattens a nested object, writing physical keys to result.
|
|
440
|
+
*/
|
|
441
|
+
private _flattenObject;
|
|
442
|
+
/**
|
|
443
|
+
* When a parent object is null/undefined, set all its flattened children to null.
|
|
444
|
+
*/
|
|
445
|
+
private _setFlattenedChildrenNull;
|
|
446
|
+
/**
|
|
447
|
+
* Reconstructs nested objects from flat __-separated column values.
|
|
448
|
+
* JSON fields are parsed from strings back to objects/arrays.
|
|
449
|
+
*/
|
|
450
|
+
protected _reconstructFromRead(row: Record<string, unknown>): Record<string, unknown>;
|
|
451
|
+
/**
|
|
452
|
+
* Sets a value at a dot-notation path, creating intermediate objects as needed.
|
|
453
|
+
*/
|
|
454
|
+
private _setNestedValue;
|
|
455
|
+
/**
|
|
456
|
+
* If all children of a flattened parent are null, collapse the parent to null.
|
|
457
|
+
*/
|
|
458
|
+
private _reconstructNullParent;
|
|
459
|
+
/**
|
|
460
|
+
* Translates a Uniquery's filter, sort, and projection from logical
|
|
461
|
+
* dot-notation paths to physical column names.
|
|
462
|
+
*/
|
|
463
|
+
private _translateQuery;
|
|
464
|
+
/**
|
|
465
|
+
* Recursively translates field names in a filter expression.
|
|
466
|
+
*/
|
|
467
|
+
private _translateFilter;
|
|
468
|
+
/**
|
|
469
|
+
* Translates field names in sort and projection controls.
|
|
470
|
+
*/
|
|
471
|
+
private _translateControls;
|
|
472
|
+
/**
|
|
473
|
+
* Translates dot-notation keys in a decomposed patch to physical column names.
|
|
474
|
+
*/
|
|
475
|
+
private _translatePatchKeys;
|
|
380
476
|
/**
|
|
381
477
|
* Extracts primary key field(s) from a payload to build a filter.
|
|
382
478
|
*/
|
package/dist/index.mjs
CHANGED
|
@@ -132,6 +132,14 @@ var AtscriptDbTable = class {
|
|
|
132
132
|
this._flatten();
|
|
133
133
|
return this._uniqueProps;
|
|
134
134
|
}
|
|
135
|
+
/** Precomputed logical dot-path → physical column name map. */ get pathToPhysical() {
|
|
136
|
+
this._flatten();
|
|
137
|
+
return this._pathToPhysical;
|
|
138
|
+
}
|
|
139
|
+
/** Precomputed physical column name → logical dot-path map (inverse). */ get physicalToPath() {
|
|
140
|
+
this._flatten();
|
|
141
|
+
return this._physicalToPath;
|
|
142
|
+
}
|
|
135
143
|
/** Descriptor for the primary ID field(s). */ getIdDescriptor() {
|
|
136
144
|
this._flatten();
|
|
137
145
|
return {
|
|
@@ -147,17 +155,31 @@ var AtscriptDbTable = class {
|
|
|
147
155
|
this._flatten();
|
|
148
156
|
if (!this._fieldDescriptors) {
|
|
149
157
|
this._fieldDescriptors = [];
|
|
158
|
+
const skipFlattening = this.adapter.supportsNestedObjects();
|
|
150
159
|
for (const [path, type] of this._flatMap.entries()) {
|
|
151
160
|
if (!path) continue;
|
|
161
|
+
if (!skipFlattening && this._flattenedParents.has(path)) continue;
|
|
162
|
+
if (!skipFlattening && this._findJsonParent(path) !== undefined) continue;
|
|
163
|
+
const isJson = this._jsonFields.has(path);
|
|
164
|
+
const isFlattened = !skipFlattening && this._findFlattenedParent(path) !== undefined;
|
|
165
|
+
const designType = isJson ? "json" : resolveDesignType(type);
|
|
166
|
+
let storage;
|
|
167
|
+
if (skipFlattening) storage = "column";
|
|
168
|
+
else if (isJson) storage = "json";
|
|
169
|
+
else if (isFlattened) storage = "flattened";
|
|
170
|
+
else storage = "column";
|
|
171
|
+
const physicalName = skipFlattening ? this._columnMap.get(path) ?? path : this._pathToPhysical.get(path) ?? this._columnMap.get(path) ?? path;
|
|
152
172
|
this._fieldDescriptors.push({
|
|
153
173
|
path,
|
|
154
174
|
type,
|
|
155
|
-
physicalName
|
|
156
|
-
designType
|
|
175
|
+
physicalName,
|
|
176
|
+
designType,
|
|
157
177
|
optional: type.optional === true,
|
|
158
178
|
isPrimaryKey: this._primaryKeys.includes(path),
|
|
159
179
|
ignored: this._ignoredFields.has(path),
|
|
160
|
-
defaultValue: this._defaults.get(path)
|
|
180
|
+
defaultValue: this._defaults.get(path),
|
|
181
|
+
storage,
|
|
182
|
+
flattenedFrom: isFlattened ? path : undefined
|
|
161
183
|
});
|
|
162
184
|
}
|
|
163
185
|
}
|
|
@@ -172,12 +194,19 @@ var AtscriptDbTable = class {
|
|
|
172
194
|
*/ resolveProjection(select) {
|
|
173
195
|
if (!select) return undefined;
|
|
174
196
|
if (Array.isArray(select)) return select.length > 0 ? select : undefined;
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (
|
|
179
|
-
|
|
180
|
-
|
|
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;
|
|
181
210
|
}
|
|
182
211
|
/**
|
|
183
212
|
* Creates a new validator with custom options.
|
|
@@ -242,9 +271,9 @@ var AtscriptDbTable = class {
|
|
|
242
271
|
const validator = this.getValidator("patch");
|
|
243
272
|
if (!validator.validate(payload)) throw new Error("Validation failed for update");
|
|
244
273
|
const filter = this._extractPrimaryKeyFilter(payload);
|
|
245
|
-
if (this.adapter.supportsNativePatch()) return this.adapter.nativePatch(filter, payload);
|
|
274
|
+
if (this.adapter.supportsNativePatch()) return this.adapter.nativePatch(this._translateFilter(filter), payload);
|
|
246
275
|
const update = decomposePatch(payload, this);
|
|
247
|
-
return this.adapter.updateOne(filter, update);
|
|
276
|
+
return this.adapter.updateOne(this._translateFilter(filter), this._translatePatchKeys(update));
|
|
248
277
|
}
|
|
249
278
|
/**
|
|
250
279
|
* Deletes a single record by primary key value.
|
|
@@ -264,34 +293,45 @@ var AtscriptDbTable = class {
|
|
|
264
293
|
filter[field] = fieldType ? this.adapter.prepareId(idObj[field], fieldType) : idObj[field];
|
|
265
294
|
}
|
|
266
295
|
}
|
|
267
|
-
return this.adapter.deleteOne(filter);
|
|
296
|
+
return this.adapter.deleteOne(this._translateFilter(filter));
|
|
268
297
|
}
|
|
269
298
|
/**
|
|
270
299
|
* Finds a single record matching the query.
|
|
271
300
|
*/ async findOne(query) {
|
|
272
|
-
|
|
301
|
+
this._flatten();
|
|
302
|
+
const translatedQuery = this._translateQuery(query);
|
|
303
|
+
const result = await this.adapter.findOne(translatedQuery);
|
|
304
|
+
return result ? this._reconstructFromRead(result) : null;
|
|
273
305
|
}
|
|
274
306
|
/**
|
|
275
307
|
* Finds all records matching the query.
|
|
276
308
|
*/ async findMany(query) {
|
|
277
|
-
|
|
309
|
+
this._flatten();
|
|
310
|
+
const translatedQuery = this._translateQuery(query);
|
|
311
|
+
const results = await this.adapter.findMany(translatedQuery);
|
|
312
|
+
return results.map((row) => this._reconstructFromRead(row));
|
|
278
313
|
}
|
|
279
314
|
/**
|
|
280
315
|
* Counts records matching the query.
|
|
281
|
-
*/ async count(query
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
316
|
+
*/ async count(query) {
|
|
317
|
+
this._flatten();
|
|
318
|
+
query ?? (query = {
|
|
319
|
+
filter: {},
|
|
320
|
+
controls: {}
|
|
321
|
+
});
|
|
322
|
+
return this.adapter.count(this._translateQuery(query));
|
|
286
323
|
}
|
|
287
324
|
async updateMany(filter, data) {
|
|
288
|
-
|
|
325
|
+
this._flatten();
|
|
326
|
+
return this.adapter.updateMany(this._translateFilter(filter), this._prepareForWrite({ ...data }));
|
|
289
327
|
}
|
|
290
328
|
async replaceMany(filter, data) {
|
|
291
|
-
|
|
329
|
+
this._flatten();
|
|
330
|
+
return this.adapter.replaceMany(this._translateFilter(filter), this._prepareForWrite({ ...data }));
|
|
292
331
|
}
|
|
293
332
|
async deleteMany(filter) {
|
|
294
|
-
|
|
333
|
+
this._flatten();
|
|
334
|
+
return this.adapter.deleteMany(this._translateFilter(filter));
|
|
295
335
|
}
|
|
296
336
|
/**
|
|
297
337
|
* Synchronizes indexes between Atscript definitions and the database.
|
|
@@ -317,6 +357,7 @@ var AtscriptDbTable = class {
|
|
|
317
357
|
this.adapter.onFieldScanned?.(path, type, metadata);
|
|
318
358
|
}
|
|
319
359
|
});
|
|
360
|
+
if (!this.adapter.supportsNestedObjects()) this._classifyFields();
|
|
320
361
|
this._finalizeIndexes();
|
|
321
362
|
this.adapter.onAfterFlatten?.();
|
|
322
363
|
}
|
|
@@ -350,6 +391,11 @@ else if (defaultFn !== undefined) this._defaults.set(fieldName, {
|
|
|
350
391
|
const name = index === true ? fieldName : typeof index === "string" ? index : index?.name || fieldName;
|
|
351
392
|
this._addIndexField("fulltext", name, fieldName);
|
|
352
393
|
}
|
|
394
|
+
if (metadata.has("db.json")) {
|
|
395
|
+
this._jsonFields.add(fieldName);
|
|
396
|
+
const hasIndex = metadata.has("db.index.plain") || metadata.has("db.index.unique") || metadata.has("db.index.fulltext");
|
|
397
|
+
if (hasIndex) this.logger.warn(`@db.index on a @db.json field "${fieldName}" — most databases cannot index into JSON columns`);
|
|
398
|
+
}
|
|
353
399
|
}
|
|
354
400
|
_addIndexField(type, name, field, sort = "asc") {
|
|
355
401
|
const key = indexKey(type, name);
|
|
@@ -366,7 +412,66 @@ else this._indexes.set(key, {
|
|
|
366
412
|
fields: [indexField]
|
|
367
413
|
});
|
|
368
414
|
}
|
|
415
|
+
/**
|
|
416
|
+
* Classifies each field as column, flattened, json, or parent-object.
|
|
417
|
+
* Builds the bidirectional _pathToPhysical / _physicalToPath maps.
|
|
418
|
+
* Only called when the adapter does NOT support nested objects natively.
|
|
419
|
+
*/ _classifyFields() {
|
|
420
|
+
for (const [path, type] of this._flatMap.entries()) {
|
|
421
|
+
if (!path) continue;
|
|
422
|
+
const designType = resolveDesignType(type);
|
|
423
|
+
const isJson = this._jsonFields.has(path);
|
|
424
|
+
const isArray = designType === "array";
|
|
425
|
+
const isObject = designType === "object";
|
|
426
|
+
if (isArray) this._jsonFields.add(path);
|
|
427
|
+
else if (isObject && isJson) {} else if (isObject && !isJson) this._flattenedParents.add(path);
|
|
428
|
+
}
|
|
429
|
+
for (const ignoredField of this._ignoredFields) if (this._flattenedParents.has(ignoredField)) {
|
|
430
|
+
const prefix = `${ignoredField}.`;
|
|
431
|
+
for (const path of this._flatMap.keys()) if (path.startsWith(prefix)) this._ignoredFields.add(path);
|
|
432
|
+
}
|
|
433
|
+
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`);
|
|
434
|
+
for (const [path] of this._flatMap.entries()) {
|
|
435
|
+
if (!path) continue;
|
|
436
|
+
if (this._flattenedParents.has(path)) continue;
|
|
437
|
+
if (this._findJsonParent(path) !== undefined) continue;
|
|
438
|
+
const isFlattened = this._findFlattenedParent(path) !== undefined;
|
|
439
|
+
const physicalName = this._columnMap.get(path) ?? (isFlattened ? path.replace(/\./g, "__") : path);
|
|
440
|
+
this._pathToPhysical.set(path, physicalName);
|
|
441
|
+
this._physicalToPath.set(physicalName, path);
|
|
442
|
+
}
|
|
443
|
+
for (const parentPath of this._flattenedParents) {
|
|
444
|
+
const prefix = `${parentPath}.`;
|
|
445
|
+
const leaves = [];
|
|
446
|
+
for (const [path, physical] of this._pathToPhysical) if (path.startsWith(prefix)) leaves.push(physical);
|
|
447
|
+
if (leaves.length > 0) this._selectExpansion.set(parentPath, leaves);
|
|
448
|
+
}
|
|
449
|
+
this._requiresMappings = this._flattenedParents.size > 0 || this._jsonFields.size > 0;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Finds the nearest ancestor that is a flattened parent object.
|
|
453
|
+
* Returns the parent path, or undefined if no ancestor is being flattened.
|
|
454
|
+
*/ _findFlattenedParent(path) {
|
|
455
|
+
let pos = path.length;
|
|
456
|
+
while ((pos = path.lastIndexOf(".", pos - 1)) !== -1) {
|
|
457
|
+
const ancestor = path.slice(0, pos);
|
|
458
|
+
if (this._flattenedParents.has(ancestor)) return ancestor;
|
|
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;
|
|
470
|
+
}
|
|
471
|
+
return undefined;
|
|
472
|
+
}
|
|
369
473
|
_finalizeIndexes() {
|
|
474
|
+
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;
|
|
370
475
|
for (const index of this._indexes.values()) if (index.type === "unique" && index.fields.length === 1) this._uniqueProps.add(index.fields[0].name);
|
|
371
476
|
}
|
|
372
477
|
/**
|
|
@@ -380,7 +485,7 @@ else this._indexes.set(key, {
|
|
|
380
485
|
}
|
|
381
486
|
/**
|
|
382
487
|
* Prepares a payload for writing to the database:
|
|
383
|
-
* prepares IDs, strips ignored fields, maps column names.
|
|
488
|
+
* prepares IDs, strips ignored fields, flattens nested objects, maps column names.
|
|
384
489
|
* Defaults should be applied before this via `_applyDefaults`.
|
|
385
490
|
*/ _prepareForWrite(payload) {
|
|
386
491
|
const data = { ...payload };
|
|
@@ -388,12 +493,213 @@ else this._indexes.set(key, {
|
|
|
388
493
|
const fieldType = this._flatMap?.get(pk);
|
|
389
494
|
if (fieldType) data[pk] = this.adapter.prepareId(data[pk], fieldType);
|
|
390
495
|
}
|
|
391
|
-
for (const field of this._ignoredFields) delete data[field];
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
496
|
+
for (const field of this._ignoredFields) if (!field.includes(".")) delete data[field];
|
|
497
|
+
if (!this._requiresMappings || this.adapter.supportsNestedObjects()) {
|
|
498
|
+
for (const [logical, physical] of this._columnMap.entries()) if (logical in data) {
|
|
499
|
+
data[physical] = data[logical];
|
|
500
|
+
delete data[logical];
|
|
501
|
+
}
|
|
502
|
+
return data;
|
|
395
503
|
}
|
|
396
|
-
return data;
|
|
504
|
+
return this._flattenPayload(data);
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Flattens nested object fields into __-separated keys and
|
|
508
|
+
* JSON-stringifies @db.json / array fields.
|
|
509
|
+
* Uses _pathToPhysical for final key names.
|
|
510
|
+
*/ _flattenPayload(data) {
|
|
511
|
+
const result = {};
|
|
512
|
+
const dataKeys = Object.keys(data);
|
|
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
|
+
}
|
|
526
|
+
return result;
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Recursively flattens a nested object, writing physical keys to result.
|
|
530
|
+
*/ _flattenObject(prefix, obj, result) {
|
|
531
|
+
const objKeys = Object.keys(obj);
|
|
532
|
+
for (const key of objKeys) {
|
|
533
|
+
const value = obj[key];
|
|
534
|
+
const dotPath = `${prefix}.${key}`;
|
|
535
|
+
if (this._ignoredFields.has(dotPath)) continue;
|
|
536
|
+
if (this._flattenedParents.has(dotPath)) {
|
|
537
|
+
if (value === null || value === undefined) this._setFlattenedChildrenNull(dotPath, result);
|
|
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;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* When a parent object is null/undefined, set all its flattened children to null.
|
|
550
|
+
*/ _setFlattenedChildrenNull(parentPath, result) {
|
|
551
|
+
const prefix = `${parentPath}.`;
|
|
552
|
+
for (const [path, physical] of this._pathToPhysical.entries()) if (path.startsWith(prefix)) result[physical] = null;
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Reconstructs nested objects from flat __-separated column values.
|
|
556
|
+
* JSON fields are parsed from strings back to objects/arrays.
|
|
557
|
+
*/ _reconstructFromRead(row) {
|
|
558
|
+
if (!this._requiresMappings || this.adapter.supportsNestedObjects()) return row;
|
|
559
|
+
const result = {};
|
|
560
|
+
const rowKeys = Object.keys(row);
|
|
561
|
+
for (const physical of rowKeys) {
|
|
562
|
+
const value = row[physical];
|
|
563
|
+
const logicalPath = this._physicalToPath.get(physical);
|
|
564
|
+
if (!logicalPath) {
|
|
565
|
+
result[physical] = value;
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
if (this._jsonFields.has(logicalPath)) {
|
|
569
|
+
const parsed = typeof value === "string" ? JSON.parse(value) : value;
|
|
570
|
+
this._setNestedValue(result, logicalPath, parsed);
|
|
571
|
+
} else if (logicalPath.includes(".")) this._setNestedValue(result, logicalPath, value);
|
|
572
|
+
else result[logicalPath] = value;
|
|
573
|
+
}
|
|
574
|
+
for (const parentPath of this._flattenedParents) this._reconstructNullParent(result, parentPath);
|
|
575
|
+
return result;
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Sets a value at a dot-notation path, creating intermediate objects as needed.
|
|
579
|
+
*/ _setNestedValue(obj, dotPath, value) {
|
|
580
|
+
const parts = dotPath.split(".");
|
|
581
|
+
let current = obj;
|
|
582
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
583
|
+
const part = parts[i];
|
|
584
|
+
if (current[part] === undefined || current[part] === null) current[part] = {};
|
|
585
|
+
current = current[part];
|
|
586
|
+
}
|
|
587
|
+
current[parts[parts.length - 1]] = value;
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* If all children of a flattened parent are null, collapse the parent to null.
|
|
591
|
+
*/ _reconstructNullParent(obj, parentPath) {
|
|
592
|
+
const parts = parentPath.split(".");
|
|
593
|
+
let current = obj;
|
|
594
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
595
|
+
if (current[parts[i]] === undefined) return;
|
|
596
|
+
current = current[parts[i]];
|
|
597
|
+
}
|
|
598
|
+
const lastPart = parts[parts.length - 1];
|
|
599
|
+
const parentObj = current[lastPart];
|
|
600
|
+
if (typeof parentObj !== "object" || parentObj === null) return;
|
|
601
|
+
let allNull = true;
|
|
602
|
+
const parentKeys = Object.keys(parentObj);
|
|
603
|
+
for (const k of parentKeys) {
|
|
604
|
+
const v = parentObj[k];
|
|
605
|
+
if (v !== null && v !== undefined) {
|
|
606
|
+
allNull = false;
|
|
607
|
+
break;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
if (allNull) {
|
|
611
|
+
const parentType = this._flatMap?.get(parentPath);
|
|
612
|
+
current[lastPart] = parentType?.optional ? null : {};
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Translates a Uniquery's filter, sort, and projection from logical
|
|
617
|
+
* dot-notation paths to physical column names.
|
|
618
|
+
*/ _translateQuery(query) {
|
|
619
|
+
if (!this._requiresMappings || this.adapter.supportsNestedObjects()) return query;
|
|
620
|
+
return {
|
|
621
|
+
filter: this._translateFilter(query.filter),
|
|
622
|
+
controls: query.controls ? this._translateControls(query.controls) : query.controls,
|
|
623
|
+
insights: query.insights
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Recursively translates field names in a filter expression.
|
|
628
|
+
*/ _translateFilter(filter) {
|
|
629
|
+
if (!filter || typeof filter !== "object") return filter;
|
|
630
|
+
if (!this._requiresMappings) return filter;
|
|
631
|
+
const result = {};
|
|
632
|
+
const filterKeys = Object.keys(filter);
|
|
633
|
+
for (const key of filterKeys) {
|
|
634
|
+
const value = filter[key];
|
|
635
|
+
if (key === "$and" || key === "$or") result[key] = value.map((f) => this._translateFilter(f));
|
|
636
|
+
else if (key === "$not") result[key] = this._translateFilter(value);
|
|
637
|
+
else if (key.startsWith("$")) result[key] = value;
|
|
638
|
+
else {
|
|
639
|
+
const physical = this._pathToPhysical.get(key) ?? key;
|
|
640
|
+
result[physical] = value;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return result;
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Translates field names in sort and projection controls.
|
|
647
|
+
*/ _translateControls(controls) {
|
|
648
|
+
if (!controls) return controls;
|
|
649
|
+
const result = { ...controls };
|
|
650
|
+
if (controls.$sort) {
|
|
651
|
+
const translated = {};
|
|
652
|
+
const sortObj = controls.$sort;
|
|
653
|
+
const sortKeys = Object.keys(sortObj);
|
|
654
|
+
for (const key of sortKeys) {
|
|
655
|
+
if (this._flattenedParents.has(key)) continue;
|
|
656
|
+
const physical = this._pathToPhysical.get(key) ?? key;
|
|
657
|
+
translated[physical] = sortObj[key];
|
|
658
|
+
}
|
|
659
|
+
result.$sort = translated;
|
|
660
|
+
}
|
|
661
|
+
if (controls.$select) if (Array.isArray(controls.$select)) {
|
|
662
|
+
const expanded = [];
|
|
663
|
+
for (const key of controls.$select) {
|
|
664
|
+
const expansion = this._selectExpansion.get(key);
|
|
665
|
+
if (expansion) expanded.push(...expansion);
|
|
666
|
+
else expanded.push(this._pathToPhysical.get(key) ?? key);
|
|
667
|
+
}
|
|
668
|
+
result.$select = expanded;
|
|
669
|
+
} else {
|
|
670
|
+
const translated = {};
|
|
671
|
+
const selectObj = controls.$select;
|
|
672
|
+
const selectKeys = Object.keys(selectObj);
|
|
673
|
+
for (const key of selectKeys) {
|
|
674
|
+
const val = selectObj[key];
|
|
675
|
+
const expansion = this._selectExpansion.get(key);
|
|
676
|
+
if (expansion) for (const leaf of expansion) translated[leaf] = val;
|
|
677
|
+
else {
|
|
678
|
+
const physical = this._pathToPhysical.get(key) ?? key;
|
|
679
|
+
translated[physical] = val;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
result.$select = translated;
|
|
683
|
+
}
|
|
684
|
+
return result;
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Translates dot-notation keys in a decomposed patch to physical column names.
|
|
688
|
+
*/ _translatePatchKeys(update) {
|
|
689
|
+
if (!this._requiresMappings || this.adapter.supportsNestedObjects()) return update;
|
|
690
|
+
const result = {};
|
|
691
|
+
const updateKeys = Object.keys(update);
|
|
692
|
+
for (const key of updateKeys) {
|
|
693
|
+
const value = update[key];
|
|
694
|
+
const operatorMatch = key.match(/^(.+?)(\.__\$.+)$/);
|
|
695
|
+
const basePath = operatorMatch ? operatorMatch[1] : key;
|
|
696
|
+
const suffix = operatorMatch ? operatorMatch[2] : "";
|
|
697
|
+
const physical = this._pathToPhysical.get(basePath) ?? basePath;
|
|
698
|
+
const finalKey = physical + suffix;
|
|
699
|
+
if (this._jsonFields.has(basePath) && typeof value === "object" && value !== null && !suffix) result[finalKey] = JSON.stringify(value);
|
|
700
|
+
else result[finalKey] = value;
|
|
701
|
+
}
|
|
702
|
+
return result;
|
|
397
703
|
}
|
|
398
704
|
/**
|
|
399
705
|
* Extracts primary key field(s) from a payload to build a filter.
|
|
@@ -445,6 +751,12 @@ else this._indexes.set(key, {
|
|
|
445
751
|
_define_property$1(this, "_defaults", void 0);
|
|
446
752
|
_define_property$1(this, "_ignoredFields", void 0);
|
|
447
753
|
_define_property$1(this, "_uniqueProps", void 0);
|
|
754
|
+
/** Logical dot-path → physical column name. */ _define_property$1(this, "_pathToPhysical", void 0);
|
|
755
|
+
/** Physical column name → logical dot-path (inverse). */ _define_property$1(this, "_physicalToPath", void 0);
|
|
756
|
+
/** Object paths being flattened into __-separated columns (no column themselves). */ _define_property$1(this, "_flattenedParents", void 0);
|
|
757
|
+
/** Fields stored as JSON (@db.json + array fields). */ _define_property$1(this, "_jsonFields", void 0);
|
|
758
|
+
/** Intermediate paths → their leaf physical column names (for $select expansion in relational DBs). */ _define_property$1(this, "_selectExpansion", void 0);
|
|
759
|
+
/** Fast-path flag: skip all mapping when no nested/json fields exist. */ _define_property$1(this, "_requiresMappings", void 0);
|
|
448
760
|
_define_property$1(this, "validators", void 0);
|
|
449
761
|
this._type = _type;
|
|
450
762
|
this.adapter = adapter;
|
|
@@ -455,6 +767,12 @@ else this._indexes.set(key, {
|
|
|
455
767
|
this._defaults = new Map();
|
|
456
768
|
this._ignoredFields = new Set();
|
|
457
769
|
this._uniqueProps = new Set();
|
|
770
|
+
this._pathToPhysical = new Map();
|
|
771
|
+
this._physicalToPath = new Map();
|
|
772
|
+
this._flattenedParents = new Set();
|
|
773
|
+
this._jsonFields = new Set();
|
|
774
|
+
this._selectExpansion = new Map();
|
|
775
|
+
this._requiresMappings = false;
|
|
458
776
|
this.validators = new Map();
|
|
459
777
|
if (!isAnnotatedType(_type)) throw new Error("Atscript Annotated Type expected");
|
|
460
778
|
if (_type.type.kind !== "object") throw new Error("Database table type must be an object type");
|
|
@@ -514,6 +832,14 @@ var BaseDbAdapter = class {
|
|
|
514
832
|
return false;
|
|
515
833
|
}
|
|
516
834
|
/**
|
|
835
|
+
* Whether this adapter handles nested objects natively.
|
|
836
|
+
* When `true`, the generic layer skips flattening and
|
|
837
|
+
* passes nested objects as-is to the adapter.
|
|
838
|
+
* MongoDB returns `true`; relational adapters return `false` (default).
|
|
839
|
+
*/ supportsNestedObjects() {
|
|
840
|
+
return false;
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
517
843
|
* Applies a patch payload using native database operations.
|
|
518
844
|
* Only called when {@link supportsNativePatch} returns `true`.
|
|
519
845
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atscript/utils-db",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.31",
|
|
4
4
|
"description": "Database adapter utilities for atscript.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"atscript",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@uniqu/core": "^0.0.1",
|
|
40
|
-
"@atscript/core": "^0.1.
|
|
41
|
-
"@atscript/typescript": "^0.1.
|
|
40
|
+
"@atscript/core": "^0.1.31",
|
|
41
|
+
"@atscript/typescript": "^0.1.31"
|
|
42
42
|
},
|
|
43
43
|
"scripts": {
|
|
44
44
|
"pub": "pnpm publish --access public",
|