@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
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
MongoAggExpr,
|
|
3
|
+
MongoFilterExpr,
|
|
4
|
+
MongoUpdatePipelineStage,
|
|
5
|
+
} from '@prisma-next/mongo-query-ast/execution';
|
|
6
|
+
import {
|
|
7
|
+
MongoAddFieldsStage,
|
|
8
|
+
MongoAggFieldRef,
|
|
9
|
+
MongoExistsExpr,
|
|
10
|
+
MongoFieldFilter,
|
|
11
|
+
MongoProjectStage,
|
|
12
|
+
MongoReplaceRootStage,
|
|
13
|
+
} from '@prisma-next/mongo-query-ast/execution';
|
|
14
|
+
import type { MongoValue } from '@prisma-next/mongo-value';
|
|
15
|
+
import type { NestedDocShape, ObjectField, ResolvePath, ValidPaths } from './resolve-path';
|
|
16
|
+
import type { DocField, DocShape, TypedAggExpr } from './types';
|
|
17
|
+
import type { TypedUpdateOp } from './update-ops';
|
|
18
|
+
import {
|
|
19
|
+
addToSetOp,
|
|
20
|
+
currentDateOp,
|
|
21
|
+
incOp,
|
|
22
|
+
maxOp,
|
|
23
|
+
minOp,
|
|
24
|
+
mulOp,
|
|
25
|
+
popOp,
|
|
26
|
+
pullAllOp,
|
|
27
|
+
pullOp,
|
|
28
|
+
pushOp,
|
|
29
|
+
renameOp,
|
|
30
|
+
setOnInsertOp,
|
|
31
|
+
setOp,
|
|
32
|
+
unsetOp,
|
|
33
|
+
} from './update-ops';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Operator surface for leaf (scalar) paths — today's full set: filter,
|
|
37
|
+
* update, and aggregation operators. Returned by `Expression<F>` for any
|
|
38
|
+
* `F extends DocField` that is not an `ObjectField<…>` sub-tree.
|
|
39
|
+
*
|
|
40
|
+
* Operator surfaces are intentionally not trait-gated by codec in this
|
|
41
|
+
* revision — tracked on Linear as TML-2259 (scope extended to cover the
|
|
42
|
+
* query-builder's `Expression<F>`). Calling, e.g. `.inc(1)` on a
|
|
43
|
+
* string-typed expression compiles; the runtime relies on Mongo to
|
|
44
|
+
* surface the error. Trait-gating can be tightened in a follow-up
|
|
45
|
+
* without changing the accessor's public shape.
|
|
46
|
+
*/
|
|
47
|
+
export interface LeafExpression<F extends DocField> extends TypedAggExpr<F> {
|
|
48
|
+
readonly _path: string;
|
|
49
|
+
|
|
50
|
+
// Filter operators
|
|
51
|
+
eq(value: MongoValue): MongoFilterExpr;
|
|
52
|
+
ne(value: MongoValue): MongoFilterExpr;
|
|
53
|
+
gt(value: MongoValue): MongoFilterExpr;
|
|
54
|
+
gte(value: MongoValue): MongoFilterExpr;
|
|
55
|
+
lt(value: MongoValue): MongoFilterExpr;
|
|
56
|
+
lte(value: MongoValue): MongoFilterExpr;
|
|
57
|
+
in(values: ReadonlyArray<MongoValue>): MongoFilterExpr;
|
|
58
|
+
nin(values: ReadonlyArray<MongoValue>): MongoFilterExpr;
|
|
59
|
+
exists(flag?: boolean): MongoFilterExpr;
|
|
60
|
+
|
|
61
|
+
// Update operators ($set family)
|
|
62
|
+
set(value: MongoValue): TypedUpdateOp;
|
|
63
|
+
unset(): TypedUpdateOp;
|
|
64
|
+
rename(newName: string): TypedUpdateOp;
|
|
65
|
+
|
|
66
|
+
// Numeric update operators
|
|
67
|
+
inc(amount: number): TypedUpdateOp;
|
|
68
|
+
mul(factor: number): TypedUpdateOp;
|
|
69
|
+
min(value: MongoValue): TypedUpdateOp;
|
|
70
|
+
max(value: MongoValue): TypedUpdateOp;
|
|
71
|
+
|
|
72
|
+
// Array update operators
|
|
73
|
+
push(value: MongoValue): TypedUpdateOp;
|
|
74
|
+
addToSet(value: MongoValue): TypedUpdateOp;
|
|
75
|
+
pop(direction?: 1 | -1): TypedUpdateOp;
|
|
76
|
+
pull(value: MongoValue): TypedUpdateOp;
|
|
77
|
+
pullAll(values: ReadonlyArray<MongoValue>): TypedUpdateOp;
|
|
78
|
+
|
|
79
|
+
// Date / upsert helpers
|
|
80
|
+
currentDate(): TypedUpdateOp;
|
|
81
|
+
setOnInsert(value: MongoValue): TypedUpdateOp;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Operator surface for non-leaf (value-object) paths — `f('address')`
|
|
86
|
+
* when `address` is a `ContractValueObject`. Intentionally minimal: the
|
|
87
|
+
* whole-value ops that make sense on a structured sub-document
|
|
88
|
+
* (`set`/`unset`/`exists`, null presence via `eq(null)`/`ne(null)`). Field-
|
|
89
|
+
* level ops belong on the constituent leaves (`f('address.city')`).
|
|
90
|
+
*
|
|
91
|
+
* The aggregation `node` is still present (`TypedAggExpr<ObjectField<N>>`)
|
|
92
|
+
* so the value object can be piped through `$addFields` /
|
|
93
|
+
* `$replaceRoot` / etc. as-is.
|
|
94
|
+
*/
|
|
95
|
+
export interface ObjectExpression<N extends NestedDocShape> extends TypedAggExpr<ObjectField<N>> {
|
|
96
|
+
readonly _path: string;
|
|
97
|
+
|
|
98
|
+
exists(flag?: boolean): MongoFilterExpr;
|
|
99
|
+
eq(value: null): MongoFilterExpr;
|
|
100
|
+
ne(value: null): MongoFilterExpr;
|
|
101
|
+
|
|
102
|
+
set(value: MongoValue): TypedUpdateOp;
|
|
103
|
+
unset(): TypedUpdateOp;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The unified field accessor expression returned by `FieldAccessor` (per
|
|
108
|
+
* [ADR 180](../../../../docs/architecture%20docs/adrs/ADR%20180%20-%20Dot-path%20field%20accessor.md)).
|
|
109
|
+
*
|
|
110
|
+
* Resolves to `ObjectExpression<Sub>` when `F` is an `ObjectField<Sub>`
|
|
111
|
+
* (non-leaf path), otherwise to `LeafExpression<F>` (the full operator
|
|
112
|
+
* surface). The conditional is driven off the `fields` marker that
|
|
113
|
+
* `ObjectField` adds to `DocField`, so existing code that uses plain
|
|
114
|
+
* `DocField` shapes continues to resolve to `LeafExpression`.
|
|
115
|
+
*/
|
|
116
|
+
export type Expression<F extends DocField> =
|
|
117
|
+
F extends ObjectField<infer N> ? ObjectExpression<N> : LeafExpression<F>;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Emitters for MongoDB update-pipeline stages (`$addFields`/`$set`,
|
|
121
|
+
* `$project`/`$unset`, `$replaceRoot`/`$replaceWith`). These return
|
|
122
|
+
* `MongoUpdatePipelineStage` nodes and let an updater callback express
|
|
123
|
+
* the pipeline-form update as an alternative to the typed-operator form.
|
|
124
|
+
*
|
|
125
|
+
* The two forms are mutually exclusive per updater call: `resolveUpdaterResult`
|
|
126
|
+
* rejects arrays that mix `TypedUpdateOp` and `MongoUpdatePipelineStage`
|
|
127
|
+
* entries with a clear error — an updater callback must return either all
|
|
128
|
+
* typed ops or all pipeline stages. Pick the form that matches the update
|
|
129
|
+
* you want and commit to it for that call site.
|
|
130
|
+
*
|
|
131
|
+
* Accessible via `f.stage` on the `FieldAccessor`.
|
|
132
|
+
*/
|
|
133
|
+
export interface StageEmitters {
|
|
134
|
+
set(fields: Record<string, MongoAggExpr>): MongoUpdatePipelineStage;
|
|
135
|
+
unset(...paths: ReadonlyArray<string>): MongoUpdatePipelineStage;
|
|
136
|
+
replaceRoot(newRoot: MongoAggExpr): MongoUpdatePipelineStage;
|
|
137
|
+
replaceWith(newRoot: MongoAggExpr): MongoUpdatePipelineStage;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function buildStageEmitters(): StageEmitters {
|
|
141
|
+
return {
|
|
142
|
+
set: (fields) => new MongoAddFieldsStage(fields),
|
|
143
|
+
unset: (...paths) => {
|
|
144
|
+
const spec: Record<string, 0> = {};
|
|
145
|
+
for (const p of paths) {
|
|
146
|
+
spec[p] = 0;
|
|
147
|
+
}
|
|
148
|
+
return new MongoProjectStage(spec);
|
|
149
|
+
},
|
|
150
|
+
replaceRoot: (newRoot) => new MongoReplaceRootStage(newRoot),
|
|
151
|
+
replaceWith: (newRoot) => new MongoReplaceRootStage(newRoot),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The unified `FieldAccessor` per ADR 180.
|
|
157
|
+
*
|
|
158
|
+
* - Property access (`f.status`) returns an `Expression<F>` whose codec
|
|
159
|
+
* comes from the current pipeline shape `S`.
|
|
160
|
+
* - Callable form (`f('address.city')`) returns an `Expression<ResolvePath<N, P>>`
|
|
161
|
+
* where `N` is the nested shape carrying value-object sub-shapes.
|
|
162
|
+
* Paths that don't exist in `N` are rejected with a compile-time error
|
|
163
|
+
* (via `P extends ValidPaths<N>`). Non-leaf paths like `f('address')`
|
|
164
|
+
* resolve to an `ObjectExpression` whose reduced surface covers the
|
|
165
|
+
* whole-value operations (`set`, `unset`, `exists`, `eq(null)`,
|
|
166
|
+
* `ne(null)`).
|
|
167
|
+
* - `f.rawPath('path')` is a deliberate escape hatch that skips path
|
|
168
|
+
* validation and returns a `LeafExpression<F>` for the given string.
|
|
169
|
+
* Intended for migration authoring where the target field is not yet
|
|
170
|
+
* part of the typed contract (e.g. a backfill writing a newly-added
|
|
171
|
+
* column before the contract hash rolls forward). The method name is
|
|
172
|
+
* deliberately `rawPath` rather than `raw` so it does not shadow a
|
|
173
|
+
* legitimate top-level `raw` field on a user model.
|
|
174
|
+
* - `f.stage` exposes pipeline-style update emitters (`$set`, `$unset`,
|
|
175
|
+
* `$replaceRoot`, `$replaceWith`).
|
|
176
|
+
*
|
|
177
|
+
* When `N` is `Record<string, never>` (the default — e.g. after a
|
|
178
|
+
* replacement stage like `$group` / `$project` / `$replaceRoot`),
|
|
179
|
+
* `ValidPaths<N>` is `never` and the callable form is effectively
|
|
180
|
+
* disabled at the type level. This keeps the builder sound downstream of
|
|
181
|
+
* stages that invalidate the original document's nested-path tree.
|
|
182
|
+
* `f.rawPath(...)` remains available in that state for callers that need
|
|
183
|
+
* an explicit unvalidated path.
|
|
184
|
+
*/
|
|
185
|
+
export type FieldAccessor<S extends DocShape, N extends NestedDocShape = Record<string, never>> = {
|
|
186
|
+
readonly [K in keyof S & string]: Expression<S[K]>;
|
|
187
|
+
} & (<P extends ValidPaths<N>>(path: P) => Expression<ResolvePath<N, P>>) & {
|
|
188
|
+
readonly stage: StageEmitters;
|
|
189
|
+
/**
|
|
190
|
+
* Escape hatch: build a `LeafExpression<F>` for an unvalidated string
|
|
191
|
+
* path. Use only when the path is intentionally outside the typed
|
|
192
|
+
* model surface — data-migration authoring is the canonical case
|
|
193
|
+
* (e.g. backfilling a field that is not yet in the contract). Default
|
|
194
|
+
* `F` is the opaque `DocField`; callers can narrow via the explicit
|
|
195
|
+
* generic: `f.rawPath<StringField>("status").set("active")`.
|
|
196
|
+
*
|
|
197
|
+
* The method is named `rawPath` (not `raw`) so a user model with a
|
|
198
|
+
* top-level `raw` field still resolves `f.raw` to the field-expression
|
|
199
|
+
* property, not to this escape hatch. Does not participate in
|
|
200
|
+
* `ValidPaths<N>` / `ResolvePath<N, P>` — the path is passed through
|
|
201
|
+
* verbatim and no IDE autocomplete is offered.
|
|
202
|
+
*/
|
|
203
|
+
rawPath<F extends DocField = DocField>(path: string): LeafExpression<F>;
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
function buildExpression<F extends DocField>(path: string): Expression<F> {
|
|
207
|
+
// The runtime object carries the full operator surface unconditionally;
|
|
208
|
+
// `ObjectExpression` is a strict subset of `LeafExpression`, so a single
|
|
209
|
+
// implementation satisfies both type-level shapes. Compile-time gating
|
|
210
|
+
// prevents misuse of leaf-only operators on object paths.
|
|
211
|
+
return {
|
|
212
|
+
_field: undefined as never,
|
|
213
|
+
_path: path,
|
|
214
|
+
node: MongoAggFieldRef.of(path),
|
|
215
|
+
|
|
216
|
+
eq: (value: MongoValue) => MongoFieldFilter.eq(path, value),
|
|
217
|
+
ne: (value: MongoValue) => MongoFieldFilter.neq(path, value),
|
|
218
|
+
gt: (value: MongoValue) => MongoFieldFilter.gt(path, value),
|
|
219
|
+
gte: (value: MongoValue) => MongoFieldFilter.gte(path, value),
|
|
220
|
+
lt: (value: MongoValue) => MongoFieldFilter.lt(path, value),
|
|
221
|
+
lte: (value: MongoValue) => MongoFieldFilter.lte(path, value),
|
|
222
|
+
in: (values: ReadonlyArray<MongoValue>) => MongoFieldFilter.in(path, values),
|
|
223
|
+
nin: (values: ReadonlyArray<MongoValue>) => MongoFieldFilter.nin(path, values),
|
|
224
|
+
exists: (flag?: boolean) =>
|
|
225
|
+
flag === false ? MongoExistsExpr.notExists(path) : MongoExistsExpr.exists(path),
|
|
226
|
+
|
|
227
|
+
set: (value: MongoValue) => setOp(path, value),
|
|
228
|
+
unset: () => unsetOp(path),
|
|
229
|
+
rename: (newName: string) => renameOp(path, newName),
|
|
230
|
+
|
|
231
|
+
inc: (amount: number) => incOp(path, amount),
|
|
232
|
+
mul: (factor: number) => mulOp(path, factor),
|
|
233
|
+
min: (value: MongoValue) => minOp(path, value),
|
|
234
|
+
max: (value: MongoValue) => maxOp(path, value),
|
|
235
|
+
|
|
236
|
+
push: (value: MongoValue) => pushOp(path, value),
|
|
237
|
+
addToSet: (value: MongoValue) => addToSetOp(path, value),
|
|
238
|
+
pop: (direction: 1 | -1 = 1) => popOp(path, direction),
|
|
239
|
+
pull: (value: MongoValue) => pullOp(path, value),
|
|
240
|
+
pullAll: (values: ReadonlyArray<MongoValue>) => pullAllOp(path, values),
|
|
241
|
+
|
|
242
|
+
currentDate: () => currentDateOp(path),
|
|
243
|
+
setOnInsert: (value: MongoValue) => setOnInsertOp(path, value),
|
|
244
|
+
} as unknown as Expression<F>;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Construct a unified `FieldAccessor<S, N>` proxy. Property access creates
|
|
249
|
+
* an `Expression` using the property name as the field path; callable
|
|
250
|
+
* form accepts a dot-path string validated against `N` at compile time.
|
|
251
|
+
*
|
|
252
|
+
* The proxy target is a function so the resulting object is both callable
|
|
253
|
+
* and indexable. Symbol-keyed accesses (e.g. `Symbol.toPrimitive`) return
|
|
254
|
+
* `undefined` to keep accidental coercion behaviour unsurprising —
|
|
255
|
+
* matching the previous `FieldProxy` / `FilterProxy` semantics.
|
|
256
|
+
*/
|
|
257
|
+
export function createFieldAccessor<
|
|
258
|
+
S extends DocShape,
|
|
259
|
+
N extends NestedDocShape = Record<string, never>,
|
|
260
|
+
>(): FieldAccessor<S, N> {
|
|
261
|
+
const stageInstance = buildStageEmitters();
|
|
262
|
+
const callable = ((path: string) => buildExpression<DocField>(path)) as unknown as FieldAccessor<
|
|
263
|
+
S,
|
|
264
|
+
N
|
|
265
|
+
>;
|
|
266
|
+
return new Proxy(callable, {
|
|
267
|
+
get(target, prop, receiver) {
|
|
268
|
+
if (typeof prop === 'symbol') {
|
|
269
|
+
return Reflect.get(target, prop, receiver);
|
|
270
|
+
}
|
|
271
|
+
if (prop === 'stage') {
|
|
272
|
+
return stageInstance;
|
|
273
|
+
}
|
|
274
|
+
if (prop === 'rawPath') {
|
|
275
|
+
return (path: string) => buildExpression<DocField>(path);
|
|
276
|
+
}
|
|
277
|
+
return buildExpression(prop);
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
}
|
package/src/markers.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phantom capability markers for `PipelineChain`.
|
|
3
|
+
*
|
|
4
|
+
* `UpdateEnabled` — gates `.updateMany()` / `.updateOne()` no-arg form
|
|
5
|
+
* (consume accumulated pipeline as an update-with-pipeline spec).
|
|
6
|
+
* `FindAndModifyEnabled` — gates `.findOneAndUpdate(...)` / `.findOneAndDelete(...)`
|
|
7
|
+
* (deconstruct pipeline into the wire command's filter/sort/skip slots).
|
|
8
|
+
* `LeadingMatch` — internal marker tracking whether the chain is still
|
|
9
|
+
* in its leading-`$match` prefix. Flips to `'past-leading'`
|
|
10
|
+
* after the first non-`$match` stage, which lets
|
|
11
|
+
* `match()` clear `UpdateEnabled` on second `$match`
|
|
12
|
+
* stages that sit past the prefix (and would otherwise
|
|
13
|
+
* fail at runtime inside `deconstructUpdateChain`).
|
|
14
|
+
*
|
|
15
|
+
* Each pipeline-stage method either preserves or clears these markers per
|
|
16
|
+
* the marker table (and rationale per row) in
|
|
17
|
+
* `docs/architecture docs/adrs/ADR 201 - State-machine pattern for typed DSL builders.md`.
|
|
18
|
+
*
|
|
19
|
+
* The markers exist only at the type level; nothing reads them at runtime.
|
|
20
|
+
* Value literals are self-identifying so the slots are distinguishable in
|
|
21
|
+
* hover tooltips and error messages (e.g. `'update-ok'` vs `'fam-ok'`).
|
|
22
|
+
*/
|
|
23
|
+
export type UpdateEnabled = 'update-ok' | 'update-cleared';
|
|
24
|
+
export type FindAndModifyEnabled = 'fam-ok' | 'fam-cleared';
|
|
25
|
+
export type LeadingMatch = 'leading' | 'past-leading';
|
package/src/query.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { PlanMeta } from '@prisma-next/contract/types';
|
|
2
|
+
import type {
|
|
3
|
+
MongoContract,
|
|
4
|
+
MongoContractWithTypeMaps,
|
|
5
|
+
MongoTypeMaps,
|
|
6
|
+
} from '@prisma-next/mongo-contract';
|
|
7
|
+
import type { AnyMongoCommand, MongoQueryPlan } from '@prisma-next/mongo-query-ast/execution';
|
|
8
|
+
import { asMongoContract, type CollectionHandle, createCollectionHandle } from './state-classes';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Public entry point of the query builder. `mongoQuery(...).from(rootName)`
|
|
12
|
+
* yields the root state of the three-state machine
|
|
13
|
+
* (`CollectionHandle` → `FilteredCollection` → `PipelineChain`).
|
|
14
|
+
*
|
|
15
|
+
* `rawCommand(cmd)` is the escape hatch for cases the typed surface does
|
|
16
|
+
* not cover (yet) — it accepts any `AnyMongoCommand` (typed CRUD or a
|
|
17
|
+
* `RawMongoCommand` of `Document`s) and packages it into a `MongoQueryPlan`
|
|
18
|
+
* with `lane: 'mongo-query'`. Row type is `unknown` because the runtime
|
|
19
|
+
* cannot know what the caller's command yields.
|
|
20
|
+
*/
|
|
21
|
+
export interface QueryRoot<
|
|
22
|
+
TContract extends MongoContractWithTypeMaps<MongoContract, MongoTypeMaps>,
|
|
23
|
+
> {
|
|
24
|
+
from<K extends keyof TContract['roots'] & string>(
|
|
25
|
+
rootName: K,
|
|
26
|
+
): CollectionHandle<TContract, TContract['roots'][K] & string & keyof TContract['models']>;
|
|
27
|
+
rawCommand<C extends AnyMongoCommand>(command: C): MongoQueryPlan<unknown, C>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function mongoQuery<
|
|
31
|
+
TContract extends MongoContractWithTypeMaps<MongoContract, MongoTypeMaps>,
|
|
32
|
+
>(options: { contractJson: unknown }): QueryRoot<TContract> {
|
|
33
|
+
const contract = options.contractJson as TContract;
|
|
34
|
+
return {
|
|
35
|
+
from<K extends keyof TContract['roots'] & string>(rootName: K) {
|
|
36
|
+
return createCollectionHandle(contract, rootName);
|
|
37
|
+
},
|
|
38
|
+
rawCommand<C extends AnyMongoCommand>(command: C): MongoQueryPlan<unknown, C> {
|
|
39
|
+
const c = asMongoContract(contract);
|
|
40
|
+
const storageHash = c.storage?.storageHash;
|
|
41
|
+
if (!storageHash) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
'Contract is missing storage.storageHash. Pass a validated contract to mongoQuery().',
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
const meta: PlanMeta = {
|
|
47
|
+
target: 'mongo',
|
|
48
|
+
storageHash: String(storageHash),
|
|
49
|
+
lane: 'mongo-query',
|
|
50
|
+
paramDescriptors: [],
|
|
51
|
+
};
|
|
52
|
+
return { collection: command.collection, command, meta };
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type { MongoContract } from '@prisma-next/mongo-contract';
|
|
2
|
+
import type { DocField } from './types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Marker `DocField` variant representing a non-leaf (value-object) path in
|
|
6
|
+
* a [NestedDocShape]. Extends `DocField` with a `fields` property carrying
|
|
7
|
+
* the sub-shape so the pipeline builder can recurse into it.
|
|
8
|
+
*
|
|
9
|
+
* `codecId` is the reserved literal `'prisma/object@1'`; the accessor's
|
|
10
|
+
* runtime implementation does not serialize it — the codec id is a purely
|
|
11
|
+
* type-level sentinel used by `Expression<F>` to select the reduced
|
|
12
|
+
* operator surface for non-leaf paths.
|
|
13
|
+
*
|
|
14
|
+
* `nullable` tracks whether the value object itself may be absent/null on
|
|
15
|
+
* the parent document. The callable form currently does not propagate the
|
|
16
|
+
* parent's `nullable` flag onto leaves beneath it (path traversal under a
|
|
17
|
+
* nullable parent resolves to the leaf's own `nullable` — matching how
|
|
18
|
+
* MongoDB treats missing intermediate documents).
|
|
19
|
+
*/
|
|
20
|
+
export interface ObjectField<N extends NestedDocShape> extends DocField {
|
|
21
|
+
readonly codecId: 'prisma/object@1';
|
|
22
|
+
readonly nullable: boolean;
|
|
23
|
+
readonly fields: N;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Document shape that carries nested value-object sub-shapes.
|
|
28
|
+
*
|
|
29
|
+
* Structurally identical to a flat `DocShape` (`Record<string, DocField>`),
|
|
30
|
+
* but individual values may be `ObjectField<SubShape>` carrying a nested
|
|
31
|
+
* `NestedDocShape` sub-tree. The pipeline builder threads a
|
|
32
|
+
* `NestedDocShape` alongside the flat `DocShape` so the callable
|
|
33
|
+
* `f('a.b.c')` form can validate dot-paths at the type level.
|
|
34
|
+
*
|
|
35
|
+
* When a stage transforms the root shape in a way that invalidates nested
|
|
36
|
+
* paths (e.g. `$group`, `$project`, `$replaceRoot`), the thread is reset
|
|
37
|
+
* to the empty shape `Record<string, never>` — which makes `ValidPaths`
|
|
38
|
+
* resolve to `never` and so disables the callable form downstream.
|
|
39
|
+
*/
|
|
40
|
+
export type NestedDocShape = Record<string, DocField>;
|
|
41
|
+
|
|
42
|
+
// ── Contract → NestedDocShape translation ────────────────────────────────
|
|
43
|
+
|
|
44
|
+
type ContractHasValueObjects = {
|
|
45
|
+
readonly valueObjects?: Record<string, { readonly fields: Record<string, unknown> }>;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
type FieldToLeaf<F> = F extends {
|
|
49
|
+
readonly type: { readonly kind: 'scalar'; readonly codecId: infer C extends string };
|
|
50
|
+
readonly nullable: infer N extends boolean;
|
|
51
|
+
}
|
|
52
|
+
? { readonly codecId: C; readonly nullable: N }
|
|
53
|
+
: F extends { readonly many: true; readonly nullable: infer N extends boolean }
|
|
54
|
+
? { readonly codecId: 'mongo/array@1'; readonly nullable: N }
|
|
55
|
+
: DocField;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Translate a single contract field to its nested-shape form. Scalars
|
|
59
|
+
* become `DocField` leaves; value-object fields become
|
|
60
|
+
* `ObjectField<Sub>`; `many: true` stops at a leaf; anything else falls
|
|
61
|
+
* through to the opaque `DocField` base.
|
|
62
|
+
*
|
|
63
|
+
* Kept as a per-field helper (rather than a `Fields → NestedShape` helper
|
|
64
|
+
* that maps over keys internally) so the parent mapped type stays
|
|
65
|
+
* homomorphic over the model/value-object `fields` record. Homomorphic
|
|
66
|
+
* mapped types preserve the literal keys of their source object through
|
|
67
|
+
* TypeScript's intersection-collapsing machinery, which keeps
|
|
68
|
+
* `ModelNestedShape` hover output and `keyof`/indexed-access resolution
|
|
69
|
+
* concrete instead of collapsing to `{ [x: string]: … }`.
|
|
70
|
+
*/
|
|
71
|
+
type TranslateField<TContract extends ContractHasValueObjects, F> = F extends {
|
|
72
|
+
readonly many: true;
|
|
73
|
+
}
|
|
74
|
+
? FieldToLeaf<F>
|
|
75
|
+
: F extends {
|
|
76
|
+
readonly type: {
|
|
77
|
+
readonly kind: 'valueObject';
|
|
78
|
+
readonly name: infer VOName extends string;
|
|
79
|
+
};
|
|
80
|
+
readonly nullable: infer Null extends boolean;
|
|
81
|
+
}
|
|
82
|
+
? ObjectField<VONestedShape<TContract, VOName>> & { readonly nullable: Null }
|
|
83
|
+
: F extends {
|
|
84
|
+
readonly type: { readonly kind: 'scalar'; readonly codecId: string };
|
|
85
|
+
}
|
|
86
|
+
? FieldToLeaf<F>
|
|
87
|
+
: DocField;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Resolve a named value object from the contract into its own
|
|
91
|
+
* `NestedDocShape`. The mapped iteration is inlined here (not delegated
|
|
92
|
+
* to a generic helper) so that the homomorphism over
|
|
93
|
+
* `VOs[VOName]['fields']` is preserved and the hover / indexed-access
|
|
94
|
+
* surface stays concrete at instantiation time.
|
|
95
|
+
*/
|
|
96
|
+
type VONestedShape<
|
|
97
|
+
TContract extends ContractHasValueObjects,
|
|
98
|
+
VOName extends string,
|
|
99
|
+
> = TContract extends {
|
|
100
|
+
readonly valueObjects: infer VOs extends Record<
|
|
101
|
+
string,
|
|
102
|
+
{ readonly fields: Record<string, unknown> }
|
|
103
|
+
>;
|
|
104
|
+
}
|
|
105
|
+
? VOName extends keyof VOs
|
|
106
|
+
? {
|
|
107
|
+
readonly [K in keyof VOs[VOName]['fields'] & string]: TranslateField<
|
|
108
|
+
TContract,
|
|
109
|
+
VOs[VOName]['fields'][K]
|
|
110
|
+
>;
|
|
111
|
+
}
|
|
112
|
+
: never
|
|
113
|
+
: never;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Build the `NestedDocShape` for a model. Scalar leaves resolve to their
|
|
117
|
+
* concrete codec id; value-object fields recurse into the referenced
|
|
118
|
+
* `valueObjects[VOName].fields` table, producing a tree that
|
|
119
|
+
* `ResolvePath` / `ValidPaths` can walk.
|
|
120
|
+
*
|
|
121
|
+
* The mapped iteration is inlined (not hidden behind a helper type that
|
|
122
|
+
* takes `Fields` as a generic) so TypeScript recognises the mapped type
|
|
123
|
+
* as homomorphic over `TContract['models'][ModelName]['fields']`. That
|
|
124
|
+
* preserves the literal field-name keys at instantiation — without this,
|
|
125
|
+
* the intersection of `Record<string, ContractField>` and the specific
|
|
126
|
+
* literal field record collapses `keyof` to `string` and the result hover
|
|
127
|
+
* degrades to `{ readonly [x: string]: any }`.
|
|
128
|
+
*/
|
|
129
|
+
export type ModelNestedShape<
|
|
130
|
+
TContract extends MongoContract,
|
|
131
|
+
ModelName extends string & keyof TContract['models'],
|
|
132
|
+
> = {
|
|
133
|
+
readonly [K in keyof TContract['models'][ModelName]['fields'] & string]: TranslateField<
|
|
134
|
+
TContract & ContractHasValueObjects,
|
|
135
|
+
TContract['models'][ModelName]['fields'][K]
|
|
136
|
+
>;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// ── Path walking ─────────────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Resolve a dot-path against a `NestedDocShape`. Returns:
|
|
143
|
+
* - the leaf `DocField` when `Path` terminates on a scalar/array leaf,
|
|
144
|
+
* - the `ObjectField<Sub>` when `Path` terminates on a value object (so
|
|
145
|
+
* the caller can operate on the whole sub-document),
|
|
146
|
+
* - `never` when the path is invalid (unknown segment, or a scalar
|
|
147
|
+
* segment followed by further traversal).
|
|
148
|
+
*
|
|
149
|
+
* Paired with the constrained callable `<P extends ValidPaths<N>>(path: P)
|
|
150
|
+
* => Expression<ResolvePath<N, P>>` so the IDE offers completions and
|
|
151
|
+
* rejects bad paths with a clear error instead of silently resolving to
|
|
152
|
+
* `never`.
|
|
153
|
+
*/
|
|
154
|
+
export type ResolvePath<
|
|
155
|
+
N extends NestedDocShape,
|
|
156
|
+
Path extends string,
|
|
157
|
+
> = Path extends `${infer Head}.${infer Rest}`
|
|
158
|
+
? Head extends keyof N & string
|
|
159
|
+
? N[Head] extends ObjectField<infer Sub>
|
|
160
|
+
? ResolvePath<Sub, Rest>
|
|
161
|
+
: never
|
|
162
|
+
: never
|
|
163
|
+
: Path extends keyof N & string
|
|
164
|
+
? N[Path]
|
|
165
|
+
: never;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Union of every valid dot-path within a `NestedDocShape`. Includes
|
|
169
|
+
* top-level keys (scalar leaves *and* value-object roots) and every
|
|
170
|
+
* recursive descent through `ObjectField` sub-shapes.
|
|
171
|
+
*
|
|
172
|
+
* Non-leaf paths are intentionally included — `f('address')` yields an
|
|
173
|
+
* `Expression<ObjectField<…>>` whose reduced operator surface (`set`,
|
|
174
|
+
* `unset`, `exists`, `eq(null)`, `ne(null)`) lets callers operate on the
|
|
175
|
+
* whole value object. Leaf paths like `f('address.city')` get the full
|
|
176
|
+
* leaf operator surface.
|
|
177
|
+
*
|
|
178
|
+
* The `string extends keyof N` guard short-circuits to `never` for
|
|
179
|
+
* open-ended index-signature shapes (e.g. the default
|
|
180
|
+
* `Record<string, never>` used to represent "no nested information" —
|
|
181
|
+
* notably downstream of replacement stages in the pipeline builder). An
|
|
182
|
+
* open-ended `keyof` cannot resolve a specific literal path, so the
|
|
183
|
+
* callable form must be disabled at the type level.
|
|
184
|
+
*/
|
|
185
|
+
export type ValidPaths<N extends NestedDocShape> = string extends keyof N
|
|
186
|
+
? never
|
|
187
|
+
: {
|
|
188
|
+
[K in keyof N & string]: N[K] extends ObjectField<infer Sub>
|
|
189
|
+
? K | `${K}.${ValidPaths<Sub>}`
|
|
190
|
+
: K;
|
|
191
|
+
}[keyof N & string];
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* IDE-oriented alias for `ValidPaths`. Kept as a separate export so future
|
|
195
|
+
* refinements (e.g. ArkType-style lazy expansion for very deep shapes) can
|
|
196
|
+
* diverge from the strict `ValidPaths` constraint without breaking
|
|
197
|
+
* downstream consumers. For now the two are intentionally equivalent.
|
|
198
|
+
*/
|
|
199
|
+
export type PathCompletions<N extends NestedDocShape> = ValidPaths<N>;
|