@lossless.org/client 0.1.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/core/drivers.d.ts +17 -0
- package/dist_ts/core/drivers.js +27 -0
- package/dist_ts/core/interfaces.d.ts +1 -1
- package/dist_ts/index.d.ts +2 -0
- package/dist_ts/index.js +2 -1
- package/dist_ts/nosqldb/classes.atomicupdate.d.ts +21 -0
- package/dist_ts/nosqldb/classes.atomicupdate.js +41 -1
- package/dist_ts/nosqldb/classes.collection.d.ts +31 -0
- package/dist_ts/nosqldb/classes.collection.js +120 -13
- package/dist_ts/nosqldb/classes.doc.d.ts +58 -0
- package/dist_ts/nosqldb/classes.doc.js +287 -22
- package/dist_ts/nosqldb/classes.ordinarypersistence.d.ts +6 -0
- package/dist_ts/nosqldb/classes.ordinarypersistence.js +7 -1
- package/dist_ts/nosqldb/index.d.ts +2 -0
- package/dist_ts/nosqldb/index.js +2 -1
- package/dist_ts/nosqldb/lineage.d.ts +13 -0
- package/dist_ts/nosqldb/lineage.js +13 -0
- package/dist_ts/objectstorage/classes.bucket.js +34 -36
- package/dist_ts/plugins.js +6 -4
- package/package.json +24 -7
- package/readme.md +30 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/core/drivers.ts +43 -0
- package/ts/core/interfaces.ts +1 -1
- package/ts/index.ts +2 -0
- package/ts/nosqldb/classes.atomicupdate.ts +67 -0
- package/ts/nosqldb/classes.collection.ts +181 -10
- package/ts/nosqldb/classes.doc.ts +525 -19
- package/ts/nosqldb/classes.ordinarypersistence.ts +8 -0
- package/ts/nosqldb/index.ts +2 -0
- package/ts/nosqldb/lineage.ts +25 -0
- package/ts/objectstorage/classes.bucket.ts +39 -43
- package/ts/plugins.ts +6 -3
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type ISmartdataStoredEvidenceInspection,
|
|
18
18
|
} from './classes.storedinspection.js';
|
|
19
19
|
import {
|
|
20
|
+
getIdentityDocumentIdField,
|
|
20
21
|
type IManager,
|
|
21
22
|
type ISmartdataIndexInfo,
|
|
22
23
|
SmartdataCollection,
|
|
@@ -25,7 +26,9 @@ import { SmartdataDbWatcher } from './classes.watcher.js';
|
|
|
25
26
|
import { SmartdataLuceneAdapter } from './classes.lucene.adapter.js';
|
|
26
27
|
import { executeAtomicDelete, executeAtomicDeleteMany } from './classes.atomicdelete.js';
|
|
27
28
|
import { executeAtomicFindOneAndUpdate } from './classes.atomicfindoneandupdate.js';
|
|
28
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
executeAtomicUpdate, executeAtomicUpdateMany, executeAtomicUpsertMany,
|
|
31
|
+
} from './classes.atomicupdate.js';
|
|
29
32
|
import {
|
|
30
33
|
SmartdataPersistenceError,
|
|
31
34
|
} from './classes.persistence.js';
|
|
@@ -590,6 +593,23 @@ export type TSmartdataNumericDocumentPath<T> = {
|
|
|
590
593
|
NonNullable<T[TKey]> extends number ? TKey : never;
|
|
591
594
|
}[TSmartdataDocumentPath<T>];
|
|
592
595
|
|
|
596
|
+
/** Declared top-level fields that carry a monotonic ordering: numbers and dates. */
|
|
597
|
+
export type TSmartdataMonotonicDocumentPath<T> = {
|
|
598
|
+
[TKey in TSmartdataDocumentPath<T>]:
|
|
599
|
+
NonNullable<T[TKey]> extends number
|
|
600
|
+
? TKey
|
|
601
|
+
: NonNullable<T[TKey]> extends Date
|
|
602
|
+
? TKey
|
|
603
|
+
: never;
|
|
604
|
+
}[TSmartdataDocumentPath<T>];
|
|
605
|
+
|
|
606
|
+
type TSmartdataAtomicMonotonic<T> =
|
|
607
|
+
& Partial<{
|
|
608
|
+
[TKey in TSmartdataMonotonicDocumentPath<T>]:
|
|
609
|
+
NonNullable<T[TKey]> extends Date ? Date : number;
|
|
610
|
+
}>
|
|
611
|
+
& Partial<Record<TSmartdataAtomicNestedDocumentPath<T>, number | Date>>;
|
|
612
|
+
|
|
593
613
|
export type TSmartdataAtomicNumericFieldReference<T> =
|
|
594
614
|
`$${TSmartdataNumericDocumentPath<T>}`;
|
|
595
615
|
|
|
@@ -691,6 +711,18 @@ export interface ISmartdataAtomicUpdate<T> {
|
|
|
691
711
|
Record<TSmartdataAtomicDocumentPath<T>, '' | true | 1>
|
|
692
712
|
>;
|
|
693
713
|
$inc?: TSmartdataAtomicIncrement<T>;
|
|
714
|
+
/**
|
|
715
|
+
* Monotonic ceiling for a declared numeric or date field: one server-side
|
|
716
|
+
* compare-and-write that never lowers the stored value, so a retried or
|
|
717
|
+
* out-of-order writer cannot regress a counter. With `upsert: true` a
|
|
718
|
+
* missing field is inserted with the operand.
|
|
719
|
+
*/
|
|
720
|
+
$max?: TSmartdataAtomicMonotonic<T>;
|
|
721
|
+
/**
|
|
722
|
+
* Monotonic floor for a declared numeric or date field: the mirror of
|
|
723
|
+
* `$max`, never raising the stored value.
|
|
724
|
+
*/
|
|
725
|
+
$min?: TSmartdataAtomicMonotonic<T>;
|
|
694
726
|
$setOnInsert?: TSmartdataAtomicSet<T>;
|
|
695
727
|
/**
|
|
696
728
|
* Appends one element to a declared top-level array field. Modifier
|
|
@@ -860,6 +892,30 @@ export interface ISmartdataAtomicUpdateManyResult {
|
|
|
860
892
|
modifiedCount: number;
|
|
861
893
|
}
|
|
862
894
|
|
|
895
|
+
/** One strict selector and its update, both validated before any write. */
|
|
896
|
+
export interface ISmartdataAtomicUpsertManyOperation<T> {
|
|
897
|
+
filter: TSmartdataAtomicFilter<T>;
|
|
898
|
+
update: ISmartdataAtomicUpdate<T>;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
export interface ISmartdataAtomicUpsertManyOptions {
|
|
902
|
+
session?: TSmartdataOrdinarySession;
|
|
903
|
+
/** Client-side bulk operation deadline, including server selection and pool waits. */
|
|
904
|
+
timeoutMS?: number;
|
|
905
|
+
/**
|
|
906
|
+
* Only `false`. The batch is always unordered, so one conflicting entry
|
|
907
|
+
* never hides the outcome of the entries behind it.
|
|
908
|
+
*/
|
|
909
|
+
ordered?: false;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
export interface ISmartdataAtomicUpsertManyResult {
|
|
913
|
+
acknowledged: boolean;
|
|
914
|
+
matchedCount: number;
|
|
915
|
+
modifiedCount: number;
|
|
916
|
+
upsertedCount: number;
|
|
917
|
+
}
|
|
918
|
+
|
|
863
919
|
const normalizeAtomicFindOneAndUpdateOptions = <
|
|
864
920
|
TReturnDocument extends TSmartdataAtomicReturnDocument,
|
|
865
921
|
>(
|
|
@@ -2255,6 +2311,7 @@ const normalizeStrictFilter = (
|
|
|
2255
2311
|
operationLabelArg:
|
|
2256
2312
|
| 'Atomic update'
|
|
2257
2313
|
| 'Atomic update-many'
|
|
2314
|
+
| 'Atomic upsert-many'
|
|
2258
2315
|
| 'Atomic find-one-and-update'
|
|
2259
2316
|
| 'Atomic delete'
|
|
2260
2317
|
| 'Atomic delete-many',
|
|
@@ -2533,6 +2590,15 @@ const normalizeAtomicUpdate = <T>(
|
|
|
2533
2590
|
declaredNestedPathsArg: Set<string>,
|
|
2534
2591
|
uniqueRootsArg: Set<string>,
|
|
2535
2592
|
serializedRootsArg: Set<string>,
|
|
2593
|
+
/**
|
|
2594
|
+
* Supplied only by upserting operations. Its presence allows `$setOnInsert`
|
|
2595
|
+
* to seed declared identity roots on the insert branch; identities stay
|
|
2596
|
+
* immutable on the update branch because MongoDB never applies
|
|
2597
|
+
* `$setOnInsert` to an existing document.
|
|
2598
|
+
*/
|
|
2599
|
+
identitySeedingArg?: {
|
|
2600
|
+
identityValueTypes: ReadonlyMap<string, TSmartdataIdentityValueType>;
|
|
2601
|
+
},
|
|
2536
2602
|
): plugins.mongodb.UpdateFilter<plugins.mongodb.Document> => {
|
|
2537
2603
|
if (
|
|
2538
2604
|
typeof updateArg !== 'object' ||
|
|
@@ -2552,8 +2618,11 @@ const normalizeAtomicUpdate = <T>(
|
|
|
2552
2618
|
'$push',
|
|
2553
2619
|
'$addToSet',
|
|
2554
2620
|
'$pull',
|
|
2621
|
+
'$max',
|
|
2622
|
+
'$min',
|
|
2555
2623
|
]);
|
|
2556
2624
|
const arrayOperators = new Set(['$push', '$addToSet', '$pull']);
|
|
2625
|
+
const monotonicOperators = new Set(['$max', '$min']);
|
|
2557
2626
|
for (const operator of Object.keys(updateArg)) {
|
|
2558
2627
|
if (!allowedOperators.has(operator)) {
|
|
2559
2628
|
throw new SmartdataPersistenceError(
|
|
@@ -2595,9 +2664,30 @@ const normalizeAtomicUpdate = <T>(
|
|
|
2595
2664
|
);
|
|
2596
2665
|
}
|
|
2597
2666
|
if (uniqueRootsArg.has(root)) {
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2667
|
+
if (operator !== '$setOnInsert') {
|
|
2668
|
+
throw new SmartdataPersistenceError(
|
|
2669
|
+
'invalid_argument',
|
|
2670
|
+
`${operator} may not modify immutable @unI() field "${root}".`,
|
|
2671
|
+
);
|
|
2672
|
+
}
|
|
2673
|
+
if (!identitySeedingArg) {
|
|
2674
|
+
throw new SmartdataPersistenceError(
|
|
2675
|
+
'invalid_argument',
|
|
2676
|
+
`$setOnInsert may not seed immutable @unI() field "${root}" without upsert: true.`,
|
|
2677
|
+
);
|
|
2678
|
+
}
|
|
2679
|
+
if (path !== root) {
|
|
2680
|
+
throw new SmartdataPersistenceError(
|
|
2681
|
+
'invalid_argument',
|
|
2682
|
+
`$setOnInsert may not target a nested path below immutable @unI() field "${root}".`,
|
|
2683
|
+
);
|
|
2684
|
+
}
|
|
2685
|
+
// The insert branch must create a complete, addressable document, so
|
|
2686
|
+
// the seeded value passes the same identity validation an insert does.
|
|
2687
|
+
requireUsableIdentityValue(
|
|
2688
|
+
value,
|
|
2689
|
+
`$setOnInsert identity field "${root}"`,
|
|
2690
|
+
identitySeedingArg.identityValueTypes.get(root) ?? 'string',
|
|
2601
2691
|
);
|
|
2602
2692
|
}
|
|
2603
2693
|
const conflict = occupiedPaths.find((occupiedPath) =>
|
|
@@ -2688,6 +2778,34 @@ const normalizeAtomicUpdate = <T>(
|
|
|
2688
2778
|
`${operator} path "${path}" may not be undefined.`,
|
|
2689
2779
|
);
|
|
2690
2780
|
}
|
|
2781
|
+
if (monotonicOperators.has(operator)) {
|
|
2782
|
+
if (typeof value === 'number') {
|
|
2783
|
+
if (!Number.isFinite(value)) {
|
|
2784
|
+
throw new SmartdataPersistenceError(
|
|
2785
|
+
'invalid_argument',
|
|
2786
|
+
`${operator} path "${path}" must be a finite number or a Date.`,
|
|
2787
|
+
);
|
|
2788
|
+
}
|
|
2789
|
+
normalizedOperation[path] = value;
|
|
2790
|
+
continue;
|
|
2791
|
+
}
|
|
2792
|
+
if (
|
|
2793
|
+
!(value instanceof Date)
|
|
2794
|
+
|| !Number.isFinite(value.getTime())
|
|
2795
|
+
) {
|
|
2796
|
+
throw new SmartdataPersistenceError(
|
|
2797
|
+
'invalid_argument',
|
|
2798
|
+
`${operator} path "${path}" must be a finite number or a Date.`,
|
|
2799
|
+
);
|
|
2800
|
+
}
|
|
2801
|
+
// A custom encoder could otherwise rewrite the comparison operand at
|
|
2802
|
+
// serialization time, so the date is captured as an inert snapshot.
|
|
2803
|
+
normalizedOperation[path] = createInertAtomicBsonScalarSnapshot(
|
|
2804
|
+
value,
|
|
2805
|
+
`${operator} path "${path}"`,
|
|
2806
|
+
);
|
|
2807
|
+
continue;
|
|
2808
|
+
}
|
|
2691
2809
|
if (
|
|
2692
2810
|
operator === '$inc' &&
|
|
2693
2811
|
(typeof value !== 'number' || !Number.isFinite(value))
|
|
@@ -2714,6 +2832,151 @@ const normalizeAtomicUpdate = <T>(
|
|
|
2714
2832
|
return normalized as plugins.mongodb.UpdateFilter<plugins.mongodb.Document>;
|
|
2715
2833
|
};
|
|
2716
2834
|
|
|
2835
|
+
/**
|
|
2836
|
+
* Reads a globally constraining equality value for one declared path from an
|
|
2837
|
+
* already normalized filter. `$or` branches are excluded because an equality
|
|
2838
|
+
* inside a single branch does not pin the matched document.
|
|
2839
|
+
*/
|
|
2840
|
+
const readNormalizedFilterEquality = (
|
|
2841
|
+
filterArg: Record<string, unknown>,
|
|
2842
|
+
pathArg: string,
|
|
2843
|
+
): { found: boolean; value?: unknown } => {
|
|
2844
|
+
for (const [key, value] of Object.entries(filterArg)) {
|
|
2845
|
+
if (key === '$and' && Array.isArray(value)) {
|
|
2846
|
+
for (const entry of value) {
|
|
2847
|
+
if (isPlainObject(entry)) {
|
|
2848
|
+
const nested = readNormalizedFilterEquality(
|
|
2849
|
+
entry as Record<string, unknown>,
|
|
2850
|
+
pathArg,
|
|
2851
|
+
);
|
|
2852
|
+
if (nested.found) {
|
|
2853
|
+
return nested;
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
continue;
|
|
2858
|
+
}
|
|
2859
|
+
if (key !== pathArg) {
|
|
2860
|
+
continue;
|
|
2861
|
+
}
|
|
2862
|
+
if (isPlainObject(value)) {
|
|
2863
|
+
const entries = Object.entries(value as Record<string, unknown>);
|
|
2864
|
+
if (entries.length === 1 && entries[0][0] === '$eq') {
|
|
2865
|
+
return { found: true, value: entries[0][1] };
|
|
2866
|
+
}
|
|
2867
|
+
continue;
|
|
2868
|
+
}
|
|
2869
|
+
return { found: true, value };
|
|
2870
|
+
}
|
|
2871
|
+
return { found: false };
|
|
2872
|
+
};
|
|
2873
|
+
|
|
2874
|
+
/**
|
|
2875
|
+
* An upsert seeds its inserted document from the filter's equality conditions
|
|
2876
|
+
* and from `$setOnInsert`. When both name the same identity they must agree,
|
|
2877
|
+
* otherwise the insert branch would create a document its own selector can
|
|
2878
|
+
* never match again.
|
|
2879
|
+
*/
|
|
2880
|
+
const assertSeededIdentitiesMatchFilter = (
|
|
2881
|
+
normalizedFilterArg: Record<string, unknown>,
|
|
2882
|
+
normalizedUpdateArg: Record<string, unknown>,
|
|
2883
|
+
uniqueRootsArg: Set<string>,
|
|
2884
|
+
operationLabelArg: string,
|
|
2885
|
+
): void => {
|
|
2886
|
+
const seeded = normalizedUpdateArg.$setOnInsert;
|
|
2887
|
+
if (!isPlainObject(seeded)) {
|
|
2888
|
+
return;
|
|
2889
|
+
}
|
|
2890
|
+
for (const [path, value] of Object.entries(seeded as Record<string, unknown>)) {
|
|
2891
|
+
if (!uniqueRootsArg.has(path)) {
|
|
2892
|
+
continue;
|
|
2893
|
+
}
|
|
2894
|
+
const anchored = readNormalizedFilterEquality(normalizedFilterArg, path);
|
|
2895
|
+
if (anchored.found && anchored.value !== value) {
|
|
2896
|
+
throw new SmartdataPersistenceError(
|
|
2897
|
+
'invalid_argument',
|
|
2898
|
+
`${operationLabelArg} $setOnInsert identity "${path}" must equal the filter's equality anchor for "${path}".`,
|
|
2899
|
+
);
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
};
|
|
2903
|
+
|
|
2904
|
+
/**
|
|
2905
|
+
* An upsert on a model whose identity owns the document `_id` must pin that
|
|
2906
|
+
* identity by equality: MongoDB derives the inserted primary key from the
|
|
2907
|
+
* filter, so an unpinned upsert would create a document with a generated `_id`
|
|
2908
|
+
* that contradicts the declared mapping. Returns the pinned identity value so
|
|
2909
|
+
* the caller can seed the stored identity field exactly as an unmapped upsert
|
|
2910
|
+
* would have done.
|
|
2911
|
+
*/
|
|
2912
|
+
const requirePinnedDocumentIdentity = (
|
|
2913
|
+
normalizedFilterArg: Record<string, unknown>,
|
|
2914
|
+
identityFieldArg: string,
|
|
2915
|
+
operationLabelArg: string,
|
|
2916
|
+
): unknown => {
|
|
2917
|
+
const anchored = readNormalizedFilterEquality(
|
|
2918
|
+
normalizedFilterArg,
|
|
2919
|
+
identityFieldArg,
|
|
2920
|
+
);
|
|
2921
|
+
if (!anchored.found) {
|
|
2922
|
+
throw new SmartdataPersistenceError(
|
|
2923
|
+
'invalid_argument',
|
|
2924
|
+
`${operationLabelArg} upsert requires an equality anchor on "${identityFieldArg}", which owns the document _id.`,
|
|
2925
|
+
);
|
|
2926
|
+
}
|
|
2927
|
+
return anchored.value;
|
|
2928
|
+
};
|
|
2929
|
+
|
|
2930
|
+
/**
|
|
2931
|
+
* Rewrites equality and operator conditions on a model's `identityAsDocumentId`
|
|
2932
|
+
* field onto `_id`, so the primary key index answers the query instead of a
|
|
2933
|
+
* collection scan. The stored identity field keeps its own value, but every
|
|
2934
|
+
* identity-filtered access is answered through `_id`, so a document written
|
|
2935
|
+
* before the option was declared resolves only if `_id` already equals it.
|
|
2936
|
+
*/
|
|
2937
|
+
const mapIdentityFilterToDocumentId = (
|
|
2938
|
+
filterArg: Record<string, unknown>,
|
|
2939
|
+
identityFieldArg: string | undefined,
|
|
2940
|
+
): Record<string, unknown> => {
|
|
2941
|
+
if (!identityFieldArg || !isPlainObject(filterArg)) {
|
|
2942
|
+
return filterArg;
|
|
2943
|
+
}
|
|
2944
|
+
const carriesExplicitDocumentId = Object.prototype.hasOwnProperty.call(
|
|
2945
|
+
filterArg,
|
|
2946
|
+
'_id',
|
|
2947
|
+
);
|
|
2948
|
+
const mapped: Record<string, unknown> = {};
|
|
2949
|
+
for (const [key, value] of Object.entries(filterArg)) {
|
|
2950
|
+
if (
|
|
2951
|
+
(key === '$and' || key === '$or' || key === '$nor')
|
|
2952
|
+
&& Array.isArray(value)
|
|
2953
|
+
) {
|
|
2954
|
+
mapped[key] = value.map((entryArg) =>
|
|
2955
|
+
isPlainObject(entryArg)
|
|
2956
|
+
? mapIdentityFilterToDocumentId(
|
|
2957
|
+
entryArg as Record<string, unknown>,
|
|
2958
|
+
identityFieldArg,
|
|
2959
|
+
)
|
|
2960
|
+
: entryArg,
|
|
2961
|
+
);
|
|
2962
|
+
continue;
|
|
2963
|
+
}
|
|
2964
|
+
if (key === '$not' && isPlainObject(value)) {
|
|
2965
|
+
mapped[key] = mapIdentityFilterToDocumentId(
|
|
2966
|
+
value as Record<string, unknown>,
|
|
2967
|
+
identityFieldArg,
|
|
2968
|
+
);
|
|
2969
|
+
continue;
|
|
2970
|
+
}
|
|
2971
|
+
if (key === identityFieldArg && !carriesExplicitDocumentId) {
|
|
2972
|
+
mapped._id = value;
|
|
2973
|
+
continue;
|
|
2974
|
+
}
|
|
2975
|
+
mapped[key] = value;
|
|
2976
|
+
}
|
|
2977
|
+
return mapped;
|
|
2978
|
+
};
|
|
2979
|
+
|
|
2717
2980
|
const normalizeProjection = <T>(
|
|
2718
2981
|
projectionArg: TSmartdataProjection<T> | undefined,
|
|
2719
2982
|
declaredRootsArg: Set<string>,
|
|
@@ -3087,6 +3350,27 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
3087
3350
|
return new Set(collection.getBoundModelSchema()?.numericFields || []);
|
|
3088
3351
|
}
|
|
3089
3352
|
|
|
3353
|
+
/**
|
|
3354
|
+
* Returns the declared identity field this model stores as the document
|
|
3355
|
+
* `_id`, or undefined when SmartData owns the primary key.
|
|
3356
|
+
*/
|
|
3357
|
+
private static getIdentityDocumentIdField(): string | undefined {
|
|
3358
|
+
return getIdentityDocumentIdField(this);
|
|
3359
|
+
}
|
|
3360
|
+
|
|
3361
|
+
/**
|
|
3362
|
+
* Converts an ordinary read filter and routes a declared document-id
|
|
3363
|
+
* identity onto `_id`, so reads use the primary key index.
|
|
3364
|
+
*/
|
|
3365
|
+
private static normalizeReadFilter(
|
|
3366
|
+
filterArg: Record<string, any>,
|
|
3367
|
+
): plugins.mongodb.Filter<plugins.mongodb.Document> {
|
|
3368
|
+
return mapIdentityFilterToDocumentId(
|
|
3369
|
+
convertFilterForMongoDb(filterArg),
|
|
3370
|
+
(this as any).getIdentityDocumentIdField() as string | undefined,
|
|
3371
|
+
) as plugins.mongodb.Filter<plugins.mongodb.Document>;
|
|
3372
|
+
}
|
|
3373
|
+
|
|
3090
3374
|
/**
|
|
3091
3375
|
* Returns every equality key set that globally constrains a single document
|
|
3092
3376
|
* of this model: each declared identity field on its own, plus the full key
|
|
@@ -3531,20 +3815,40 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
3531
3815
|
'Atomic update does not support upsert when the filter contains $expr.',
|
|
3532
3816
|
);
|
|
3533
3817
|
}
|
|
3818
|
+
const upserting = normalizedOptions?.upsert === true;
|
|
3534
3819
|
const normalizedUpdate = normalizeAtomicUpdate(
|
|
3535
3820
|
updateArg,
|
|
3536
3821
|
declaredRoots,
|
|
3537
3822
|
declaredAtomicPaths,
|
|
3538
3823
|
uniqueRoots,
|
|
3539
3824
|
serializedRoots,
|
|
3825
|
+
upserting ? { identityValueTypes } : undefined,
|
|
3826
|
+
);
|
|
3827
|
+
assertSeededIdentitiesMatchFilter(
|
|
3828
|
+
normalizedFilter,
|
|
3829
|
+
normalizedUpdate,
|
|
3830
|
+
uniqueRoots,
|
|
3831
|
+
'Atomic update',
|
|
3540
3832
|
);
|
|
3541
3833
|
const now = new Date().toISOString();
|
|
3542
3834
|
normalizedUpdate.$set = {
|
|
3543
3835
|
...(normalizedUpdate.$set || {}),
|
|
3544
3836
|
_updatedAt: now,
|
|
3545
3837
|
};
|
|
3546
|
-
|
|
3838
|
+
const identityDocumentIdField = (this as any).getIdentityDocumentIdField() as
|
|
3839
|
+
| string
|
|
3840
|
+
| undefined;
|
|
3841
|
+
if (upserting) {
|
|
3547
3842
|
normalizedUpdate.$setOnInsert = {
|
|
3843
|
+
...(identityDocumentIdField
|
|
3844
|
+
? {
|
|
3845
|
+
[identityDocumentIdField]: requirePinnedDocumentIdentity(
|
|
3846
|
+
normalizedFilter,
|
|
3847
|
+
identityDocumentIdField,
|
|
3848
|
+
'Atomic update',
|
|
3849
|
+
),
|
|
3850
|
+
}
|
|
3851
|
+
: {}),
|
|
3548
3852
|
...(normalizedUpdate.$setOnInsert || {}),
|
|
3549
3853
|
_createdAt: now,
|
|
3550
3854
|
};
|
|
@@ -3552,7 +3856,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
3552
3856
|
const collection: SmartdataCollection<T> = (this as any).collection;
|
|
3553
3857
|
const result = await executeAtomicUpdate(
|
|
3554
3858
|
collection,
|
|
3555
|
-
normalizedFilter,
|
|
3859
|
+
mapIdentityFilterToDocumentId(normalizedFilter, identityDocumentIdField),
|
|
3556
3860
|
normalizedUpdate,
|
|
3557
3861
|
normalizedOptions,
|
|
3558
3862
|
);
|
|
@@ -3615,6 +3919,13 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
3615
3919
|
declaredAtomicPaths,
|
|
3616
3920
|
uniqueRoots,
|
|
3617
3921
|
serializedRoots,
|
|
3922
|
+
normalizedOptions.upsert ? { identityValueTypes } : undefined,
|
|
3923
|
+
);
|
|
3924
|
+
assertSeededIdentitiesMatchFilter(
|
|
3925
|
+
normalizedFilter,
|
|
3926
|
+
normalizedUpdate,
|
|
3927
|
+
uniqueRoots,
|
|
3928
|
+
'Atomic find-one-and-update',
|
|
3618
3929
|
);
|
|
3619
3930
|
if (policy?.timestamps !== 'none') {
|
|
3620
3931
|
const now = new Date().toISOString();
|
|
@@ -3623,6 +3934,19 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
3623
3934
|
normalizedUpdate.$setOnInsert = { ...(normalizedUpdate.$setOnInsert || {}), _createdAt: now };
|
|
3624
3935
|
}
|
|
3625
3936
|
}
|
|
3937
|
+
const identityDocumentIdField = (this as any).getIdentityDocumentIdField() as
|
|
3938
|
+
| string
|
|
3939
|
+
| undefined;
|
|
3940
|
+
if (normalizedOptions.upsert && identityDocumentIdField) {
|
|
3941
|
+
normalizedUpdate.$setOnInsert = {
|
|
3942
|
+
[identityDocumentIdField]: requirePinnedDocumentIdentity(
|
|
3943
|
+
normalizedFilter,
|
|
3944
|
+
identityDocumentIdField,
|
|
3945
|
+
'Atomic find-one-and-update',
|
|
3946
|
+
),
|
|
3947
|
+
...(normalizedUpdate.$setOnInsert || {}),
|
|
3948
|
+
};
|
|
3949
|
+
}
|
|
3626
3950
|
const normalizedSort = normalizeSort(
|
|
3627
3951
|
normalizedOptions.sort as TSmartdataSort<T> | undefined,
|
|
3628
3952
|
declaredRoots,
|
|
@@ -3631,7 +3955,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
3631
3955
|
const collection: SmartdataCollection<T> = (this as any).collection;
|
|
3632
3956
|
const result = await executeAtomicFindOneAndUpdate(
|
|
3633
3957
|
collection,
|
|
3634
|
-
normalizedFilter,
|
|
3958
|
+
mapIdentityFilterToDocumentId(normalizedFilter, identityDocumentIdField),
|
|
3635
3959
|
normalizedUpdate,
|
|
3636
3960
|
{
|
|
3637
3961
|
returnDocument: normalizedOptions.returnDocument,
|
|
@@ -3688,7 +4012,10 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
3688
4012
|
const collection: SmartdataCollection<T> = (this as any).collection;
|
|
3689
4013
|
const result = await executeAtomicDelete(
|
|
3690
4014
|
collection,
|
|
3691
|
-
|
|
4015
|
+
mapIdentityFilterToDocumentId(
|
|
4016
|
+
normalizedFilter,
|
|
4017
|
+
(this as any).getIdentityDocumentIdField() as string | undefined,
|
|
4018
|
+
),
|
|
3692
4019
|
{ session: opts?.session, timeoutMS: requireBoundedInteger(opts?.timeoutMS, 'Atomic delete timeoutMS', 120_000) },
|
|
3693
4020
|
);
|
|
3694
4021
|
return {
|
|
@@ -3736,7 +4063,10 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
3736
4063
|
const collection: SmartdataCollection<T> = (this as any).collection;
|
|
3737
4064
|
const result = await executeAtomicDeleteMany(
|
|
3738
4065
|
collection,
|
|
3739
|
-
|
|
4066
|
+
mapIdentityFilterToDocumentId(
|
|
4067
|
+
normalizedFilter,
|
|
4068
|
+
(this as any).getIdentityDocumentIdField() as string | undefined,
|
|
4069
|
+
),
|
|
3740
4070
|
{ session: opts?.session, timeoutMS: requireBoundedInteger(opts?.timeoutMS, 'Atomic delete-many timeoutMS', 120_000) },
|
|
3741
4071
|
);
|
|
3742
4072
|
return {
|
|
@@ -3810,7 +4140,10 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
3810
4140
|
const collection: SmartdataCollection<T> = (this as any).collection;
|
|
3811
4141
|
const result = await executeAtomicUpdateMany(
|
|
3812
4142
|
collection,
|
|
3813
|
-
|
|
4143
|
+
mapIdentityFilterToDocumentId(
|
|
4144
|
+
normalizedFilter,
|
|
4145
|
+
(this as any).getIdentityDocumentIdField() as string | undefined,
|
|
4146
|
+
),
|
|
3814
4147
|
normalizedUpdate,
|
|
3815
4148
|
opts,
|
|
3816
4149
|
);
|
|
@@ -3821,6 +4154,172 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
3821
4154
|
};
|
|
3822
4155
|
}
|
|
3823
4156
|
|
|
4157
|
+
/**
|
|
4158
|
+
* Atomically upserts a bounded batch of ordinary model documents in one
|
|
4159
|
+
* round trip. Every entry carries its own strict selector and update, and
|
|
4160
|
+
* each entry applies atomically; the batch as a whole is not isolated unless
|
|
4161
|
+
* the caller supplies a transaction session. The complete batch is validated
|
|
4162
|
+
* before any write, so one invalid entry rejects the call without writing.
|
|
4163
|
+
*/
|
|
4164
|
+
public static async atomicUpsertMany<T>(
|
|
4165
|
+
this: plugins.tsclass.typeFest.Class<T>,
|
|
4166
|
+
operationsArg: ReadonlyArray<ISmartdataAtomicUpsertManyOperation<T>>,
|
|
4167
|
+
opts?: ISmartdataAtomicUpsertManyOptions,
|
|
4168
|
+
): Promise<ISmartdataAtomicUpsertManyResult> {
|
|
4169
|
+
if (getOrdinaryPersistencePolicy(this)) {
|
|
4170
|
+
throw new SmartdataPersistenceError('unsupported_operation',
|
|
4171
|
+
'Validated ordinary models require a transactional postimage update.');
|
|
4172
|
+
}
|
|
4173
|
+
if ((this as any)[exactPersistencePolicySymbol]) {
|
|
4174
|
+
throw new SmartdataPersistenceError(
|
|
4175
|
+
'unsupported_operation',
|
|
4176
|
+
'atomicUpsertMany is unavailable for exact-persistence models.',
|
|
4177
|
+
);
|
|
4178
|
+
}
|
|
4179
|
+
if (
|
|
4180
|
+
!Array.isArray(operationsArg)
|
|
4181
|
+
|| operationsArg.length < 1
|
|
4182
|
+
|| operationsArg.length > 1000
|
|
4183
|
+
) {
|
|
4184
|
+
throw new SmartdataPersistenceError(
|
|
4185
|
+
'invalid_argument',
|
|
4186
|
+
'Atomic upsert-many requires 1 to 1000 operations.',
|
|
4187
|
+
);
|
|
4188
|
+
}
|
|
4189
|
+
if (opts !== undefined) {
|
|
4190
|
+
const optionsValue: unknown = opts;
|
|
4191
|
+
if (!isPlainObject(optionsValue) || plugins.nodeUtil.types.isProxy(optionsValue)) {
|
|
4192
|
+
throw new SmartdataPersistenceError(
|
|
4193
|
+
'invalid_argument',
|
|
4194
|
+
'Atomic upsert-many options must be an inert ordinary object.',
|
|
4195
|
+
);
|
|
4196
|
+
}
|
|
4197
|
+
for (const key of Reflect.ownKeys(opts)) {
|
|
4198
|
+
if (
|
|
4199
|
+
typeof key !== 'string'
|
|
4200
|
+
|| !['session', 'timeoutMS', 'ordered'].includes(key)
|
|
4201
|
+
) {
|
|
4202
|
+
throw new SmartdataPersistenceError(
|
|
4203
|
+
'invalid_argument',
|
|
4204
|
+
`Atomic upsert-many received unsupported option "${String(key)}".`,
|
|
4205
|
+
);
|
|
4206
|
+
}
|
|
4207
|
+
}
|
|
4208
|
+
if (opts.ordered !== undefined && opts.ordered !== false) {
|
|
4209
|
+
throw new SmartdataPersistenceError(
|
|
4210
|
+
'invalid_argument',
|
|
4211
|
+
'Atomic upsert-many is always unordered; ordered may only be false.',
|
|
4212
|
+
);
|
|
4213
|
+
}
|
|
4214
|
+
}
|
|
4215
|
+
const timeoutMS = requireBoundedInteger(
|
|
4216
|
+
opts?.timeoutMS,
|
|
4217
|
+
'Atomic upsert-many timeoutMS',
|
|
4218
|
+
120_000,
|
|
4219
|
+
);
|
|
4220
|
+
const declaredRoots = (this as any).getDeclaredPersistedRoots() as Set<string>;
|
|
4221
|
+
const declaredAtomicPaths = (this as any).getDeclaredAtomicPaths() as Set<string>;
|
|
4222
|
+
const identityValueTypes = (this as any).getDeclaredIdentityValueTypes() as Map<
|
|
4223
|
+
string,
|
|
4224
|
+
TSmartdataIdentityValueType
|
|
4225
|
+
>;
|
|
4226
|
+
const numericRoots = (this as any).getDeclaredNumericRoots() as Set<string>;
|
|
4227
|
+
const uniqueRoots = new Set(identityValueTypes.keys());
|
|
4228
|
+
const serializedRoots = (this as any).getSerializedPersistedRoots() as Set<string>;
|
|
4229
|
+
const identityDocumentIdField = (this as any).getIdentityDocumentIdField() as
|
|
4230
|
+
| string
|
|
4231
|
+
| undefined;
|
|
4232
|
+
const now = new Date().toISOString();
|
|
4233
|
+
const normalizedOperations: Array<{
|
|
4234
|
+
filter: plugins.mongodb.Filter<plugins.mongodb.Document>;
|
|
4235
|
+
update: plugins.mongodb.UpdateFilter<plugins.mongodb.Document>;
|
|
4236
|
+
}> = [];
|
|
4237
|
+
let bytes = 0;
|
|
4238
|
+
for (const operation of operationsArg) {
|
|
4239
|
+
if (
|
|
4240
|
+
!isPlainObject(operation)
|
|
4241
|
+
|| plugins.nodeUtil.types.isProxy(operation)
|
|
4242
|
+
|| Object.keys(operation).some(
|
|
4243
|
+
(keyArg) => !['filter', 'update'].includes(keyArg),
|
|
4244
|
+
)
|
|
4245
|
+
) {
|
|
4246
|
+
throw new SmartdataPersistenceError(
|
|
4247
|
+
'invalid_argument',
|
|
4248
|
+
'Atomic upsert-many operations must be inert objects with a filter and an update.',
|
|
4249
|
+
);
|
|
4250
|
+
}
|
|
4251
|
+
const normalizedFilter = normalizeStrictFilter(
|
|
4252
|
+
operation.filter as Record<string, unknown>,
|
|
4253
|
+
declaredRoots,
|
|
4254
|
+
declaredAtomicPaths,
|
|
4255
|
+
identityValueTypes,
|
|
4256
|
+
numericRoots,
|
|
4257
|
+
serializedRoots,
|
|
4258
|
+
'Atomic upsert-many',
|
|
4259
|
+
);
|
|
4260
|
+
if ('$expr' in normalizedFilter) {
|
|
4261
|
+
throw new SmartdataPersistenceError(
|
|
4262
|
+
'invalid_argument',
|
|
4263
|
+
'Atomic upsert-many does not support filters that contain $expr.',
|
|
4264
|
+
);
|
|
4265
|
+
}
|
|
4266
|
+
const normalizedUpdate = normalizeAtomicUpdate(
|
|
4267
|
+
operation.update as ISmartdataAtomicUpdate<T>,
|
|
4268
|
+
declaredRoots,
|
|
4269
|
+
declaredAtomicPaths,
|
|
4270
|
+
uniqueRoots,
|
|
4271
|
+
serializedRoots,
|
|
4272
|
+
{ identityValueTypes },
|
|
4273
|
+
);
|
|
4274
|
+
assertSeededIdentitiesMatchFilter(
|
|
4275
|
+
normalizedFilter,
|
|
4276
|
+
normalizedUpdate,
|
|
4277
|
+
uniqueRoots,
|
|
4278
|
+
'Atomic upsert-many',
|
|
4279
|
+
);
|
|
4280
|
+
normalizedUpdate.$set = {
|
|
4281
|
+
...(normalizedUpdate.$set || {}),
|
|
4282
|
+
_updatedAt: now,
|
|
4283
|
+
};
|
|
4284
|
+
normalizedUpdate.$setOnInsert = {
|
|
4285
|
+
...(identityDocumentIdField
|
|
4286
|
+
? {
|
|
4287
|
+
[identityDocumentIdField]: requirePinnedDocumentIdentity(
|
|
4288
|
+
normalizedFilter,
|
|
4289
|
+
identityDocumentIdField,
|
|
4290
|
+
'Atomic upsert-many',
|
|
4291
|
+
),
|
|
4292
|
+
}
|
|
4293
|
+
: {}),
|
|
4294
|
+
...(normalizedUpdate.$setOnInsert || {}),
|
|
4295
|
+
_createdAt: now,
|
|
4296
|
+
};
|
|
4297
|
+
const mappedFilter = mapIdentityFilterToDocumentId(
|
|
4298
|
+
normalizedFilter,
|
|
4299
|
+
identityDocumentIdField,
|
|
4300
|
+
);
|
|
4301
|
+
bytes += plugins.mongodb.BSON.calculateObjectSize({
|
|
4302
|
+
q: mappedFilter,
|
|
4303
|
+
u: normalizedUpdate,
|
|
4304
|
+
});
|
|
4305
|
+
if (bytes > 16 * 1024 * 1024) {
|
|
4306
|
+
throw new SmartdataPersistenceError(
|
|
4307
|
+
'invalid_argument',
|
|
4308
|
+
'Atomic upsert-many accepts at most 16 MiB of serialized operations.',
|
|
4309
|
+
);
|
|
4310
|
+
}
|
|
4311
|
+
normalizedOperations.push({
|
|
4312
|
+
filter: mappedFilter as plugins.mongodb.Filter<plugins.mongodb.Document>,
|
|
4313
|
+
update: normalizedUpdate,
|
|
4314
|
+
});
|
|
4315
|
+
}
|
|
4316
|
+
const collection: SmartdataCollection<T> = (this as any).collection;
|
|
4317
|
+
return await executeAtomicUpsertMany(collection, normalizedOperations, {
|
|
4318
|
+
session: opts?.session,
|
|
4319
|
+
timeoutMS,
|
|
4320
|
+
});
|
|
4321
|
+
}
|
|
4322
|
+
|
|
3824
4323
|
/**
|
|
3825
4324
|
* Computes bounded grouped counts and optional numeric sums server-side.
|
|
3826
4325
|
* Groups by one or two declared top-level fields, always returns a `count`
|
|
@@ -4001,7 +4500,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
4001
4500
|
const collection: SmartdataCollection<T> = (this as any).collection;
|
|
4002
4501
|
const rows = await collection.aggregateGroupedTotals(
|
|
4003
4502
|
filterOption !== undefined
|
|
4004
|
-
?
|
|
4503
|
+
? (this as any).normalizeReadFilter(filterOption)
|
|
4005
4504
|
: undefined,
|
|
4006
4505
|
groupStage,
|
|
4007
4506
|
limit + 1,
|
|
@@ -4073,7 +4572,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
4073
4572
|
): Promise<T[]> {
|
|
4074
4573
|
// Pass session through to findAll for transactional queries
|
|
4075
4574
|
const foundDocs = await (this as any).collection.findAll(
|
|
4076
|
-
|
|
4575
|
+
(this as any).normalizeReadFilter(filterArg),
|
|
4077
4576
|
{ session: opts?.session },
|
|
4078
4577
|
);
|
|
4079
4578
|
const returnArray: T[] = [];
|
|
@@ -4122,7 +4621,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
4122
4621
|
? optionsArg.limit!
|
|
4123
4622
|
: 100;
|
|
4124
4623
|
const limit = Math.min(requestedLimit, 1000);
|
|
4125
|
-
const baseSelector =
|
|
4624
|
+
const baseSelector = (this as any).normalizeReadFilter(optionsArg.filter || {});
|
|
4126
4625
|
|
|
4127
4626
|
let selector: any = baseSelector;
|
|
4128
4627
|
if (optionsArg.cursor) {
|
|
@@ -4194,7 +4693,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
4194
4693
|
const declaredRoots = (this as any).getDeclaredPersistedRoots() as Set<string>;
|
|
4195
4694
|
// Retrieve one document, with optional session for transactions
|
|
4196
4695
|
const foundDoc = await (this as any).collection.findOne(
|
|
4197
|
-
|
|
4696
|
+
(this as any).normalizeReadFilter(filterArg),
|
|
4198
4697
|
{
|
|
4199
4698
|
projection: normalizeProjection(
|
|
4200
4699
|
opts?.projection,
|
|
@@ -4226,7 +4725,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
4226
4725
|
},
|
|
4227
4726
|
): Promise<boolean> {
|
|
4228
4727
|
const foundDoc = await (this as any).collection.findOne(
|
|
4229
|
-
|
|
4728
|
+
(this as any).normalizeReadFilter(filterArg),
|
|
4230
4729
|
{
|
|
4231
4730
|
projection: { _id: 1 },
|
|
4232
4731
|
maxTimeMS: requireBoundedInteger(
|
|
@@ -4291,7 +4790,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
4291
4790
|
const collection: SmartdataCollection<T> = (this as any).collection;
|
|
4292
4791
|
await collection.init();
|
|
4293
4792
|
let rawCursor: plugins.mongodb.FindCursor<any> =
|
|
4294
|
-
collection.mongoDbCollection.find(
|
|
4793
|
+
collection.mongoDbCollection.find((this as any).normalizeReadFilter(filterArg), {
|
|
4295
4794
|
projection,
|
|
4296
4795
|
session,
|
|
4297
4796
|
hint,
|
|
@@ -4365,7 +4864,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
4365
4864
|
}
|
|
4366
4865
|
const timeoutMS = requireBoundedInteger(opts?.timeoutMS === undefined ? 5_000 : opts.timeoutMS, 'timeoutMS', 120_000);
|
|
4367
4866
|
const maxTimeMS = requireBoundedInteger(opts?.maxTimeMS, 'maxTimeMS', 120_000);
|
|
4368
|
-
const filter =
|
|
4867
|
+
const filter = (this as any).normalizeReadFilter(filterArg);
|
|
4369
4868
|
opts.signal?.throwIfAborted();
|
|
4370
4869
|
const collection: SmartdataCollection<T> = (this as any).collection;
|
|
4371
4870
|
if (!collection.isInitializedForCurrentDatabase()) {
|
|
@@ -4410,7 +4909,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
4410
4909
|
): Promise<SmartdataDbWatcher<T>> {
|
|
4411
4910
|
const collection: SmartdataCollection<T> = (this as any).collection;
|
|
4412
4911
|
const watcher: SmartdataDbWatcher<T> = await collection.watch(
|
|
4413
|
-
|
|
4912
|
+
(this as any).normalizeReadFilter(filterArg),
|
|
4414
4913
|
opts || {},
|
|
4415
4914
|
this as any,
|
|
4416
4915
|
);
|
|
@@ -4440,7 +4939,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
4440
4939
|
) {
|
|
4441
4940
|
const hint = normalizeQueryHint(opts?.hint, this);
|
|
4442
4941
|
const collection: SmartdataCollection<T> = (this as any).collection;
|
|
4443
|
-
return await collection.getCount(
|
|
4942
|
+
return await collection.getCount((this as any).normalizeReadFilter(filterArg), {
|
|
4444
4943
|
hint,
|
|
4445
4944
|
limit: requireBoundedInteger(opts?.limit, 'limit', 10_000),
|
|
4446
4945
|
maxTimeMS: requireBoundedInteger(
|
|
@@ -4913,7 +5412,14 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
4913
5412
|
this.identityValueTypes,
|
|
4914
5413
|
);
|
|
4915
5414
|
const identifiableObject: any = {}; // is not exposed to outside, so any is ok here
|
|
5415
|
+
const identityDocumentIdField = getIdentityDocumentIdField(this.constructor);
|
|
4916
5416
|
for (const propertyNameString of this.uniqueIndexes || []) {
|
|
5417
|
+
if (propertyNameString === identityDocumentIdField) {
|
|
5418
|
+
// The declared identity is the stored primary key, so instance writes
|
|
5419
|
+
// and reads address the document through _id.
|
|
5420
|
+
identifiableObject._id = this[propertyNameString];
|
|
5421
|
+
continue;
|
|
5422
|
+
}
|
|
4917
5423
|
identifiableObject[propertyNameString] = this[propertyNameString];
|
|
4918
5424
|
}
|
|
4919
5425
|
if (getOrdinaryPersistencePolicy(this.constructor)?.idType === 'string') {
|