@prisma-next/mongo-query-builder 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +144 -0
- package/dist/index.d.mts +1198 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1591 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +46 -0
- package/src/accumulator-helpers.ts +172 -0
- package/src/builder.ts +958 -0
- package/src/exports/index.ts +51 -0
- package/src/expression-helpers.ts +519 -0
- package/src/field-accessor.ts +280 -0
- package/src/markers.ts +25 -0
- package/src/query.ts +55 -0
- package/src/resolve-path.ts +199 -0
- package/src/state-classes.ts +651 -0
- package/src/types.ts +113 -0
- package/src/update-ops.ts +229 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { MongoContract } from '@prisma-next/mongo-contract';
|
|
2
|
+
import type { MongoAggAccumulator, MongoAggExpr } from '@prisma-next/mongo-query-ast/execution';
|
|
3
|
+
|
|
4
|
+
export interface DocField {
|
|
5
|
+
readonly codecId: string;
|
|
6
|
+
readonly nullable: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type NumericField = { readonly codecId: 'mongo/double@1'; readonly nullable: false };
|
|
10
|
+
export type NullableNumericField = { readonly codecId: 'mongo/double@1'; readonly nullable: true };
|
|
11
|
+
export type StringField = { readonly codecId: 'mongo/string@1'; readonly nullable: false };
|
|
12
|
+
export type ArrayField = { readonly codecId: 'mongo/array@1'; readonly nullable: false };
|
|
13
|
+
export type BooleanField = { readonly codecId: 'mongo/bool@1'; readonly nullable: false };
|
|
14
|
+
export type DateField = { readonly codecId: 'mongo/date@1'; readonly nullable: false };
|
|
15
|
+
export type NullableDocField = { readonly codecId: string; readonly nullable: true };
|
|
16
|
+
|
|
17
|
+
export type LiteralValue<F extends DocField> = F extends StringField
|
|
18
|
+
? string
|
|
19
|
+
: F extends NumericField
|
|
20
|
+
? number
|
|
21
|
+
: F extends BooleanField
|
|
22
|
+
? boolean
|
|
23
|
+
: F extends DateField
|
|
24
|
+
? Date
|
|
25
|
+
: unknown;
|
|
26
|
+
|
|
27
|
+
export type DocShape = Record<string, DocField>;
|
|
28
|
+
|
|
29
|
+
type ExtractCodecId<F> = F extends { type: { kind: 'scalar'; codecId: infer C } }
|
|
30
|
+
? C
|
|
31
|
+
: F extends { codecId: infer C extends string }
|
|
32
|
+
? C
|
|
33
|
+
: string;
|
|
34
|
+
|
|
35
|
+
export type ModelToDocShape<
|
|
36
|
+
TContract extends MongoContract,
|
|
37
|
+
ModelName extends string & keyof TContract['models'],
|
|
38
|
+
> = {
|
|
39
|
+
[K in keyof TContract['models'][ModelName]['fields'] & string]: {
|
|
40
|
+
readonly codecId: ExtractCodecId<TContract['models'][ModelName]['fields'][K]>;
|
|
41
|
+
readonly nullable: TContract['models'][ModelName]['fields'][K]['nullable'];
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type ResolveRow<
|
|
46
|
+
Shape extends DocShape,
|
|
47
|
+
CodecTypes extends Record<string, { readonly output: unknown }>,
|
|
48
|
+
> = {
|
|
49
|
+
-readonly [K in keyof Shape & string]: Shape[K]['codecId'] extends keyof CodecTypes
|
|
50
|
+
? Shape[K]['nullable'] extends true
|
|
51
|
+
? CodecTypes[Shape[K]['codecId']]['output'] | null
|
|
52
|
+
: CodecTypes[Shape[K]['codecId']]['output']
|
|
53
|
+
: unknown;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export interface TypedAggExpr<F extends DocField> {
|
|
57
|
+
readonly _field: F;
|
|
58
|
+
readonly node: MongoAggExpr;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface TypedAccumulatorExpr<F extends DocField> {
|
|
62
|
+
readonly _field: F;
|
|
63
|
+
readonly node: MongoAggAccumulator;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type ExtractDocShape<T extends Record<string, TypedAggExpr<DocField>>> = {
|
|
67
|
+
[K in keyof T & string]: T[K]['_field'];
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export type SortSpec<S extends DocShape> = Partial<Record<keyof S & string, 1 | -1>>;
|
|
71
|
+
|
|
72
|
+
export type ProjectedShape<
|
|
73
|
+
Shape extends DocShape,
|
|
74
|
+
Spec extends Record<string, 1 | TypedAggExpr<DocField>>,
|
|
75
|
+
> = {
|
|
76
|
+
[K in keyof Spec & string]: Spec[K] extends 1
|
|
77
|
+
? K extends keyof Shape
|
|
78
|
+
? Shape[K]
|
|
79
|
+
: DocField
|
|
80
|
+
: Spec[K] extends TypedAggExpr<infer F>
|
|
81
|
+
? F
|
|
82
|
+
: DocField;
|
|
83
|
+
} & ('_id' extends keyof Shape
|
|
84
|
+
? '_id' extends keyof Spec
|
|
85
|
+
? Record<keyof never, never>
|
|
86
|
+
: Pick<Shape, '_id'>
|
|
87
|
+
: Record<keyof never, never>);
|
|
88
|
+
|
|
89
|
+
export type GroupSpec = {
|
|
90
|
+
_id: TypedAggExpr<DocField> | null;
|
|
91
|
+
[key: string]: TypedAggExpr<DocField> | TypedAccumulatorExpr<DocField> | null;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export type GroupedDocShape<Spec extends GroupSpec> = {
|
|
95
|
+
[K in keyof Spec & string]: Spec[K] extends TypedAggExpr<infer F>
|
|
96
|
+
? F
|
|
97
|
+
: Spec[K] extends TypedAccumulatorExpr<infer F>
|
|
98
|
+
? F
|
|
99
|
+
: Spec[K] extends null
|
|
100
|
+
? { readonly codecId: 'mongo/null@1'; readonly nullable: true }
|
|
101
|
+
: DocField;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Intentionally identity — full array element type extraction is deferred.
|
|
106
|
+
* Used by `UnwoundShape` so the unwind result shape can be refined later
|
|
107
|
+
* without changing the public API.
|
|
108
|
+
*/
|
|
109
|
+
type UnwrapArrayDocField<F extends DocField> = F;
|
|
110
|
+
|
|
111
|
+
export type UnwoundShape<S extends DocShape, K extends keyof S & string> = {
|
|
112
|
+
[P in keyof S & string]: P extends K ? UnwrapArrayDocField<S[P]> : S[P];
|
|
113
|
+
};
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
MongoUpdatePipelineStage,
|
|
3
|
+
MongoUpdateSpec,
|
|
4
|
+
} from '@prisma-next/mongo-query-ast/execution';
|
|
5
|
+
import type { MongoValue } from '@prisma-next/mongo-value';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Per-field update operations produced by `Expression`'s update methods
|
|
9
|
+
* (`set`, `inc`, `push`, …). A write terminal folds an array of these into a
|
|
10
|
+
* `MongoUpdateSpec` record (`{ $set: { … }, $inc: { … }, … }`) before
|
|
11
|
+
* constructing the underlying `UpdateManyCommand` / `UpdateOneCommand` AST node.
|
|
12
|
+
*
|
|
13
|
+
* One `TypedUpdateOp` value corresponds to one Mongo update operator applied
|
|
14
|
+
* to one field path. The `op` string is the wire-level operator name (`$set`,
|
|
15
|
+
* `$inc`, …); the `path` is the dot-path to the field (or its top-level name).
|
|
16
|
+
*/
|
|
17
|
+
export type TypedUpdateOp =
|
|
18
|
+
| { readonly op: '$set'; readonly path: string; readonly value: MongoValue }
|
|
19
|
+
| { readonly op: '$unset'; readonly path: string }
|
|
20
|
+
| { readonly op: '$rename'; readonly path: string; readonly newName: string }
|
|
21
|
+
| { readonly op: '$inc'; readonly path: string; readonly amount: number }
|
|
22
|
+
| { readonly op: '$mul'; readonly path: string; readonly factor: number }
|
|
23
|
+
| { readonly op: '$min'; readonly path: string; readonly value: MongoValue }
|
|
24
|
+
| { readonly op: '$max'; readonly path: string; readonly value: MongoValue }
|
|
25
|
+
| { readonly op: '$push'; readonly path: string; readonly value: MongoValue }
|
|
26
|
+
| { readonly op: '$addToSet'; readonly path: string; readonly value: MongoValue }
|
|
27
|
+
| { readonly op: '$pop'; readonly path: string; readonly direction: 1 | -1 }
|
|
28
|
+
| { readonly op: '$pull'; readonly path: string; readonly value: MongoValue }
|
|
29
|
+
| { readonly op: '$pullAll'; readonly path: string; readonly values: ReadonlyArray<MongoValue> }
|
|
30
|
+
| { readonly op: '$currentDate'; readonly path: string }
|
|
31
|
+
| { readonly op: '$setOnInsert'; readonly path: string; readonly value: MongoValue };
|
|
32
|
+
|
|
33
|
+
export const setOp = (path: string, value: MongoValue): TypedUpdateOp => ({
|
|
34
|
+
op: '$set',
|
|
35
|
+
path,
|
|
36
|
+
value,
|
|
37
|
+
});
|
|
38
|
+
export const unsetOp = (path: string): TypedUpdateOp => ({ op: '$unset', path });
|
|
39
|
+
export const renameOp = (path: string, newName: string): TypedUpdateOp => ({
|
|
40
|
+
op: '$rename',
|
|
41
|
+
path,
|
|
42
|
+
newName,
|
|
43
|
+
});
|
|
44
|
+
export const incOp = (path: string, amount: number): TypedUpdateOp => ({
|
|
45
|
+
op: '$inc',
|
|
46
|
+
path,
|
|
47
|
+
amount,
|
|
48
|
+
});
|
|
49
|
+
export const mulOp = (path: string, factor: number): TypedUpdateOp => ({
|
|
50
|
+
op: '$mul',
|
|
51
|
+
path,
|
|
52
|
+
factor,
|
|
53
|
+
});
|
|
54
|
+
export const minOp = (path: string, value: MongoValue): TypedUpdateOp => ({
|
|
55
|
+
op: '$min',
|
|
56
|
+
path,
|
|
57
|
+
value,
|
|
58
|
+
});
|
|
59
|
+
export const maxOp = (path: string, value: MongoValue): TypedUpdateOp => ({
|
|
60
|
+
op: '$max',
|
|
61
|
+
path,
|
|
62
|
+
value,
|
|
63
|
+
});
|
|
64
|
+
export const pushOp = (path: string, value: MongoValue): TypedUpdateOp => ({
|
|
65
|
+
op: '$push',
|
|
66
|
+
path,
|
|
67
|
+
value,
|
|
68
|
+
});
|
|
69
|
+
export const addToSetOp = (path: string, value: MongoValue): TypedUpdateOp => ({
|
|
70
|
+
op: '$addToSet',
|
|
71
|
+
path,
|
|
72
|
+
value,
|
|
73
|
+
});
|
|
74
|
+
export const popOp = (path: string, direction: 1 | -1): TypedUpdateOp => ({
|
|
75
|
+
op: '$pop',
|
|
76
|
+
path,
|
|
77
|
+
direction,
|
|
78
|
+
});
|
|
79
|
+
export const pullOp = (path: string, value: MongoValue): TypedUpdateOp => ({
|
|
80
|
+
op: '$pull',
|
|
81
|
+
path,
|
|
82
|
+
value,
|
|
83
|
+
});
|
|
84
|
+
export const pullAllOp = (path: string, values: ReadonlyArray<MongoValue>): TypedUpdateOp => ({
|
|
85
|
+
op: '$pullAll',
|
|
86
|
+
path,
|
|
87
|
+
values,
|
|
88
|
+
});
|
|
89
|
+
export const currentDateOp = (path: string): TypedUpdateOp => ({ op: '$currentDate', path });
|
|
90
|
+
export const setOnInsertOp = (path: string, value: MongoValue): TypedUpdateOp => ({
|
|
91
|
+
op: '$setOnInsert',
|
|
92
|
+
path,
|
|
93
|
+
value,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Per-operator bucket: `{ '<fieldPath>': <operatorValue> }`. Every value is
|
|
98
|
+
* already a `MongoValue` (operators store numbers/strings/booleans/arrays
|
|
99
|
+
* directly), so no blind casts are needed at assignment sites.
|
|
100
|
+
*/
|
|
101
|
+
type UpdateOpBucket = Record<string, MongoValue>;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The full nested shape that `foldUpdateOps` accumulates before returning a
|
|
105
|
+
* `MongoUpdateSpec`: `operator → fieldPath → value`. Each inner bucket is
|
|
106
|
+
* itself a `MongoDocument`-compatible record, which is why the outer map is
|
|
107
|
+
* structurally a `MongoUpdateSpec` (`Record<string, MongoValue>` where every
|
|
108
|
+
* value is a document).
|
|
109
|
+
*/
|
|
110
|
+
type UpdateOpBuckets = Record<string, UpdateOpBucket>;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Fold an array of `TypedUpdateOp` into the non-pipeline variant of
|
|
114
|
+
* `MongoUpdateSpec` (`{ $set: { … }, $inc: { … }, … }`).
|
|
115
|
+
*
|
|
116
|
+
* Throws if the same operator targets the same path twice — a clear authoring
|
|
117
|
+
* error that Mongo would otherwise silently coalesce.
|
|
118
|
+
*/
|
|
119
|
+
export function foldUpdateOps(ops: ReadonlyArray<TypedUpdateOp>): MongoUpdateSpec {
|
|
120
|
+
const buckets: UpdateOpBuckets = {};
|
|
121
|
+
const seen = new Set<string>();
|
|
122
|
+
|
|
123
|
+
const ensure = (key: string): UpdateOpBucket => {
|
|
124
|
+
let bucket = buckets[key];
|
|
125
|
+
if (!bucket) {
|
|
126
|
+
bucket = {};
|
|
127
|
+
buckets[key] = bucket;
|
|
128
|
+
}
|
|
129
|
+
return bucket;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const claim = (op: string, path: string): void => {
|
|
133
|
+
const k = `${op}::${path}`;
|
|
134
|
+
if (seen.has(k)) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`Update spec collision: ${op} on '${path}' was specified more than once. Combine the operations into a single call site.`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
seen.add(k);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
for (const entry of ops) {
|
|
143
|
+
claim(entry.op, entry.path);
|
|
144
|
+
switch (entry.op) {
|
|
145
|
+
case '$set':
|
|
146
|
+
case '$min':
|
|
147
|
+
case '$max':
|
|
148
|
+
case '$push':
|
|
149
|
+
case '$addToSet':
|
|
150
|
+
case '$pull':
|
|
151
|
+
case '$setOnInsert':
|
|
152
|
+
ensure(entry.op)[entry.path] = entry.value;
|
|
153
|
+
break;
|
|
154
|
+
case '$unset':
|
|
155
|
+
ensure('$unset')[entry.path] = '';
|
|
156
|
+
break;
|
|
157
|
+
case '$rename':
|
|
158
|
+
ensure('$rename')[entry.path] = entry.newName;
|
|
159
|
+
break;
|
|
160
|
+
case '$inc':
|
|
161
|
+
ensure('$inc')[entry.path] = entry.amount;
|
|
162
|
+
break;
|
|
163
|
+
case '$mul':
|
|
164
|
+
ensure('$mul')[entry.path] = entry.factor;
|
|
165
|
+
break;
|
|
166
|
+
case '$pop':
|
|
167
|
+
ensure('$pop')[entry.path] = entry.direction;
|
|
168
|
+
break;
|
|
169
|
+
case '$pullAll':
|
|
170
|
+
ensure('$pullAll')[entry.path] = entry.values;
|
|
171
|
+
break;
|
|
172
|
+
case '$currentDate':
|
|
173
|
+
ensure('$currentDate')[entry.path] = true;
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return buckets;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export type UpdaterItem = TypedUpdateOp | MongoUpdatePipelineStage;
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* The return type for updater callbacks. Typed as a union of homogeneous
|
|
185
|
+
* arrays so mixed-shape updaters (operator + pipeline stage in the same
|
|
186
|
+
* array) are a compile error. The runtime guard in `resolveUpdaterResult`
|
|
187
|
+
* remains as defence-in-depth.
|
|
188
|
+
*/
|
|
189
|
+
export type UpdaterResult = ReadonlyArray<TypedUpdateOp> | ReadonlyArray<MongoUpdatePipelineStage>;
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Classify an array of updater items and produce a `MongoUpdateSpec`.
|
|
193
|
+
*
|
|
194
|
+
* - All `TypedUpdateOp` → fold via `foldUpdateOps` (classic `{ $set, $inc, … }`)
|
|
195
|
+
* - All `MongoUpdatePipelineStage` → return as-is (pipeline-style update)
|
|
196
|
+
* - Mixed → throw (also a type error at the call site via the union shape)
|
|
197
|
+
*/
|
|
198
|
+
export function resolveUpdaterResult(items: ReadonlyArray<UpdaterItem>): MongoUpdateSpec {
|
|
199
|
+
if (items.length === 0) {
|
|
200
|
+
throw new Error(
|
|
201
|
+
'Updater returned no operations. Return at least one update from the callback (e.g. `[f.amount.set(0)]`).',
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const isOp = (item: UpdaterItem): item is TypedUpdateOp =>
|
|
206
|
+
'op' in item && typeof (item as TypedUpdateOp).op === 'string';
|
|
207
|
+
|
|
208
|
+
const first = items[0];
|
|
209
|
+
if (first === undefined) {
|
|
210
|
+
throw new Error('Unreachable: items.length > 0 but first is undefined');
|
|
211
|
+
}
|
|
212
|
+
const firstIsOp = isOp(first);
|
|
213
|
+
|
|
214
|
+
for (let i = 1; i < items.length; i++) {
|
|
215
|
+
const item = items[i];
|
|
216
|
+
if (item === undefined) continue;
|
|
217
|
+
if (isOp(item) !== firstIsOp) {
|
|
218
|
+
throw new Error(
|
|
219
|
+
'Cannot mix TypedUpdateOp values and pipeline stages in a single updater. ' +
|
|
220
|
+
'Use either `[f.amount.set(0)]` (operator form) or `[f.stage.set({...})]` (pipeline form), not both.',
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (firstIsOp) {
|
|
226
|
+
return foldUpdateOps(items as ReadonlyArray<TypedUpdateOp>);
|
|
227
|
+
}
|
|
228
|
+
return items as ReadonlyArray<MongoUpdatePipelineStage>;
|
|
229
|
+
}
|