@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.
@@ -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>;