@lossless.org/client 1.0.0 → 1.2.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/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 +203 -32
- package/dist_ts/nosqldb/classes.collectiontopology.d.ts +11 -0
- package/dist_ts/nosqldb/classes.collectiontopology.js +16 -1
- package/dist_ts/nosqldb/classes.doc.d.ts +66 -0
- package/dist_ts/nosqldb/classes.doc.js +313 -24
- package/dist_ts/nosqldb/classes.exactpersistence.js +10 -2
- package/dist_ts/nosqldb/classes.ordinarypersistence.d.ts +6 -0
- package/dist_ts/nosqldb/classes.ordinarypersistence.js +7 -1
- package/dist_ts/nosqldb/classes.persistence.d.ts +6 -0
- package/dist_ts/nosqldb/classes.persistence.js +13 -1
- package/package.json +1 -1
- package/readme.md +11 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/nosqldb/classes.atomicupdate.ts +67 -0
- package/ts/nosqldb/classes.collection.ts +313 -37
- package/ts/nosqldb/classes.collectiontopology.ts +25 -0
- package/ts/nosqldb/classes.doc.ts +575 -21
- package/ts/nosqldb/classes.exactpersistence.ts +9 -1
- package/ts/nosqldb/classes.ordinarypersistence.ts +8 -0
- package/ts/nosqldb/classes.persistence.ts +17 -0
|
@@ -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';
|
|
@@ -75,6 +78,7 @@ interface ISmartdataDecoratorMetadata extends DecoratorMetadataObject {
|
|
|
75
78
|
saveableProperties?: string[];
|
|
76
79
|
uniqueIndexes?: string[];
|
|
77
80
|
identityValueTypes?: Record<string, TSmartdataIdentityValueType>;
|
|
81
|
+
identityIndexNames?: Record<string, string>;
|
|
78
82
|
regularIndexes?: Array<{field: string, options: IIndexOptions}>;
|
|
79
83
|
compoundIndexes?: Array<{
|
|
80
84
|
name: string;
|
|
@@ -95,6 +99,14 @@ export interface IUnIOptions {
|
|
|
95
99
|
* non-empty string. Numeric identities require an explicit opt-in.
|
|
96
100
|
*/
|
|
97
101
|
valueType?: TSmartdataIdentityValueType;
|
|
102
|
+
/**
|
|
103
|
+
* Name of the single-field ascending unique index that backs this identity.
|
|
104
|
+
* Defaults to `<field>_1`. A collection whose unique index was created by a
|
|
105
|
+
* migration under another name declares that name here: MongoDB refuses a
|
|
106
|
+
* second index over the same key, so without it the identity could not be
|
|
107
|
+
* declared at all.
|
|
108
|
+
*/
|
|
109
|
+
indexName?: string;
|
|
98
110
|
}
|
|
99
111
|
|
|
100
112
|
/**
|
|
@@ -320,11 +332,13 @@ export function unI(optionsArg: IUnIOptions = {}) {
|
|
|
320
332
|
typeof optionsArg !== 'object'
|
|
321
333
|
|| optionsArg === null
|
|
322
334
|
|| Array.isArray(optionsArg)
|
|
323
|
-
|| Object.keys(optionsArg).some(
|
|
335
|
+
|| Object.keys(optionsArg).some(
|
|
336
|
+
(keyArg) => keyArg !== 'valueType' && keyArg !== 'indexName',
|
|
337
|
+
)
|
|
324
338
|
) {
|
|
325
339
|
throw new SmartdataPersistenceError(
|
|
326
340
|
'invalid_configuration',
|
|
327
|
-
'unI options must be an object containing only valueType.',
|
|
341
|
+
'unI options must be an object containing only valueType and indexName.',
|
|
328
342
|
);
|
|
329
343
|
}
|
|
330
344
|
const valueType = optionsArg.valueType ?? 'string';
|
|
@@ -334,6 +348,25 @@ export function unI(optionsArg: IUnIOptions = {}) {
|
|
|
334
348
|
'unI valueType must be "string" or "positiveSafeInteger".',
|
|
335
349
|
);
|
|
336
350
|
}
|
|
351
|
+
const indexName = optionsArg.indexName;
|
|
352
|
+
if (indexName !== undefined) {
|
|
353
|
+
if (
|
|
354
|
+
typeof indexName !== 'string'
|
|
355
|
+
|| indexName.trim().length === 0
|
|
356
|
+
|| indexName.includes('\0')
|
|
357
|
+
) {
|
|
358
|
+
throw new SmartdataPersistenceError(
|
|
359
|
+
'invalid_configuration',
|
|
360
|
+
'unI indexName must be a non-empty index name.',
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
if (indexName === '_id_') {
|
|
364
|
+
throw new SmartdataPersistenceError(
|
|
365
|
+
'invalid_configuration',
|
|
366
|
+
'unI indexName "_id_" is reserved for MongoDB.',
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
337
370
|
return (value: undefined, context: ClassFieldDecoratorContext) => {
|
|
338
371
|
if (context.kind !== 'field') {
|
|
339
372
|
throw new Error('unI can only decorate fields');
|
|
@@ -362,6 +395,24 @@ export function unI(optionsArg: IUnIOptions = {}) {
|
|
|
362
395
|
);
|
|
363
396
|
}
|
|
364
397
|
metadata.identityValueTypes![propName] = valueType;
|
|
398
|
+
// An unnamed declaration writes no entry, so a subclass that redeclares
|
|
399
|
+
// the identity without a name keeps the index the base class named. Only
|
|
400
|
+
// a second, different name is a contradiction worth refusing.
|
|
401
|
+
if (indexName !== undefined) {
|
|
402
|
+
if (
|
|
403
|
+
!Object.prototype.hasOwnProperty.call(metadata, 'identityIndexNames')
|
|
404
|
+
) {
|
|
405
|
+
metadata.identityIndexNames = { ...(metadata.identityIndexNames || {}) };
|
|
406
|
+
}
|
|
407
|
+
const existingIndexName = metadata.identityIndexNames![propName];
|
|
408
|
+
if (existingIndexName && existingIndexName !== indexName) {
|
|
409
|
+
throw new SmartdataPersistenceError(
|
|
410
|
+
'invalid_configuration',
|
|
411
|
+
`Identity field "${propName}" has divergent indexName declarations.`,
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
metadata.identityIndexNames![propName] = indexName;
|
|
415
|
+
}
|
|
365
416
|
|
|
366
417
|
// Also mark as saveable
|
|
367
418
|
if (!Object.prototype.hasOwnProperty.call(metadata, 'saveableProperties')) {
|
|
@@ -590,6 +641,23 @@ export type TSmartdataNumericDocumentPath<T> = {
|
|
|
590
641
|
NonNullable<T[TKey]> extends number ? TKey : never;
|
|
591
642
|
}[TSmartdataDocumentPath<T>];
|
|
592
643
|
|
|
644
|
+
/** Declared top-level fields that carry a monotonic ordering: numbers and dates. */
|
|
645
|
+
export type TSmartdataMonotonicDocumentPath<T> = {
|
|
646
|
+
[TKey in TSmartdataDocumentPath<T>]:
|
|
647
|
+
NonNullable<T[TKey]> extends number
|
|
648
|
+
? TKey
|
|
649
|
+
: NonNullable<T[TKey]> extends Date
|
|
650
|
+
? TKey
|
|
651
|
+
: never;
|
|
652
|
+
}[TSmartdataDocumentPath<T>];
|
|
653
|
+
|
|
654
|
+
type TSmartdataAtomicMonotonic<T> =
|
|
655
|
+
& Partial<{
|
|
656
|
+
[TKey in TSmartdataMonotonicDocumentPath<T>]:
|
|
657
|
+
NonNullable<T[TKey]> extends Date ? Date : number;
|
|
658
|
+
}>
|
|
659
|
+
& Partial<Record<TSmartdataAtomicNestedDocumentPath<T>, number | Date>>;
|
|
660
|
+
|
|
593
661
|
export type TSmartdataAtomicNumericFieldReference<T> =
|
|
594
662
|
`$${TSmartdataNumericDocumentPath<T>}`;
|
|
595
663
|
|
|
@@ -691,6 +759,18 @@ export interface ISmartdataAtomicUpdate<T> {
|
|
|
691
759
|
Record<TSmartdataAtomicDocumentPath<T>, '' | true | 1>
|
|
692
760
|
>;
|
|
693
761
|
$inc?: TSmartdataAtomicIncrement<T>;
|
|
762
|
+
/**
|
|
763
|
+
* Monotonic ceiling for a declared numeric or date field: one server-side
|
|
764
|
+
* compare-and-write that never lowers the stored value, so a retried or
|
|
765
|
+
* out-of-order writer cannot regress a counter. With `upsert: true` a
|
|
766
|
+
* missing field is inserted with the operand.
|
|
767
|
+
*/
|
|
768
|
+
$max?: TSmartdataAtomicMonotonic<T>;
|
|
769
|
+
/**
|
|
770
|
+
* Monotonic floor for a declared numeric or date field: the mirror of
|
|
771
|
+
* `$max`, never raising the stored value.
|
|
772
|
+
*/
|
|
773
|
+
$min?: TSmartdataAtomicMonotonic<T>;
|
|
694
774
|
$setOnInsert?: TSmartdataAtomicSet<T>;
|
|
695
775
|
/**
|
|
696
776
|
* Appends one element to a declared top-level array field. Modifier
|
|
@@ -860,6 +940,30 @@ export interface ISmartdataAtomicUpdateManyResult {
|
|
|
860
940
|
modifiedCount: number;
|
|
861
941
|
}
|
|
862
942
|
|
|
943
|
+
/** One strict selector and its update, both validated before any write. */
|
|
944
|
+
export interface ISmartdataAtomicUpsertManyOperation<T> {
|
|
945
|
+
filter: TSmartdataAtomicFilter<T>;
|
|
946
|
+
update: ISmartdataAtomicUpdate<T>;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
export interface ISmartdataAtomicUpsertManyOptions {
|
|
950
|
+
session?: TSmartdataOrdinarySession;
|
|
951
|
+
/** Client-side bulk operation deadline, including server selection and pool waits. */
|
|
952
|
+
timeoutMS?: number;
|
|
953
|
+
/**
|
|
954
|
+
* Only `false`. The batch is always unordered, so one conflicting entry
|
|
955
|
+
* never hides the outcome of the entries behind it.
|
|
956
|
+
*/
|
|
957
|
+
ordered?: false;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
export interface ISmartdataAtomicUpsertManyResult {
|
|
961
|
+
acknowledged: boolean;
|
|
962
|
+
matchedCount: number;
|
|
963
|
+
modifiedCount: number;
|
|
964
|
+
upsertedCount: number;
|
|
965
|
+
}
|
|
966
|
+
|
|
863
967
|
const normalizeAtomicFindOneAndUpdateOptions = <
|
|
864
968
|
TReturnDocument extends TSmartdataAtomicReturnDocument,
|
|
865
969
|
>(
|
|
@@ -2255,6 +2359,7 @@ const normalizeStrictFilter = (
|
|
|
2255
2359
|
operationLabelArg:
|
|
2256
2360
|
| 'Atomic update'
|
|
2257
2361
|
| 'Atomic update-many'
|
|
2362
|
+
| 'Atomic upsert-many'
|
|
2258
2363
|
| 'Atomic find-one-and-update'
|
|
2259
2364
|
| 'Atomic delete'
|
|
2260
2365
|
| 'Atomic delete-many',
|
|
@@ -2533,6 +2638,15 @@ const normalizeAtomicUpdate = <T>(
|
|
|
2533
2638
|
declaredNestedPathsArg: Set<string>,
|
|
2534
2639
|
uniqueRootsArg: Set<string>,
|
|
2535
2640
|
serializedRootsArg: Set<string>,
|
|
2641
|
+
/**
|
|
2642
|
+
* Supplied only by upserting operations. Its presence allows `$setOnInsert`
|
|
2643
|
+
* to seed declared identity roots on the insert branch; identities stay
|
|
2644
|
+
* immutable on the update branch because MongoDB never applies
|
|
2645
|
+
* `$setOnInsert` to an existing document.
|
|
2646
|
+
*/
|
|
2647
|
+
identitySeedingArg?: {
|
|
2648
|
+
identityValueTypes: ReadonlyMap<string, TSmartdataIdentityValueType>;
|
|
2649
|
+
},
|
|
2536
2650
|
): plugins.mongodb.UpdateFilter<plugins.mongodb.Document> => {
|
|
2537
2651
|
if (
|
|
2538
2652
|
typeof updateArg !== 'object' ||
|
|
@@ -2552,8 +2666,11 @@ const normalizeAtomicUpdate = <T>(
|
|
|
2552
2666
|
'$push',
|
|
2553
2667
|
'$addToSet',
|
|
2554
2668
|
'$pull',
|
|
2669
|
+
'$max',
|
|
2670
|
+
'$min',
|
|
2555
2671
|
]);
|
|
2556
2672
|
const arrayOperators = new Set(['$push', '$addToSet', '$pull']);
|
|
2673
|
+
const monotonicOperators = new Set(['$max', '$min']);
|
|
2557
2674
|
for (const operator of Object.keys(updateArg)) {
|
|
2558
2675
|
if (!allowedOperators.has(operator)) {
|
|
2559
2676
|
throw new SmartdataPersistenceError(
|
|
@@ -2595,9 +2712,30 @@ const normalizeAtomicUpdate = <T>(
|
|
|
2595
2712
|
);
|
|
2596
2713
|
}
|
|
2597
2714
|
if (uniqueRootsArg.has(root)) {
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2715
|
+
if (operator !== '$setOnInsert') {
|
|
2716
|
+
throw new SmartdataPersistenceError(
|
|
2717
|
+
'invalid_argument',
|
|
2718
|
+
`${operator} may not modify immutable @unI() field "${root}".`,
|
|
2719
|
+
);
|
|
2720
|
+
}
|
|
2721
|
+
if (!identitySeedingArg) {
|
|
2722
|
+
throw new SmartdataPersistenceError(
|
|
2723
|
+
'invalid_argument',
|
|
2724
|
+
`$setOnInsert may not seed immutable @unI() field "${root}" without upsert: true.`,
|
|
2725
|
+
);
|
|
2726
|
+
}
|
|
2727
|
+
if (path !== root) {
|
|
2728
|
+
throw new SmartdataPersistenceError(
|
|
2729
|
+
'invalid_argument',
|
|
2730
|
+
`$setOnInsert may not target a nested path below immutable @unI() field "${root}".`,
|
|
2731
|
+
);
|
|
2732
|
+
}
|
|
2733
|
+
// The insert branch must create a complete, addressable document, so
|
|
2734
|
+
// the seeded value passes the same identity validation an insert does.
|
|
2735
|
+
requireUsableIdentityValue(
|
|
2736
|
+
value,
|
|
2737
|
+
`$setOnInsert identity field "${root}"`,
|
|
2738
|
+
identitySeedingArg.identityValueTypes.get(root) ?? 'string',
|
|
2601
2739
|
);
|
|
2602
2740
|
}
|
|
2603
2741
|
const conflict = occupiedPaths.find((occupiedPath) =>
|
|
@@ -2688,6 +2826,34 @@ const normalizeAtomicUpdate = <T>(
|
|
|
2688
2826
|
`${operator} path "${path}" may not be undefined.`,
|
|
2689
2827
|
);
|
|
2690
2828
|
}
|
|
2829
|
+
if (monotonicOperators.has(operator)) {
|
|
2830
|
+
if (typeof value === 'number') {
|
|
2831
|
+
if (!Number.isFinite(value)) {
|
|
2832
|
+
throw new SmartdataPersistenceError(
|
|
2833
|
+
'invalid_argument',
|
|
2834
|
+
`${operator} path "${path}" must be a finite number or a Date.`,
|
|
2835
|
+
);
|
|
2836
|
+
}
|
|
2837
|
+
normalizedOperation[path] = value;
|
|
2838
|
+
continue;
|
|
2839
|
+
}
|
|
2840
|
+
if (
|
|
2841
|
+
!(value instanceof Date)
|
|
2842
|
+
|| !Number.isFinite(value.getTime())
|
|
2843
|
+
) {
|
|
2844
|
+
throw new SmartdataPersistenceError(
|
|
2845
|
+
'invalid_argument',
|
|
2846
|
+
`${operator} path "${path}" must be a finite number or a Date.`,
|
|
2847
|
+
);
|
|
2848
|
+
}
|
|
2849
|
+
// A custom encoder could otherwise rewrite the comparison operand at
|
|
2850
|
+
// serialization time, so the date is captured as an inert snapshot.
|
|
2851
|
+
normalizedOperation[path] = createInertAtomicBsonScalarSnapshot(
|
|
2852
|
+
value,
|
|
2853
|
+
`${operator} path "${path}"`,
|
|
2854
|
+
);
|
|
2855
|
+
continue;
|
|
2856
|
+
}
|
|
2691
2857
|
if (
|
|
2692
2858
|
operator === '$inc' &&
|
|
2693
2859
|
(typeof value !== 'number' || !Number.isFinite(value))
|
|
@@ -2714,6 +2880,151 @@ const normalizeAtomicUpdate = <T>(
|
|
|
2714
2880
|
return normalized as plugins.mongodb.UpdateFilter<plugins.mongodb.Document>;
|
|
2715
2881
|
};
|
|
2716
2882
|
|
|
2883
|
+
/**
|
|
2884
|
+
* Reads a globally constraining equality value for one declared path from an
|
|
2885
|
+
* already normalized filter. `$or` branches are excluded because an equality
|
|
2886
|
+
* inside a single branch does not pin the matched document.
|
|
2887
|
+
*/
|
|
2888
|
+
const readNormalizedFilterEquality = (
|
|
2889
|
+
filterArg: Record<string, unknown>,
|
|
2890
|
+
pathArg: string,
|
|
2891
|
+
): { found: boolean; value?: unknown } => {
|
|
2892
|
+
for (const [key, value] of Object.entries(filterArg)) {
|
|
2893
|
+
if (key === '$and' && Array.isArray(value)) {
|
|
2894
|
+
for (const entry of value) {
|
|
2895
|
+
if (isPlainObject(entry)) {
|
|
2896
|
+
const nested = readNormalizedFilterEquality(
|
|
2897
|
+
entry as Record<string, unknown>,
|
|
2898
|
+
pathArg,
|
|
2899
|
+
);
|
|
2900
|
+
if (nested.found) {
|
|
2901
|
+
return nested;
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
continue;
|
|
2906
|
+
}
|
|
2907
|
+
if (key !== pathArg) {
|
|
2908
|
+
continue;
|
|
2909
|
+
}
|
|
2910
|
+
if (isPlainObject(value)) {
|
|
2911
|
+
const entries = Object.entries(value as Record<string, unknown>);
|
|
2912
|
+
if (entries.length === 1 && entries[0][0] === '$eq') {
|
|
2913
|
+
return { found: true, value: entries[0][1] };
|
|
2914
|
+
}
|
|
2915
|
+
continue;
|
|
2916
|
+
}
|
|
2917
|
+
return { found: true, value };
|
|
2918
|
+
}
|
|
2919
|
+
return { found: false };
|
|
2920
|
+
};
|
|
2921
|
+
|
|
2922
|
+
/**
|
|
2923
|
+
* An upsert seeds its inserted document from the filter's equality conditions
|
|
2924
|
+
* and from `$setOnInsert`. When both name the same identity they must agree,
|
|
2925
|
+
* otherwise the insert branch would create a document its own selector can
|
|
2926
|
+
* never match again.
|
|
2927
|
+
*/
|
|
2928
|
+
const assertSeededIdentitiesMatchFilter = (
|
|
2929
|
+
normalizedFilterArg: Record<string, unknown>,
|
|
2930
|
+
normalizedUpdateArg: Record<string, unknown>,
|
|
2931
|
+
uniqueRootsArg: Set<string>,
|
|
2932
|
+
operationLabelArg: string,
|
|
2933
|
+
): void => {
|
|
2934
|
+
const seeded = normalizedUpdateArg.$setOnInsert;
|
|
2935
|
+
if (!isPlainObject(seeded)) {
|
|
2936
|
+
return;
|
|
2937
|
+
}
|
|
2938
|
+
for (const [path, value] of Object.entries(seeded as Record<string, unknown>)) {
|
|
2939
|
+
if (!uniqueRootsArg.has(path)) {
|
|
2940
|
+
continue;
|
|
2941
|
+
}
|
|
2942
|
+
const anchored = readNormalizedFilterEquality(normalizedFilterArg, path);
|
|
2943
|
+
if (anchored.found && anchored.value !== value) {
|
|
2944
|
+
throw new SmartdataPersistenceError(
|
|
2945
|
+
'invalid_argument',
|
|
2946
|
+
`${operationLabelArg} $setOnInsert identity "${path}" must equal the filter's equality anchor for "${path}".`,
|
|
2947
|
+
);
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2950
|
+
};
|
|
2951
|
+
|
|
2952
|
+
/**
|
|
2953
|
+
* An upsert on a model whose identity owns the document `_id` must pin that
|
|
2954
|
+
* identity by equality: MongoDB derives the inserted primary key from the
|
|
2955
|
+
* filter, so an unpinned upsert would create a document with a generated `_id`
|
|
2956
|
+
* that contradicts the declared mapping. Returns the pinned identity value so
|
|
2957
|
+
* the caller can seed the stored identity field exactly as an unmapped upsert
|
|
2958
|
+
* would have done.
|
|
2959
|
+
*/
|
|
2960
|
+
const requirePinnedDocumentIdentity = (
|
|
2961
|
+
normalizedFilterArg: Record<string, unknown>,
|
|
2962
|
+
identityFieldArg: string,
|
|
2963
|
+
operationLabelArg: string,
|
|
2964
|
+
): unknown => {
|
|
2965
|
+
const anchored = readNormalizedFilterEquality(
|
|
2966
|
+
normalizedFilterArg,
|
|
2967
|
+
identityFieldArg,
|
|
2968
|
+
);
|
|
2969
|
+
if (!anchored.found) {
|
|
2970
|
+
throw new SmartdataPersistenceError(
|
|
2971
|
+
'invalid_argument',
|
|
2972
|
+
`${operationLabelArg} upsert requires an equality anchor on "${identityFieldArg}", which owns the document _id.`,
|
|
2973
|
+
);
|
|
2974
|
+
}
|
|
2975
|
+
return anchored.value;
|
|
2976
|
+
};
|
|
2977
|
+
|
|
2978
|
+
/**
|
|
2979
|
+
* Rewrites equality and operator conditions on a model's `identityAsDocumentId`
|
|
2980
|
+
* field onto `_id`, so the primary key index answers the query instead of a
|
|
2981
|
+
* collection scan. The stored identity field keeps its own value, but every
|
|
2982
|
+
* identity-filtered access is answered through `_id`, so a document written
|
|
2983
|
+
* before the option was declared resolves only if `_id` already equals it.
|
|
2984
|
+
*/
|
|
2985
|
+
const mapIdentityFilterToDocumentId = (
|
|
2986
|
+
filterArg: Record<string, unknown>,
|
|
2987
|
+
identityFieldArg: string | undefined,
|
|
2988
|
+
): Record<string, unknown> => {
|
|
2989
|
+
if (!identityFieldArg || !isPlainObject(filterArg)) {
|
|
2990
|
+
return filterArg;
|
|
2991
|
+
}
|
|
2992
|
+
const carriesExplicitDocumentId = Object.prototype.hasOwnProperty.call(
|
|
2993
|
+
filterArg,
|
|
2994
|
+
'_id',
|
|
2995
|
+
);
|
|
2996
|
+
const mapped: Record<string, unknown> = {};
|
|
2997
|
+
for (const [key, value] of Object.entries(filterArg)) {
|
|
2998
|
+
if (
|
|
2999
|
+
(key === '$and' || key === '$or' || key === '$nor')
|
|
3000
|
+
&& Array.isArray(value)
|
|
3001
|
+
) {
|
|
3002
|
+
mapped[key] = value.map((entryArg) =>
|
|
3003
|
+
isPlainObject(entryArg)
|
|
3004
|
+
? mapIdentityFilterToDocumentId(
|
|
3005
|
+
entryArg as Record<string, unknown>,
|
|
3006
|
+
identityFieldArg,
|
|
3007
|
+
)
|
|
3008
|
+
: entryArg,
|
|
3009
|
+
);
|
|
3010
|
+
continue;
|
|
3011
|
+
}
|
|
3012
|
+
if (key === '$not' && isPlainObject(value)) {
|
|
3013
|
+
mapped[key] = mapIdentityFilterToDocumentId(
|
|
3014
|
+
value as Record<string, unknown>,
|
|
3015
|
+
identityFieldArg,
|
|
3016
|
+
);
|
|
3017
|
+
continue;
|
|
3018
|
+
}
|
|
3019
|
+
if (key === identityFieldArg && !carriesExplicitDocumentId) {
|
|
3020
|
+
mapped._id = value;
|
|
3021
|
+
continue;
|
|
3022
|
+
}
|
|
3023
|
+
mapped[key] = value;
|
|
3024
|
+
}
|
|
3025
|
+
return mapped;
|
|
3026
|
+
};
|
|
3027
|
+
|
|
2717
3028
|
const normalizeProjection = <T>(
|
|
2718
3029
|
projectionArg: TSmartdataProjection<T> | undefined,
|
|
2719
3030
|
declaredRootsArg: Set<string>,
|
|
@@ -3087,6 +3398,27 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
3087
3398
|
return new Set(collection.getBoundModelSchema()?.numericFields || []);
|
|
3088
3399
|
}
|
|
3089
3400
|
|
|
3401
|
+
/**
|
|
3402
|
+
* Returns the declared identity field this model stores as the document
|
|
3403
|
+
* `_id`, or undefined when SmartData owns the primary key.
|
|
3404
|
+
*/
|
|
3405
|
+
private static getIdentityDocumentIdField(): string | undefined {
|
|
3406
|
+
return getIdentityDocumentIdField(this);
|
|
3407
|
+
}
|
|
3408
|
+
|
|
3409
|
+
/**
|
|
3410
|
+
* Converts an ordinary read filter and routes a declared document-id
|
|
3411
|
+
* identity onto `_id`, so reads use the primary key index.
|
|
3412
|
+
*/
|
|
3413
|
+
private static normalizeReadFilter(
|
|
3414
|
+
filterArg: Record<string, any>,
|
|
3415
|
+
): plugins.mongodb.Filter<plugins.mongodb.Document> {
|
|
3416
|
+
return mapIdentityFilterToDocumentId(
|
|
3417
|
+
convertFilterForMongoDb(filterArg),
|
|
3418
|
+
(this as any).getIdentityDocumentIdField() as string | undefined,
|
|
3419
|
+
) as plugins.mongodb.Filter<plugins.mongodb.Document>;
|
|
3420
|
+
}
|
|
3421
|
+
|
|
3090
3422
|
/**
|
|
3091
3423
|
* Returns every equality key set that globally constrains a single document
|
|
3092
3424
|
* of this model: each declared identity field on its own, plus the full key
|
|
@@ -3531,20 +3863,40 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
3531
3863
|
'Atomic update does not support upsert when the filter contains $expr.',
|
|
3532
3864
|
);
|
|
3533
3865
|
}
|
|
3866
|
+
const upserting = normalizedOptions?.upsert === true;
|
|
3534
3867
|
const normalizedUpdate = normalizeAtomicUpdate(
|
|
3535
3868
|
updateArg,
|
|
3536
3869
|
declaredRoots,
|
|
3537
3870
|
declaredAtomicPaths,
|
|
3538
3871
|
uniqueRoots,
|
|
3539
3872
|
serializedRoots,
|
|
3873
|
+
upserting ? { identityValueTypes } : undefined,
|
|
3874
|
+
);
|
|
3875
|
+
assertSeededIdentitiesMatchFilter(
|
|
3876
|
+
normalizedFilter,
|
|
3877
|
+
normalizedUpdate,
|
|
3878
|
+
uniqueRoots,
|
|
3879
|
+
'Atomic update',
|
|
3540
3880
|
);
|
|
3541
3881
|
const now = new Date().toISOString();
|
|
3542
3882
|
normalizedUpdate.$set = {
|
|
3543
3883
|
...(normalizedUpdate.$set || {}),
|
|
3544
3884
|
_updatedAt: now,
|
|
3545
3885
|
};
|
|
3546
|
-
|
|
3886
|
+
const identityDocumentIdField = (this as any).getIdentityDocumentIdField() as
|
|
3887
|
+
| string
|
|
3888
|
+
| undefined;
|
|
3889
|
+
if (upserting) {
|
|
3547
3890
|
normalizedUpdate.$setOnInsert = {
|
|
3891
|
+
...(identityDocumentIdField
|
|
3892
|
+
? {
|
|
3893
|
+
[identityDocumentIdField]: requirePinnedDocumentIdentity(
|
|
3894
|
+
normalizedFilter,
|
|
3895
|
+
identityDocumentIdField,
|
|
3896
|
+
'Atomic update',
|
|
3897
|
+
),
|
|
3898
|
+
}
|
|
3899
|
+
: {}),
|
|
3548
3900
|
...(normalizedUpdate.$setOnInsert || {}),
|
|
3549
3901
|
_createdAt: now,
|
|
3550
3902
|
};
|
|
@@ -3552,7 +3904,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
3552
3904
|
const collection: SmartdataCollection<T> = (this as any).collection;
|
|
3553
3905
|
const result = await executeAtomicUpdate(
|
|
3554
3906
|
collection,
|
|
3555
|
-
normalizedFilter,
|
|
3907
|
+
mapIdentityFilterToDocumentId(normalizedFilter, identityDocumentIdField),
|
|
3556
3908
|
normalizedUpdate,
|
|
3557
3909
|
normalizedOptions,
|
|
3558
3910
|
);
|
|
@@ -3615,6 +3967,13 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
3615
3967
|
declaredAtomicPaths,
|
|
3616
3968
|
uniqueRoots,
|
|
3617
3969
|
serializedRoots,
|
|
3970
|
+
normalizedOptions.upsert ? { identityValueTypes } : undefined,
|
|
3971
|
+
);
|
|
3972
|
+
assertSeededIdentitiesMatchFilter(
|
|
3973
|
+
normalizedFilter,
|
|
3974
|
+
normalizedUpdate,
|
|
3975
|
+
uniqueRoots,
|
|
3976
|
+
'Atomic find-one-and-update',
|
|
3618
3977
|
);
|
|
3619
3978
|
if (policy?.timestamps !== 'none') {
|
|
3620
3979
|
const now = new Date().toISOString();
|
|
@@ -3623,6 +3982,19 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
3623
3982
|
normalizedUpdate.$setOnInsert = { ...(normalizedUpdate.$setOnInsert || {}), _createdAt: now };
|
|
3624
3983
|
}
|
|
3625
3984
|
}
|
|
3985
|
+
const identityDocumentIdField = (this as any).getIdentityDocumentIdField() as
|
|
3986
|
+
| string
|
|
3987
|
+
| undefined;
|
|
3988
|
+
if (normalizedOptions.upsert && identityDocumentIdField) {
|
|
3989
|
+
normalizedUpdate.$setOnInsert = {
|
|
3990
|
+
[identityDocumentIdField]: requirePinnedDocumentIdentity(
|
|
3991
|
+
normalizedFilter,
|
|
3992
|
+
identityDocumentIdField,
|
|
3993
|
+
'Atomic find-one-and-update',
|
|
3994
|
+
),
|
|
3995
|
+
...(normalizedUpdate.$setOnInsert || {}),
|
|
3996
|
+
};
|
|
3997
|
+
}
|
|
3626
3998
|
const normalizedSort = normalizeSort(
|
|
3627
3999
|
normalizedOptions.sort as TSmartdataSort<T> | undefined,
|
|
3628
4000
|
declaredRoots,
|
|
@@ -3631,7 +4003,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
3631
4003
|
const collection: SmartdataCollection<T> = (this as any).collection;
|
|
3632
4004
|
const result = await executeAtomicFindOneAndUpdate(
|
|
3633
4005
|
collection,
|
|
3634
|
-
normalizedFilter,
|
|
4006
|
+
mapIdentityFilterToDocumentId(normalizedFilter, identityDocumentIdField),
|
|
3635
4007
|
normalizedUpdate,
|
|
3636
4008
|
{
|
|
3637
4009
|
returnDocument: normalizedOptions.returnDocument,
|
|
@@ -3688,7 +4060,10 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
3688
4060
|
const collection: SmartdataCollection<T> = (this as any).collection;
|
|
3689
4061
|
const result = await executeAtomicDelete(
|
|
3690
4062
|
collection,
|
|
3691
|
-
|
|
4063
|
+
mapIdentityFilterToDocumentId(
|
|
4064
|
+
normalizedFilter,
|
|
4065
|
+
(this as any).getIdentityDocumentIdField() as string | undefined,
|
|
4066
|
+
),
|
|
3692
4067
|
{ session: opts?.session, timeoutMS: requireBoundedInteger(opts?.timeoutMS, 'Atomic delete timeoutMS', 120_000) },
|
|
3693
4068
|
);
|
|
3694
4069
|
return {
|
|
@@ -3736,7 +4111,10 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
3736
4111
|
const collection: SmartdataCollection<T> = (this as any).collection;
|
|
3737
4112
|
const result = await executeAtomicDeleteMany(
|
|
3738
4113
|
collection,
|
|
3739
|
-
|
|
4114
|
+
mapIdentityFilterToDocumentId(
|
|
4115
|
+
normalizedFilter,
|
|
4116
|
+
(this as any).getIdentityDocumentIdField() as string | undefined,
|
|
4117
|
+
),
|
|
3740
4118
|
{ session: opts?.session, timeoutMS: requireBoundedInteger(opts?.timeoutMS, 'Atomic delete-many timeoutMS', 120_000) },
|
|
3741
4119
|
);
|
|
3742
4120
|
return {
|
|
@@ -3810,7 +4188,10 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
3810
4188
|
const collection: SmartdataCollection<T> = (this as any).collection;
|
|
3811
4189
|
const result = await executeAtomicUpdateMany(
|
|
3812
4190
|
collection,
|
|
3813
|
-
|
|
4191
|
+
mapIdentityFilterToDocumentId(
|
|
4192
|
+
normalizedFilter,
|
|
4193
|
+
(this as any).getIdentityDocumentIdField() as string | undefined,
|
|
4194
|
+
),
|
|
3814
4195
|
normalizedUpdate,
|
|
3815
4196
|
opts,
|
|
3816
4197
|
);
|
|
@@ -3821,6 +4202,172 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
3821
4202
|
};
|
|
3822
4203
|
}
|
|
3823
4204
|
|
|
4205
|
+
/**
|
|
4206
|
+
* Atomically upserts a bounded batch of ordinary model documents in one
|
|
4207
|
+
* round trip. Every entry carries its own strict selector and update, and
|
|
4208
|
+
* each entry applies atomically; the batch as a whole is not isolated unless
|
|
4209
|
+
* the caller supplies a transaction session. The complete batch is validated
|
|
4210
|
+
* before any write, so one invalid entry rejects the call without writing.
|
|
4211
|
+
*/
|
|
4212
|
+
public static async atomicUpsertMany<T>(
|
|
4213
|
+
this: plugins.tsclass.typeFest.Class<T>,
|
|
4214
|
+
operationsArg: ReadonlyArray<ISmartdataAtomicUpsertManyOperation<T>>,
|
|
4215
|
+
opts?: ISmartdataAtomicUpsertManyOptions,
|
|
4216
|
+
): Promise<ISmartdataAtomicUpsertManyResult> {
|
|
4217
|
+
if (getOrdinaryPersistencePolicy(this)) {
|
|
4218
|
+
throw new SmartdataPersistenceError('unsupported_operation',
|
|
4219
|
+
'Validated ordinary models require a transactional postimage update.');
|
|
4220
|
+
}
|
|
4221
|
+
if ((this as any)[exactPersistencePolicySymbol]) {
|
|
4222
|
+
throw new SmartdataPersistenceError(
|
|
4223
|
+
'unsupported_operation',
|
|
4224
|
+
'atomicUpsertMany is unavailable for exact-persistence models.',
|
|
4225
|
+
);
|
|
4226
|
+
}
|
|
4227
|
+
if (
|
|
4228
|
+
!Array.isArray(operationsArg)
|
|
4229
|
+
|| operationsArg.length < 1
|
|
4230
|
+
|| operationsArg.length > 1000
|
|
4231
|
+
) {
|
|
4232
|
+
throw new SmartdataPersistenceError(
|
|
4233
|
+
'invalid_argument',
|
|
4234
|
+
'Atomic upsert-many requires 1 to 1000 operations.',
|
|
4235
|
+
);
|
|
4236
|
+
}
|
|
4237
|
+
if (opts !== undefined) {
|
|
4238
|
+
const optionsValue: unknown = opts;
|
|
4239
|
+
if (!isPlainObject(optionsValue) || plugins.nodeUtil.types.isProxy(optionsValue)) {
|
|
4240
|
+
throw new SmartdataPersistenceError(
|
|
4241
|
+
'invalid_argument',
|
|
4242
|
+
'Atomic upsert-many options must be an inert ordinary object.',
|
|
4243
|
+
);
|
|
4244
|
+
}
|
|
4245
|
+
for (const key of Reflect.ownKeys(opts)) {
|
|
4246
|
+
if (
|
|
4247
|
+
typeof key !== 'string'
|
|
4248
|
+
|| !['session', 'timeoutMS', 'ordered'].includes(key)
|
|
4249
|
+
) {
|
|
4250
|
+
throw new SmartdataPersistenceError(
|
|
4251
|
+
'invalid_argument',
|
|
4252
|
+
`Atomic upsert-many received unsupported option "${String(key)}".`,
|
|
4253
|
+
);
|
|
4254
|
+
}
|
|
4255
|
+
}
|
|
4256
|
+
if (opts.ordered !== undefined && opts.ordered !== false) {
|
|
4257
|
+
throw new SmartdataPersistenceError(
|
|
4258
|
+
'invalid_argument',
|
|
4259
|
+
'Atomic upsert-many is always unordered; ordered may only be false.',
|
|
4260
|
+
);
|
|
4261
|
+
}
|
|
4262
|
+
}
|
|
4263
|
+
const timeoutMS = requireBoundedInteger(
|
|
4264
|
+
opts?.timeoutMS,
|
|
4265
|
+
'Atomic upsert-many timeoutMS',
|
|
4266
|
+
120_000,
|
|
4267
|
+
);
|
|
4268
|
+
const declaredRoots = (this as any).getDeclaredPersistedRoots() as Set<string>;
|
|
4269
|
+
const declaredAtomicPaths = (this as any).getDeclaredAtomicPaths() as Set<string>;
|
|
4270
|
+
const identityValueTypes = (this as any).getDeclaredIdentityValueTypes() as Map<
|
|
4271
|
+
string,
|
|
4272
|
+
TSmartdataIdentityValueType
|
|
4273
|
+
>;
|
|
4274
|
+
const numericRoots = (this as any).getDeclaredNumericRoots() as Set<string>;
|
|
4275
|
+
const uniqueRoots = new Set(identityValueTypes.keys());
|
|
4276
|
+
const serializedRoots = (this as any).getSerializedPersistedRoots() as Set<string>;
|
|
4277
|
+
const identityDocumentIdField = (this as any).getIdentityDocumentIdField() as
|
|
4278
|
+
| string
|
|
4279
|
+
| undefined;
|
|
4280
|
+
const now = new Date().toISOString();
|
|
4281
|
+
const normalizedOperations: Array<{
|
|
4282
|
+
filter: plugins.mongodb.Filter<plugins.mongodb.Document>;
|
|
4283
|
+
update: plugins.mongodb.UpdateFilter<plugins.mongodb.Document>;
|
|
4284
|
+
}> = [];
|
|
4285
|
+
let bytes = 0;
|
|
4286
|
+
for (const operation of operationsArg) {
|
|
4287
|
+
if (
|
|
4288
|
+
!isPlainObject(operation)
|
|
4289
|
+
|| plugins.nodeUtil.types.isProxy(operation)
|
|
4290
|
+
|| Object.keys(operation).some(
|
|
4291
|
+
(keyArg) => !['filter', 'update'].includes(keyArg),
|
|
4292
|
+
)
|
|
4293
|
+
) {
|
|
4294
|
+
throw new SmartdataPersistenceError(
|
|
4295
|
+
'invalid_argument',
|
|
4296
|
+
'Atomic upsert-many operations must be inert objects with a filter and an update.',
|
|
4297
|
+
);
|
|
4298
|
+
}
|
|
4299
|
+
const normalizedFilter = normalizeStrictFilter(
|
|
4300
|
+
operation.filter as Record<string, unknown>,
|
|
4301
|
+
declaredRoots,
|
|
4302
|
+
declaredAtomicPaths,
|
|
4303
|
+
identityValueTypes,
|
|
4304
|
+
numericRoots,
|
|
4305
|
+
serializedRoots,
|
|
4306
|
+
'Atomic upsert-many',
|
|
4307
|
+
);
|
|
4308
|
+
if ('$expr' in normalizedFilter) {
|
|
4309
|
+
throw new SmartdataPersistenceError(
|
|
4310
|
+
'invalid_argument',
|
|
4311
|
+
'Atomic upsert-many does not support filters that contain $expr.',
|
|
4312
|
+
);
|
|
4313
|
+
}
|
|
4314
|
+
const normalizedUpdate = normalizeAtomicUpdate(
|
|
4315
|
+
operation.update as ISmartdataAtomicUpdate<T>,
|
|
4316
|
+
declaredRoots,
|
|
4317
|
+
declaredAtomicPaths,
|
|
4318
|
+
uniqueRoots,
|
|
4319
|
+
serializedRoots,
|
|
4320
|
+
{ identityValueTypes },
|
|
4321
|
+
);
|
|
4322
|
+
assertSeededIdentitiesMatchFilter(
|
|
4323
|
+
normalizedFilter,
|
|
4324
|
+
normalizedUpdate,
|
|
4325
|
+
uniqueRoots,
|
|
4326
|
+
'Atomic upsert-many',
|
|
4327
|
+
);
|
|
4328
|
+
normalizedUpdate.$set = {
|
|
4329
|
+
...(normalizedUpdate.$set || {}),
|
|
4330
|
+
_updatedAt: now,
|
|
4331
|
+
};
|
|
4332
|
+
normalizedUpdate.$setOnInsert = {
|
|
4333
|
+
...(identityDocumentIdField
|
|
4334
|
+
? {
|
|
4335
|
+
[identityDocumentIdField]: requirePinnedDocumentIdentity(
|
|
4336
|
+
normalizedFilter,
|
|
4337
|
+
identityDocumentIdField,
|
|
4338
|
+
'Atomic upsert-many',
|
|
4339
|
+
),
|
|
4340
|
+
}
|
|
4341
|
+
: {}),
|
|
4342
|
+
...(normalizedUpdate.$setOnInsert || {}),
|
|
4343
|
+
_createdAt: now,
|
|
4344
|
+
};
|
|
4345
|
+
const mappedFilter = mapIdentityFilterToDocumentId(
|
|
4346
|
+
normalizedFilter,
|
|
4347
|
+
identityDocumentIdField,
|
|
4348
|
+
);
|
|
4349
|
+
bytes += plugins.mongodb.BSON.calculateObjectSize({
|
|
4350
|
+
q: mappedFilter,
|
|
4351
|
+
u: normalizedUpdate,
|
|
4352
|
+
});
|
|
4353
|
+
if (bytes > 16 * 1024 * 1024) {
|
|
4354
|
+
throw new SmartdataPersistenceError(
|
|
4355
|
+
'invalid_argument',
|
|
4356
|
+
'Atomic upsert-many accepts at most 16 MiB of serialized operations.',
|
|
4357
|
+
);
|
|
4358
|
+
}
|
|
4359
|
+
normalizedOperations.push({
|
|
4360
|
+
filter: mappedFilter as plugins.mongodb.Filter<plugins.mongodb.Document>,
|
|
4361
|
+
update: normalizedUpdate,
|
|
4362
|
+
});
|
|
4363
|
+
}
|
|
4364
|
+
const collection: SmartdataCollection<T> = (this as any).collection;
|
|
4365
|
+
return await executeAtomicUpsertMany(collection, normalizedOperations, {
|
|
4366
|
+
session: opts?.session,
|
|
4367
|
+
timeoutMS,
|
|
4368
|
+
});
|
|
4369
|
+
}
|
|
4370
|
+
|
|
3824
4371
|
/**
|
|
3825
4372
|
* Computes bounded grouped counts and optional numeric sums server-side.
|
|
3826
4373
|
* Groups by one or two declared top-level fields, always returns a `count`
|
|
@@ -4001,7 +4548,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
4001
4548
|
const collection: SmartdataCollection<T> = (this as any).collection;
|
|
4002
4549
|
const rows = await collection.aggregateGroupedTotals(
|
|
4003
4550
|
filterOption !== undefined
|
|
4004
|
-
?
|
|
4551
|
+
? (this as any).normalizeReadFilter(filterOption)
|
|
4005
4552
|
: undefined,
|
|
4006
4553
|
groupStage,
|
|
4007
4554
|
limit + 1,
|
|
@@ -4073,7 +4620,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
4073
4620
|
): Promise<T[]> {
|
|
4074
4621
|
// Pass session through to findAll for transactional queries
|
|
4075
4622
|
const foundDocs = await (this as any).collection.findAll(
|
|
4076
|
-
|
|
4623
|
+
(this as any).normalizeReadFilter(filterArg),
|
|
4077
4624
|
{ session: opts?.session },
|
|
4078
4625
|
);
|
|
4079
4626
|
const returnArray: T[] = [];
|
|
@@ -4122,7 +4669,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
4122
4669
|
? optionsArg.limit!
|
|
4123
4670
|
: 100;
|
|
4124
4671
|
const limit = Math.min(requestedLimit, 1000);
|
|
4125
|
-
const baseSelector =
|
|
4672
|
+
const baseSelector = (this as any).normalizeReadFilter(optionsArg.filter || {});
|
|
4126
4673
|
|
|
4127
4674
|
let selector: any = baseSelector;
|
|
4128
4675
|
if (optionsArg.cursor) {
|
|
@@ -4194,7 +4741,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
4194
4741
|
const declaredRoots = (this as any).getDeclaredPersistedRoots() as Set<string>;
|
|
4195
4742
|
// Retrieve one document, with optional session for transactions
|
|
4196
4743
|
const foundDoc = await (this as any).collection.findOne(
|
|
4197
|
-
|
|
4744
|
+
(this as any).normalizeReadFilter(filterArg),
|
|
4198
4745
|
{
|
|
4199
4746
|
projection: normalizeProjection(
|
|
4200
4747
|
opts?.projection,
|
|
@@ -4226,7 +4773,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
4226
4773
|
},
|
|
4227
4774
|
): Promise<boolean> {
|
|
4228
4775
|
const foundDoc = await (this as any).collection.findOne(
|
|
4229
|
-
|
|
4776
|
+
(this as any).normalizeReadFilter(filterArg),
|
|
4230
4777
|
{
|
|
4231
4778
|
projection: { _id: 1 },
|
|
4232
4779
|
maxTimeMS: requireBoundedInteger(
|
|
@@ -4291,7 +4838,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
4291
4838
|
const collection: SmartdataCollection<T> = (this as any).collection;
|
|
4292
4839
|
await collection.init();
|
|
4293
4840
|
let rawCursor: plugins.mongodb.FindCursor<any> =
|
|
4294
|
-
collection.mongoDbCollection.find(
|
|
4841
|
+
collection.mongoDbCollection.find((this as any).normalizeReadFilter(filterArg), {
|
|
4295
4842
|
projection,
|
|
4296
4843
|
session,
|
|
4297
4844
|
hint,
|
|
@@ -4365,7 +4912,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
4365
4912
|
}
|
|
4366
4913
|
const timeoutMS = requireBoundedInteger(opts?.timeoutMS === undefined ? 5_000 : opts.timeoutMS, 'timeoutMS', 120_000);
|
|
4367
4914
|
const maxTimeMS = requireBoundedInteger(opts?.maxTimeMS, 'maxTimeMS', 120_000);
|
|
4368
|
-
const filter =
|
|
4915
|
+
const filter = (this as any).normalizeReadFilter(filterArg);
|
|
4369
4916
|
opts.signal?.throwIfAborted();
|
|
4370
4917
|
const collection: SmartdataCollection<T> = (this as any).collection;
|
|
4371
4918
|
if (!collection.isInitializedForCurrentDatabase()) {
|
|
@@ -4410,7 +4957,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
4410
4957
|
): Promise<SmartdataDbWatcher<T>> {
|
|
4411
4958
|
const collection: SmartdataCollection<T> = (this as any).collection;
|
|
4412
4959
|
const watcher: SmartdataDbWatcher<T> = await collection.watch(
|
|
4413
|
-
|
|
4960
|
+
(this as any).normalizeReadFilter(filterArg),
|
|
4414
4961
|
opts || {},
|
|
4415
4962
|
this as any,
|
|
4416
4963
|
);
|
|
@@ -4440,7 +4987,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
4440
4987
|
) {
|
|
4441
4988
|
const hint = normalizeQueryHint(opts?.hint, this);
|
|
4442
4989
|
const collection: SmartdataCollection<T> = (this as any).collection;
|
|
4443
|
-
return await collection.getCount(
|
|
4990
|
+
return await collection.getCount((this as any).normalizeReadFilter(filterArg), {
|
|
4444
4991
|
hint,
|
|
4445
4992
|
limit: requireBoundedInteger(opts?.limit, 'limit', 10_000),
|
|
4446
4993
|
maxTimeMS: requireBoundedInteger(
|
|
@@ -4913,7 +5460,14 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
4913
5460
|
this.identityValueTypes,
|
|
4914
5461
|
);
|
|
4915
5462
|
const identifiableObject: any = {}; // is not exposed to outside, so any is ok here
|
|
5463
|
+
const identityDocumentIdField = getIdentityDocumentIdField(this.constructor);
|
|
4916
5464
|
for (const propertyNameString of this.uniqueIndexes || []) {
|
|
5465
|
+
if (propertyNameString === identityDocumentIdField) {
|
|
5466
|
+
// The declared identity is the stored primary key, so instance writes
|
|
5467
|
+
// and reads address the document through _id.
|
|
5468
|
+
identifiableObject._id = this[propertyNameString];
|
|
5469
|
+
continue;
|
|
5470
|
+
}
|
|
4917
5471
|
identifiableObject[propertyNameString] = this[propertyNameString];
|
|
4918
5472
|
}
|
|
4919
5473
|
if (getOrdinaryPersistencePolicy(this.constructor)?.idType === 'string') {
|