@lossless.org/client 1.13.0 → 1.15.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.collection.d.ts +4 -1
- package/dist_ts/nosqldb/classes.collection.js +7 -2
- package/dist_ts/nosqldb/classes.doc.d.ts +88 -30
- package/dist_ts/nosqldb/classes.doc.js +114 -36
- package/dist_ts/nosqldb/classes.queryplaninspection.d.ts +65 -0
- package/dist_ts/nosqldb/classes.queryplaninspection.js +148 -0
- package/dist_ts/nosqldb/index.d.ts +1 -0
- package/dist_ts/nosqldb/index.js +1 -1
- package/package.json +3 -3
- package/readme.md +6 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/nosqldb/classes.collection.ts +13 -2
- package/ts/nosqldb/classes.doc.ts +257 -70
- package/ts/nosqldb/classes.queryplaninspection.ts +220 -0
- package/ts/nosqldb/index.ts +3 -0
|
@@ -16,6 +16,10 @@ import {
|
|
|
16
16
|
type ISmartdataStoredInspectionOptions, type ISmartdataStoredInspection,
|
|
17
17
|
type ISmartdataStoredEvidenceInspection,
|
|
18
18
|
} from './classes.storedinspection.js';
|
|
19
|
+
import {
|
|
20
|
+
inspectQueryPlan, requireQueryPlanOptions,
|
|
21
|
+
type ISmartdataQueryPlan, type ISmartdataQueryPlanOptions,
|
|
22
|
+
} from './classes.queryplaninspection.js';
|
|
19
23
|
import {
|
|
20
24
|
getIdentityDocumentIdField,
|
|
21
25
|
type IManager,
|
|
@@ -218,6 +222,13 @@ export interface SvDbOptions {
|
|
|
218
222
|
* below this persisted field.
|
|
219
223
|
*/
|
|
220
224
|
atomicPaths?: readonly string[];
|
|
225
|
+
/**
|
|
226
|
+
* Explicit relative nested paths below this persisted field that hold
|
|
227
|
+
* arrays. Each one is a declared nested path exactly like an `atomicPaths`
|
|
228
|
+
* entry, and in addition the array operators `$push`, `$addToSet` and
|
|
229
|
+
* `$pull` and the grouped-totals `unwind` may target it.
|
|
230
|
+
*/
|
|
231
|
+
arrayPaths?: readonly string[];
|
|
221
232
|
/**
|
|
222
233
|
* Declares this persisted top-level field as numeric for strict atomic
|
|
223
234
|
* expression references.
|
|
@@ -282,6 +293,13 @@ export function svDb(options?: SvDbOptions) {
|
|
|
282
293
|
: options.atomicPaths,
|
|
283
294
|
}
|
|
284
295
|
: {}),
|
|
296
|
+
...(options.arrayPaths !== undefined
|
|
297
|
+
? {
|
|
298
|
+
arrayPaths: Array.isArray(options.arrayPaths)
|
|
299
|
+
? [...options.arrayPaths]
|
|
300
|
+
: options.arrayPaths,
|
|
301
|
+
}
|
|
302
|
+
: {}),
|
|
285
303
|
}
|
|
286
304
|
: undefined;
|
|
287
305
|
return (value: undefined, context: ClassFieldDecoratorContext) => {
|
|
@@ -773,16 +791,48 @@ export interface ISmartdataAtomicNumericExpression<T> {
|
|
|
773
791
|
];
|
|
774
792
|
}
|
|
775
793
|
|
|
776
|
-
|
|
794
|
+
/**
|
|
795
|
+
* A path grouped totals may group by or sum: a declared top-level field or a
|
|
796
|
+
* nested path declared through `@svDb({ atomicPaths })` or
|
|
797
|
+
* `@svDb({ arrayPaths })`.
|
|
798
|
+
*/
|
|
799
|
+
export type TSmartdataGroupedTotalsPath<T> = TSmartdataAtomicDocumentPath<T>;
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* A path grouped totals may unwind: a declared top-level array field or a
|
|
803
|
+
* nested path declared through `@svDb({ arrayPaths })`.
|
|
804
|
+
*/
|
|
805
|
+
export type TSmartdataGroupedTotalsUnwindPath<T> =
|
|
806
|
+
| TSmartdataAtomicArrayPath<T>
|
|
807
|
+
| TSmartdataAtomicNestedDocumentPath<T>;
|
|
808
|
+
|
|
809
|
+
export interface ISmartdataGroupedTotalsOptions<
|
|
810
|
+
T,
|
|
811
|
+
TUnwind extends TSmartdataGroupedTotalsUnwindPath<T> = never,
|
|
812
|
+
> {
|
|
813
|
+
/**
|
|
814
|
+
* One or two distinct declared paths whose value combinations form the
|
|
815
|
+
* groups: declared top-level fields or nested paths declared through
|
|
816
|
+
* `@svDb({ atomicPaths })` or `@svDb({ arrayPaths })`.
|
|
817
|
+
*/
|
|
818
|
+
groupBy: ReadonlyArray<TSmartdataGroupedTotalsPath<T>>;
|
|
777
819
|
/**
|
|
778
|
-
*
|
|
779
|
-
*
|
|
820
|
+
* Groups by each element of one array instead of by the array: names
|
|
821
|
+
* exactly one `groupBy` path, which must be a declared top-level array
|
|
822
|
+
* field or a nested path declared through `@svDb({ arrayPaths })`. Every
|
|
823
|
+
* element forms its own row, so `count` counts elements and a document
|
|
824
|
+
* contributes its `sums` once per element; a document whose array is
|
|
825
|
+
* missing, null or empty contributes nothing. The `filter` still selects
|
|
826
|
+
* whole documents before the array is unwound.
|
|
780
827
|
*/
|
|
781
|
-
|
|
828
|
+
unwind?: TUnwind;
|
|
782
829
|
/**
|
|
783
|
-
* Declared numeric top-level fields
|
|
830
|
+
* Declared numeric top-level fields, or declared nested paths holding
|
|
831
|
+
* numbers, to sum per group.
|
|
784
832
|
*/
|
|
785
|
-
sums?: ReadonlyArray<
|
|
833
|
+
sums?: ReadonlyArray<
|
|
834
|
+
TSmartdataNumericDocumentPath<T> | TSmartdataAtomicNestedDocumentPath<T>
|
|
835
|
+
>;
|
|
786
836
|
filter?: MongoFilter<T>;
|
|
787
837
|
/**
|
|
788
838
|
* Required bound on the number of returned groups (1..10000). The group
|
|
@@ -803,18 +853,37 @@ export interface ISmartdataGroupedTotalsOptions<T> {
|
|
|
803
853
|
session?: TSmartdataOrdinarySession;
|
|
804
854
|
}
|
|
805
855
|
|
|
806
|
-
export interface ISmartdataGroupedTotalsRow<
|
|
856
|
+
export interface ISmartdataGroupedTotalsRow<
|
|
857
|
+
T,
|
|
858
|
+
TUnwind extends TSmartdataGroupedTotalsUnwindPath<T> = never,
|
|
859
|
+
> {
|
|
807
860
|
/**
|
|
808
|
-
* The group's key values
|
|
809
|
-
* holding an explicit null merge into
|
|
861
|
+
* The group's key values, keyed by the `groupBy` paths as given. Documents
|
|
862
|
+
* missing a group path and documents holding an explicit null merge into
|
|
863
|
+
* one group reported as null. An unwound top-level array reports the
|
|
864
|
+
* element; nested paths report the stored value untyped.
|
|
810
865
|
*/
|
|
811
|
-
group:
|
|
866
|
+
group:
|
|
867
|
+
& {
|
|
868
|
+
[TKey in TSmartdataDocumentPath<T>]?:
|
|
869
|
+
| (TKey extends TUnwind ? TSmartdataAtomicArrayElement<T[TKey]> : T[TKey])
|
|
870
|
+
| null;
|
|
871
|
+
}
|
|
872
|
+
& Partial<Record<TSmartdataAtomicNestedDocumentPath<T>, unknown>>;
|
|
812
873
|
count: number;
|
|
813
|
-
sums: Partial<
|
|
874
|
+
sums: Partial<
|
|
875
|
+
Record<
|
|
876
|
+
TSmartdataNumericDocumentPath<T> | TSmartdataAtomicNestedDocumentPath<T>,
|
|
877
|
+
number
|
|
878
|
+
>
|
|
879
|
+
>;
|
|
814
880
|
}
|
|
815
881
|
|
|
816
|
-
export interface ISmartdataGroupedTotalsPage<
|
|
817
|
-
|
|
882
|
+
export interface ISmartdataGroupedTotalsPage<
|
|
883
|
+
T,
|
|
884
|
+
TUnwind extends TSmartdataGroupedTotalsUnwindPath<T> = never,
|
|
885
|
+
> {
|
|
886
|
+
rows: Array<ISmartdataGroupedTotalsRow<T, TUnwind>>;
|
|
818
887
|
hasMore: boolean;
|
|
819
888
|
}
|
|
820
889
|
|
|
@@ -826,12 +895,19 @@ export type TSmartdataAtomicArrayPath<T> = {
|
|
|
826
895
|
type TSmartdataAtomicArrayElement<TValue> =
|
|
827
896
|
NonNullable<TValue> extends ReadonlyArray<infer TElement> ? TElement : never;
|
|
828
897
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
898
|
+
/**
|
|
899
|
+
* Array operators take a declared top-level array field, typed by its element,
|
|
900
|
+
* or a nested path below a declared field. A nested path is accepted at run
|
|
901
|
+
* time only when the model declares it through `@svDb({ arrayPaths })`.
|
|
902
|
+
*/
|
|
903
|
+
type TSmartdataAtomicArrayOperation<T> =
|
|
904
|
+
& Partial<{
|
|
905
|
+
[TKey in TSmartdataAtomicArrayPath<T>]: TSmartdataAtomicArrayElement<T[TKey]>;
|
|
906
|
+
}>
|
|
907
|
+
& Partial<Record<TSmartdataAtomicNestedDocumentPath<T>, unknown>>;
|
|
832
908
|
|
|
833
|
-
|
|
834
|
-
|
|
909
|
+
/** The scalar equality operands `$pull` accepts. */
|
|
910
|
+
type TSmartdataAtomicPullScalarValue =
|
|
835
911
|
| string
|
|
836
912
|
| number
|
|
837
913
|
| boolean
|
|
@@ -843,14 +919,22 @@ type TSmartdataAtomicPullScalar<TValue> = Extract<
|
|
|
843
919
|
| plugins.mongodb.Long
|
|
844
920
|
| plugins.mongodb.Int32
|
|
845
921
|
| plugins.mongodb.Double
|
|
846
|
-
| plugins.mongodb.Timestamp
|
|
922
|
+
| plugins.mongodb.Timestamp;
|
|
923
|
+
|
|
924
|
+
type TSmartdataAtomicPullScalar<TValue> = Extract<
|
|
925
|
+
TValue,
|
|
926
|
+
TSmartdataAtomicPullScalarValue
|
|
847
927
|
>;
|
|
848
928
|
|
|
849
|
-
type TSmartdataAtomicArrayPullOperation<T> =
|
|
850
|
-
|
|
851
|
-
|
|
929
|
+
type TSmartdataAtomicArrayPullOperation<T> =
|
|
930
|
+
& Partial<{
|
|
931
|
+
[TKey in TSmartdataAtomicArrayPath<T>]: TSmartdataAtomicPullScalar<
|
|
932
|
+
TSmartdataAtomicArrayElement<T[TKey]>
|
|
933
|
+
>;
|
|
934
|
+
}>
|
|
935
|
+
& Partial<
|
|
936
|
+
Record<TSmartdataAtomicNestedDocumentPath<T>, TSmartdataAtomicPullScalarValue>
|
|
852
937
|
>;
|
|
853
|
-
}>;
|
|
854
938
|
|
|
855
939
|
export interface ISmartdataAtomicUpdate<T> {
|
|
856
940
|
$set?: TSmartdataAtomicSet<T>;
|
|
@@ -872,19 +956,21 @@ export interface ISmartdataAtomicUpdate<T> {
|
|
|
872
956
|
$min?: TSmartdataAtomicMonotonic<T>;
|
|
873
957
|
$setOnInsert?: TSmartdataAtomicSet<T>;
|
|
874
958
|
/**
|
|
875
|
-
* Appends one element to a declared top-level array field
|
|
876
|
-
*
|
|
959
|
+
* Appends one element to a declared top-level array field or a nested
|
|
960
|
+
* path declared through `@svDb({ arrayPaths })`. Modifier documents
|
|
961
|
+
* ($each, $position, $slice, $sort) are rejected.
|
|
877
962
|
*/
|
|
878
963
|
$push?: TSmartdataAtomicArrayOperation<T>;
|
|
879
964
|
/**
|
|
880
|
-
* Adds one element to a declared top-level array field
|
|
881
|
-
* element is already present. Modifier
|
|
965
|
+
* Adds one element to a declared top-level array field or a declared
|
|
966
|
+
* nested array path unless an equal element is already present. Modifier
|
|
967
|
+
* documents are rejected.
|
|
882
968
|
*/
|
|
883
969
|
$addToSet?: TSmartdataAtomicArrayOperation<T>;
|
|
884
970
|
/**
|
|
885
971
|
* Removes every element equal to a scalar operand from a declared
|
|
886
|
-
* top-level array field
|
|
887
|
-
* regular expressions are rejected.
|
|
972
|
+
* top-level array field or a declared nested array path. Operator
|
|
973
|
+
* documents, plain objects, arrays, and regular expressions are rejected.
|
|
888
974
|
*/
|
|
889
975
|
$pull?: TSmartdataAtomicArrayPullOperation<T>;
|
|
890
976
|
}
|
|
@@ -3047,6 +3133,8 @@ const normalizeAtomicUpdate = <T>(
|
|
|
3047
3133
|
updateArg: ISmartdataAtomicUpdate<T>,
|
|
3048
3134
|
declaredRootsArg: Set<string>,
|
|
3049
3135
|
declaredNestedPathsArg: Set<string>,
|
|
3136
|
+
/** Nested paths declared through `@svDb({ arrayPaths })`; array operators may target them. */
|
|
3137
|
+
declaredArrayPathsArg: Set<string>,
|
|
3050
3138
|
uniqueRootsArg: Set<string>,
|
|
3051
3139
|
serializedRootsArg: Set<string>,
|
|
3052
3140
|
/**
|
|
@@ -3160,10 +3248,13 @@ const normalizeAtomicUpdate = <T>(
|
|
|
3160
3248
|
}
|
|
3161
3249
|
occupiedPaths.push({ path, operator });
|
|
3162
3250
|
if (arrayOperators.has(operator)) {
|
|
3163
|
-
if (path !== root) {
|
|
3251
|
+
if (path !== root && !declaredArrayPathsArg.has(path)) {
|
|
3252
|
+
// The shape of a nested value is known only from its declaration, so
|
|
3253
|
+
// a nested path that is not declared as an array is refused rather
|
|
3254
|
+
// than left to fail on the stored value.
|
|
3164
3255
|
throw new SmartdataPersistenceError(
|
|
3165
3256
|
'invalid_argument',
|
|
3166
|
-
`${operator} may only target declared top-level array
|
|
3257
|
+
`${operator} may only target a declared top-level array field or a nested path declared through @svDb({ arrayPaths }); "${path}" is not a declared array path.`,
|
|
3167
3258
|
);
|
|
3168
3259
|
}
|
|
3169
3260
|
if (value === undefined) {
|
|
@@ -3727,6 +3818,32 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
3727
3818
|
return inspectStoredDocumentEvidence(this, dbArg, optionsArg);
|
|
3728
3819
|
}
|
|
3729
3820
|
|
|
3821
|
+
/**
|
|
3822
|
+
* Explains one model query with `executionStats` on `dbArg` and returns the
|
|
3823
|
+
* winning plan's stages and execution totals, so a consumer can prove which
|
|
3824
|
+
* index serves the query. Read-only: nothing is created, bound or hydrated.
|
|
3825
|
+
*/
|
|
3826
|
+
public static async inspectQueryPlan<T>(
|
|
3827
|
+
this: plugins.tsclass.typeFest.Class<T>,
|
|
3828
|
+
dbArg: SmartdataDb,
|
|
3829
|
+
optionsArg: ISmartdataQueryPlanOptions<T>,
|
|
3830
|
+
): Promise<ISmartdataQueryPlan> {
|
|
3831
|
+
const model = this as unknown as typeof SmartDataDbDoc;
|
|
3832
|
+
const options = requireQueryPlanOptions(optionsArg);
|
|
3833
|
+
model.assertHydratedProjection(options.projection);
|
|
3834
|
+
const declaredRoots = model.getDeclaredPersistedRoots();
|
|
3835
|
+
const declaredQueryPaths = model.getDeclaredQueryPaths();
|
|
3836
|
+
return inspectQueryPlan(dbArg, {
|
|
3837
|
+
collectionName: getExpectedCollectionTopologyForModel(model).collectionName,
|
|
3838
|
+
filter: model.normalizeReadFilter(options.filter),
|
|
3839
|
+
sort: normalizeSort(options.sort, declaredRoots, declaredQueryPaths),
|
|
3840
|
+
projection: normalizeProjection(options.projection, declaredRoots, declaredQueryPaths),
|
|
3841
|
+
limit: options.limit,
|
|
3842
|
+
timeoutMS: options.timeoutMS,
|
|
3843
|
+
signal: options.signal,
|
|
3844
|
+
});
|
|
3845
|
+
}
|
|
3846
|
+
|
|
3730
3847
|
public static createInstanceFromMongoDbNativeDoc<T>(
|
|
3731
3848
|
this: plugins.tsclass.typeFest.Class<T>,
|
|
3732
3849
|
mongoDbNativeDocArg: any,
|
|
@@ -3873,21 +3990,41 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
3873
3990
|
return keySets;
|
|
3874
3991
|
}
|
|
3875
3992
|
|
|
3993
|
+
/**
|
|
3994
|
+
* Every nested path the model declares below a persisted field, through
|
|
3995
|
+
* `@svDb({ atomicPaths })` or `@svDb({ arrayPaths })`.
|
|
3996
|
+
*/
|
|
3876
3997
|
private static getDeclaredAtomicPaths(): Set<string> {
|
|
3998
|
+
return new Set([
|
|
3999
|
+
...(this as any).readDeclaredNestedPaths('atomicPaths') as Set<string>,
|
|
4000
|
+
...(this as any).readDeclaredNestedPaths('arrayPaths') as Set<string>,
|
|
4001
|
+
]);
|
|
4002
|
+
}
|
|
4003
|
+
|
|
4004
|
+
/** The nested paths declared through `@svDb({ arrayPaths })` as holding arrays. */
|
|
4005
|
+
private static getDeclaredArrayPaths(): Set<string> {
|
|
4006
|
+
return (this as any).readDeclaredNestedPaths('arrayPaths') as Set<string>;
|
|
4007
|
+
}
|
|
4008
|
+
|
|
4009
|
+
private static readDeclaredNestedPaths(
|
|
4010
|
+
declarationArg: 'atomicPaths' | 'arrayPaths',
|
|
4011
|
+
): Set<string> {
|
|
3877
4012
|
const declaredRoots = (this as any).getDeclaredPersistedRoots() as Set<string>;
|
|
3878
4013
|
const uniqueRoots = (this as any).getDeclaredUniqueRoots() as Set<string>;
|
|
3879
4014
|
const options = (this as any)._svDbOptions as
|
|
3880
4015
|
| Record<string, SvDbOptions>
|
|
3881
4016
|
| undefined;
|
|
3882
|
-
const
|
|
4017
|
+
const label = declarationArg === 'atomicPaths' ? 'Atomic' : 'Array';
|
|
4018
|
+
const nestedPaths = new Set<string>();
|
|
3883
4019
|
for (const [root, fieldOptions] of Object.entries(options || {})) {
|
|
3884
|
-
|
|
4020
|
+
const relativePaths = fieldOptions[declarationArg];
|
|
4021
|
+
if (relativePaths === undefined) {
|
|
3885
4022
|
continue;
|
|
3886
4023
|
}
|
|
3887
|
-
if (!Array.isArray(
|
|
4024
|
+
if (!Array.isArray(relativePaths)) {
|
|
3888
4025
|
throw new SmartdataPersistenceError(
|
|
3889
4026
|
'invalid_configuration',
|
|
3890
|
-
|
|
4027
|
+
`${label} paths for persisted field "${root}" must be an array.`,
|
|
3891
4028
|
);
|
|
3892
4029
|
}
|
|
3893
4030
|
if (
|
|
@@ -3896,20 +4033,20 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
3896
4033
|
) {
|
|
3897
4034
|
throw new SmartdataPersistenceError(
|
|
3898
4035
|
'invalid_configuration',
|
|
3899
|
-
`Serialized persisted field "${root}" may not declare
|
|
4036
|
+
`Serialized persisted field "${root}" may not declare ${label.toLowerCase()} nested paths.`,
|
|
3900
4037
|
);
|
|
3901
4038
|
}
|
|
3902
4039
|
if (uniqueRoots.has(root)) {
|
|
3903
4040
|
throw new SmartdataPersistenceError(
|
|
3904
4041
|
'invalid_configuration',
|
|
3905
|
-
`Immutable @unI() field "${root}" may not declare
|
|
4042
|
+
`Immutable @unI() field "${root}" may not declare ${label.toLowerCase()} nested paths.`,
|
|
3906
4043
|
);
|
|
3907
4044
|
}
|
|
3908
|
-
for (const relativePath of
|
|
4045
|
+
for (const relativePath of relativePaths) {
|
|
3909
4046
|
if (typeof relativePath !== 'string' || relativePath.length === 0) {
|
|
3910
4047
|
throw new SmartdataPersistenceError(
|
|
3911
4048
|
'invalid_configuration',
|
|
3912
|
-
|
|
4049
|
+
`${label} paths for persisted field "${root}" must be non-empty strings.`,
|
|
3913
4050
|
);
|
|
3914
4051
|
}
|
|
3915
4052
|
const fullPath = `${root}.${relativePath}`;
|
|
@@ -3917,20 +4054,20 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
3917
4054
|
requireDeclaredPath(
|
|
3918
4055
|
fullPath,
|
|
3919
4056
|
declaredRoots,
|
|
3920
|
-
|
|
4057
|
+
`${label} path declaration`,
|
|
3921
4058
|
new Set([fullPath]),
|
|
3922
4059
|
);
|
|
3923
4060
|
} catch (errorArg) {
|
|
3924
4061
|
throw new SmartdataPersistenceError(
|
|
3925
4062
|
'invalid_configuration',
|
|
3926
|
-
|
|
4063
|
+
`${label} path declaration "${fullPath}" is invalid.`,
|
|
3927
4064
|
{ cause: errorArg },
|
|
3928
4065
|
);
|
|
3929
4066
|
}
|
|
3930
|
-
|
|
4067
|
+
nestedPaths.add(fullPath);
|
|
3931
4068
|
}
|
|
3932
4069
|
}
|
|
3933
|
-
return
|
|
4070
|
+
return nestedPaths;
|
|
3934
4071
|
}
|
|
3935
4072
|
|
|
3936
4073
|
private static getSerializedPersistedRoots(): Set<string> {
|
|
@@ -4267,6 +4404,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
4267
4404
|
}
|
|
4268
4405
|
const declaredRoots = (this as any).getDeclaredPersistedRoots() as Set<string>;
|
|
4269
4406
|
const declaredAtomicPaths = (this as any).getDeclaredAtomicPaths() as Set<string>;
|
|
4407
|
+
const declaredArrayPaths = (this as any).getDeclaredArrayPaths() as Set<string>;
|
|
4270
4408
|
const identityValueTypes = (this as any).getDeclaredIdentityValueTypes() as Map<
|
|
4271
4409
|
string,
|
|
4272
4410
|
TSmartdataIdentityValueType
|
|
@@ -4302,6 +4440,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
4302
4440
|
updateArg,
|
|
4303
4441
|
declaredRoots,
|
|
4304
4442
|
declaredAtomicPaths,
|
|
4443
|
+
declaredArrayPaths,
|
|
4305
4444
|
uniqueRoots,
|
|
4306
4445
|
serializedRoots,
|
|
4307
4446
|
upserting ? { identityValueTypes } : undefined,
|
|
@@ -4377,6 +4516,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
4377
4516
|
const policy = getOrdinaryPersistencePolicy(this);
|
|
4378
4517
|
const declaredRoots = (this as any).getDeclaredPersistedRoots() as Set<string>;
|
|
4379
4518
|
const declaredAtomicPaths = (this as any).getDeclaredAtomicPaths() as Set<string>;
|
|
4519
|
+
const declaredArrayPaths = (this as any).getDeclaredArrayPaths() as Set<string>;
|
|
4380
4520
|
const identityValueTypes = (this as any).getDeclaredIdentityValueTypes() as Map<
|
|
4381
4521
|
string,
|
|
4382
4522
|
TSmartdataIdentityValueType
|
|
@@ -4403,6 +4543,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
4403
4543
|
updateArg,
|
|
4404
4544
|
declaredRoots,
|
|
4405
4545
|
declaredAtomicPaths,
|
|
4546
|
+
declaredArrayPaths,
|
|
4406
4547
|
uniqueRoots,
|
|
4407
4548
|
serializedRoots,
|
|
4408
4549
|
normalizedOptions.upsert ? { identityValueTypes } : undefined,
|
|
@@ -4733,6 +4874,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
4733
4874
|
const timeoutMS = requireBoundedInteger(opts?.timeoutMS, 'Atomic update-many timeoutMS', 120_000);
|
|
4734
4875
|
const declaredRoots = (this as any).getDeclaredPersistedRoots() as Set<string>;
|
|
4735
4876
|
const declaredAtomicPaths = (this as any).getDeclaredAtomicPaths() as Set<string>;
|
|
4877
|
+
const declaredArrayPaths = (this as any).getDeclaredArrayPaths() as Set<string>;
|
|
4736
4878
|
const uniqueRoots = (this as any).getDeclaredUniqueRoots() as Set<string>;
|
|
4737
4879
|
const identityValueTypes = (this as any).getDeclaredIdentityValueTypes() as Map<
|
|
4738
4880
|
string,
|
|
@@ -4753,6 +4895,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
4753
4895
|
updateArg,
|
|
4754
4896
|
declaredRoots,
|
|
4755
4897
|
declaredAtomicPaths,
|
|
4898
|
+
declaredArrayPaths,
|
|
4756
4899
|
uniqueRoots,
|
|
4757
4900
|
serializedRoots,
|
|
4758
4901
|
);
|
|
@@ -4843,6 +4986,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
4843
4986
|
);
|
|
4844
4987
|
const declaredRoots = (this as any).getDeclaredPersistedRoots() as Set<string>;
|
|
4845
4988
|
const declaredAtomicPaths = (this as any).getDeclaredAtomicPaths() as Set<string>;
|
|
4989
|
+
const declaredArrayPaths = (this as any).getDeclaredArrayPaths() as Set<string>;
|
|
4846
4990
|
const identityValueTypes = (this as any).getDeclaredIdentityValueTypes() as Map<
|
|
4847
4991
|
string,
|
|
4848
4992
|
TSmartdataIdentityValueType
|
|
@@ -4891,6 +5035,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
4891
5035
|
operation.update as ISmartdataAtomicUpdate<T>,
|
|
4892
5036
|
declaredRoots,
|
|
4893
5037
|
declaredAtomicPaths,
|
|
5038
|
+
declaredArrayPaths,
|
|
4894
5039
|
uniqueRoots,
|
|
4895
5040
|
serializedRoots,
|
|
4896
5041
|
{ identityValueTypes },
|
|
@@ -4946,18 +5091,24 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
4946
5091
|
|
|
4947
5092
|
/**
|
|
4948
5093
|
* Computes bounded grouped counts and optional numeric sums server-side.
|
|
4949
|
-
* Groups by one or two declared top-level fields
|
|
4950
|
-
*
|
|
4951
|
-
*
|
|
4952
|
-
*
|
|
4953
|
-
*
|
|
5094
|
+
* Groups by one or two declared paths — top-level fields or nested paths
|
|
5095
|
+
* declared through `@svDb({ atomicPaths })` or `@svDb({ arrayPaths })` —
|
|
5096
|
+
* optionally by each element of one declared array (`unwind`), always
|
|
5097
|
+
* returns a `count` per group, and sums only declared numeric paths. The
|
|
5098
|
+
* `$limit` follows the group stage without a sort, so when more groups
|
|
5099
|
+
* exist than the limit the returned subset is arbitrary. Use
|
|
5100
|
+
* `getGroupedTotalsPage()` when the caller must detect overflow. The
|
|
5101
|
+
* aggregation cursor never leaves SmartData.
|
|
4954
5102
|
*/
|
|
4955
|
-
public static async getGroupedTotals<
|
|
5103
|
+
public static async getGroupedTotals<
|
|
5104
|
+
T,
|
|
5105
|
+
TUnwind extends TSmartdataGroupedTotalsUnwindPath<T> = never,
|
|
5106
|
+
>(
|
|
4956
5107
|
this: plugins.tsclass.typeFest.Class<T>,
|
|
4957
|
-
optionsArg: ISmartdataGroupedTotalsOptions<T>,
|
|
4958
|
-
): Promise<Array<ISmartdataGroupedTotalsRow<T>>> {
|
|
5108
|
+
optionsArg: ISmartdataGroupedTotalsOptions<T, TUnwind>,
|
|
5109
|
+
): Promise<Array<ISmartdataGroupedTotalsRow<T, TUnwind>>> {
|
|
4959
5110
|
const page = await (this as any).getGroupedTotalsPage(optionsArg) as
|
|
4960
|
-
ISmartdataGroupedTotalsPage<T>;
|
|
5111
|
+
ISmartdataGroupedTotalsPage<T, TUnwind>;
|
|
4961
5112
|
return page.rows;
|
|
4962
5113
|
}
|
|
4963
5114
|
|
|
@@ -4966,10 +5117,13 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
4966
5117
|
* reports whether at least one additional group exists beyond the requested
|
|
4967
5118
|
* row limit.
|
|
4968
5119
|
*/
|
|
4969
|
-
public static async getGroupedTotalsPage<
|
|
5120
|
+
public static async getGroupedTotalsPage<
|
|
5121
|
+
T,
|
|
5122
|
+
TUnwind extends TSmartdataGroupedTotalsUnwindPath<T> = never,
|
|
5123
|
+
>(
|
|
4970
5124
|
this: plugins.tsclass.typeFest.Class<T>,
|
|
4971
|
-
optionsArg: ISmartdataGroupedTotalsOptions<T>,
|
|
4972
|
-
): Promise<ISmartdataGroupedTotalsPage<T>> {
|
|
5125
|
+
optionsArg: ISmartdataGroupedTotalsOptions<T, TUnwind>,
|
|
5126
|
+
): Promise<ISmartdataGroupedTotalsPage<T, TUnwind>> {
|
|
4973
5127
|
if ((this as any)[exactPersistencePolicySymbol]) {
|
|
4974
5128
|
throw new SmartdataPersistenceError(
|
|
4975
5129
|
'unsupported_operation',
|
|
@@ -4989,6 +5143,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
4989
5143
|
}
|
|
4990
5144
|
const allowedKeys = new Set([
|
|
4991
5145
|
'groupBy',
|
|
5146
|
+
'unwind',
|
|
4992
5147
|
'sums',
|
|
4993
5148
|
'filter',
|
|
4994
5149
|
'limit',
|
|
@@ -5017,6 +5172,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
5017
5172
|
}
|
|
5018
5173
|
}
|
|
5019
5174
|
const groupByOption = optionsArg.groupBy;
|
|
5175
|
+
const unwindOption = optionsArg.unwind;
|
|
5020
5176
|
const sumsOption = optionsArg.sums;
|
|
5021
5177
|
const filterOption = optionsArg.filter;
|
|
5022
5178
|
const limitOption = optionsArg.limit;
|
|
@@ -5029,6 +5185,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
5029
5185
|
signalOption?.throwIfAborted();
|
|
5030
5186
|
const sessionOption = optionsArg.session;
|
|
5031
5187
|
const declaredRoots = (this as any).getDeclaredPersistedRoots() as Set<string>;
|
|
5188
|
+
const declaredNestedPaths = (this as any).getDeclaredAtomicPaths() as Set<string>;
|
|
5032
5189
|
const serializedRoots = (this as any).getSerializedPersistedRoots() as Set<string>;
|
|
5033
5190
|
const requireDeclaredGroupField = (
|
|
5034
5191
|
fieldArg: unknown,
|
|
@@ -5037,23 +5194,22 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
5037
5194
|
if (typeof fieldArg !== 'string') {
|
|
5038
5195
|
throw new SmartdataPersistenceError(
|
|
5039
5196
|
'invalid_argument',
|
|
5040
|
-
`${labelArg} must name a declared top-level field.`,
|
|
5041
|
-
);
|
|
5042
|
-
}
|
|
5043
|
-
const root = requireDeclaredPath(fieldArg, declaredRoots, labelArg);
|
|
5044
|
-
if (root !== fieldArg) {
|
|
5045
|
-
throw new SmartdataPersistenceError(
|
|
5046
|
-
'invalid_argument',
|
|
5047
|
-
`${labelArg} must name a declared top-level field, not a nested path.`,
|
|
5197
|
+
`${labelArg} must name a declared top-level field or a declared nested path.`,
|
|
5048
5198
|
);
|
|
5049
5199
|
}
|
|
5200
|
+
const root = requireDeclaredPath(
|
|
5201
|
+
fieldArg,
|
|
5202
|
+
declaredRoots,
|
|
5203
|
+
labelArg,
|
|
5204
|
+
declaredNestedPaths,
|
|
5205
|
+
);
|
|
5050
5206
|
if (serializedRoots.has(root)) {
|
|
5051
5207
|
throw new SmartdataPersistenceError(
|
|
5052
5208
|
'unsupported_operation',
|
|
5053
5209
|
`${labelArg} cannot target serialized field "${root}".`,
|
|
5054
5210
|
);
|
|
5055
5211
|
}
|
|
5056
|
-
return
|
|
5212
|
+
return fieldArg;
|
|
5057
5213
|
};
|
|
5058
5214
|
if (
|
|
5059
5215
|
!Array.isArray(groupByOption)
|
|
@@ -5071,11 +5227,14 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
5071
5227
|
if (
|
|
5072
5228
|
groupByFields.length < 1
|
|
5073
5229
|
|| groupByFields.length > 2
|
|
5074
|
-
||
|
|
5230
|
+
|| (
|
|
5231
|
+
groupByFields.length === 2
|
|
5232
|
+
&& pathsConflict(groupByFields[0], groupByFields[1])
|
|
5233
|
+
)
|
|
5075
5234
|
) {
|
|
5076
5235
|
throw new SmartdataPersistenceError(
|
|
5077
5236
|
'invalid_argument',
|
|
5078
|
-
'Grouped totals groupBy fields must be one or two distinct fields.',
|
|
5237
|
+
'Grouped totals groupBy fields must be one or two distinct fields, neither nested in the other.',
|
|
5079
5238
|
);
|
|
5080
5239
|
}
|
|
5081
5240
|
if (sumsOption !== undefined && !Array.isArray(sumsOption)) {
|
|
@@ -5093,6 +5252,27 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
5093
5252
|
'Grouped totals sum fields must be distinct.',
|
|
5094
5253
|
);
|
|
5095
5254
|
}
|
|
5255
|
+
let unwindField: string | undefined;
|
|
5256
|
+
if (unwindOption !== undefined) {
|
|
5257
|
+
if (typeof unwindOption !== 'string' || !groupByFields.includes(unwindOption)) {
|
|
5258
|
+
throw new SmartdataPersistenceError(
|
|
5259
|
+
'invalid_argument',
|
|
5260
|
+
'Grouped totals unwind must name one of the groupBy paths.',
|
|
5261
|
+
);
|
|
5262
|
+
}
|
|
5263
|
+
if (
|
|
5264
|
+
unwindOption.includes('.')
|
|
5265
|
+
&& !((this as any).getDeclaredArrayPaths() as Set<string>).has(unwindOption)
|
|
5266
|
+
) {
|
|
5267
|
+
// A nested value's shape is known only from its declaration; unwinding
|
|
5268
|
+
// a path not declared as an array would count scalars as elements.
|
|
5269
|
+
throw new SmartdataPersistenceError(
|
|
5270
|
+
'invalid_argument',
|
|
5271
|
+
`Grouped totals unwind path "${unwindOption}" is not declared through @svDb({ arrayPaths }).`,
|
|
5272
|
+
);
|
|
5273
|
+
}
|
|
5274
|
+
unwindField = unwindOption;
|
|
5275
|
+
}
|
|
5096
5276
|
const limit = requireBoundedInteger(
|
|
5097
5277
|
limitOption,
|
|
5098
5278
|
'Grouped totals limit',
|
|
@@ -5124,11 +5304,17 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
5124
5304
|
+ `call, such as groupBy: ["${groupByFields[0]}"].`,
|
|
5125
5305
|
);
|
|
5126
5306
|
}
|
|
5307
|
+
// A composite key names its members by position: a group path may be
|
|
5308
|
+
// dotted, and an expression object's field names may not contain a dot.
|
|
5309
|
+
const compositeKeyName = (indexArg: number): string => 'k' + indexArg;
|
|
5127
5310
|
const groupStage: Record<string, unknown> = {
|
|
5128
5311
|
_id: groupByFields.length === 1
|
|
5129
5312
|
? '$' + groupByFields[0]
|
|
5130
5313
|
: Object.fromEntries(
|
|
5131
|
-
groupByFields.map((fieldArg) => [
|
|
5314
|
+
groupByFields.map((fieldArg, indexArg) => [
|
|
5315
|
+
compositeKeyName(indexArg),
|
|
5316
|
+
'$' + fieldArg,
|
|
5317
|
+
]),
|
|
5132
5318
|
),
|
|
5133
5319
|
__groupedTotalsCount: { $sum: 1 },
|
|
5134
5320
|
};
|
|
@@ -5146,6 +5332,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
5146
5332
|
...(maxTimeMS !== undefined ? { maxTimeMS } : {}),
|
|
5147
5333
|
...(timeoutMS !== undefined ? { timeoutMS } : {}),
|
|
5148
5334
|
...(signalOption !== undefined ? { signal: signalOption } : {}),
|
|
5335
|
+
...(unwindField !== undefined ? { unwindPath: unwindField } : {}),
|
|
5149
5336
|
},
|
|
5150
5337
|
);
|
|
5151
5338
|
const hasMore = rows.length > limit;
|
|
@@ -5168,10 +5355,10 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
5168
5355
|
group[groupByFields[0]] = rowArg._id ?? null;
|
|
5169
5356
|
} else {
|
|
5170
5357
|
const compoundKey = (rowArg._id ?? {}) as Record<string, unknown>;
|
|
5171
|
-
for (const fieldArg of groupByFields) {
|
|
5358
|
+
for (const [index, fieldArg] of groupByFields.entries()) {
|
|
5172
5359
|
// Missing and explicit-null values merge into one group that is
|
|
5173
5360
|
// reported as null in both the single-field and compound shapes.
|
|
5174
|
-
group[fieldArg] = compoundKey[
|
|
5361
|
+
group[fieldArg] = compoundKey[compositeKeyName(index)] ?? null;
|
|
5175
5362
|
}
|
|
5176
5363
|
}
|
|
5177
5364
|
const sums: Record<string, number> = {};
|
|
@@ -5188,7 +5375,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
5188
5375
|
'Grouped totals count',
|
|
5189
5376
|
),
|
|
5190
5377
|
sums,
|
|
5191
|
-
} as ISmartdataGroupedTotalsRow<T>;
|
|
5378
|
+
} as ISmartdataGroupedTotalsRow<T, TUnwind>;
|
|
5192
5379
|
});
|
|
5193
5380
|
return {
|
|
5194
5381
|
rows: mappedRows,
|