@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 ADDED
@@ -0,0 +1,144 @@
1
+ # @prisma-next/mongo-query-builder
2
+
3
+ A typed CRUD query builder for MongoDB contracts. Reads, writes, pipeline aggregations, and find-and-modify operations all produce `MongoQueryPlan` values that the runtime executes. Authored against a contract, not directly against the driver.
4
+
5
+ ## Quick start
6
+
7
+ ```typescript
8
+ import { mongoQuery } from '@prisma-next/mongo-query-builder';
9
+
10
+ const q = mongoQuery<TContract>({ contractJson: contract });
11
+
12
+ // Read — aggregation pipeline
13
+ const analytics = q
14
+ .from('orders')
15
+ .match((f) => f.status.eq('completed'))
16
+ .group((f) => ({
17
+ _id: f.department,
18
+ totalRevenue: acc.sum(f.amount),
19
+ }))
20
+ .sort({ totalRevenue: -1 })
21
+ .build();
22
+
23
+ // Write — filtered update
24
+ const updated = q
25
+ .from('orders')
26
+ .match((f) => f.total.gt(100))
27
+ .updateMany((f) => [f.status.set('shipped')]);
28
+
29
+ // Find-and-modify
30
+ const doc = q
31
+ .from('orders')
32
+ .match((f) => f.status.eq('pending'))
33
+ .sort({ createdAt: 1 })
34
+ .findOneAndUpdate((f) => [f.status.set('processing')], { returnDocument: 'after' });
35
+ ```
36
+
37
+ Each call returns a `MongoQueryPlan` with a narrowly typed `command` field and a phantom `Row` type parameter that tracks the shape of the result.
38
+
39
+ ## The three states
40
+
41
+ The builder is a three-state machine. The available terminals depend on where you are in the chain.
42
+
43
+ ### CollectionHandle
44
+
45
+ Returned by `q.from('rootName')`. Represents an unfiltered collection binding.
46
+
47
+ | Terminals | Description |
48
+ |-----------|-------------|
49
+ | `insertOne(doc)` | Insert a single document |
50
+ | `insertMany(docs)` | Insert a batch of documents |
51
+ | `updateAll(fn)` | Update every document (match-all filter) |
52
+ | `deleteAll()` | Delete every document |
53
+ | `upsertOne(filterFn, updaterFn)` | Insert-or-update with an explicit filter |
54
+ | `match(...)` | Transitions to **FilteredCollection** |
55
+ | *stage methods* | Transitions to **PipelineChain** |
56
+
57
+ ### FilteredCollection
58
+
59
+ Reached after one or more `.match(...)` calls on a `CollectionHandle`. Filters accumulate via AND-folding.
60
+
61
+ | Terminals | Description |
62
+ |-----------|-------------|
63
+ | `updateMany(fn)` | Update all matching documents |
64
+ | `updateOne(fn)` | Update one matching document |
65
+ | `deleteMany()` | Delete all matching documents |
66
+ | `deleteOne()` | Delete one matching document |
67
+ | `upsertOne(fn)` | Upsert against the accumulated filter |
68
+ | `findOneAndUpdate(fn, opts?)` | Find + update, returns the document |
69
+ | `findOneAndDelete()` | Find + delete, returns the deleted document |
70
+ | `match(...)` | Appends another filter |
71
+ | *stage methods* | Transitions to **PipelineChain** |
72
+
73
+ ### PipelineChain
74
+
75
+ The general-purpose aggregation chain. Reached after calling any pipeline stage (e.g. `.sort()`, `.group()`, `.addFields()`).
76
+
77
+ | Terminals | Description |
78
+ |-----------|-------------|
79
+ | `build()` / `aggregate()` | Produce an `AggregateCommand` |
80
+ | `out(collection)` | `$out` terminal |
81
+ | `merge(opts)` | `$merge` terminal |
82
+ | `updateMany()` / `updateOne()` | Pipeline-style update (no callback — the chain _is_ the update) |
83
+ | `findOneAndUpdate(fn)` | Deconstructs `$match`/`$sort`/`$skip` into command slots |
84
+ | `findOneAndDelete()` | Same deconstruction |
85
+
86
+ ## Field accessor
87
+
88
+ Stage callbacks receive a `FieldAccessor<Shape, Nested>` (typically named `f`):
89
+
90
+ - **Property form** — `f.status`, `f.amount`: produces a `TypedAggExpr` bound to the field's declared type.
91
+ - **Callable form** — `f("address.city")`: type-safe dot-path traversal through the contract's model + value-object structure. Paths are validated at compile time against `ValidPaths<Nested>`, the resolved leaf's codec drives the returned expression, and IDE autocomplete surfaces the valid path union. Non-leaf paths (`f("address")`) return an `Expression<ObjectField<…>>` whose reduced operator surface exposes `set`, `unset`, `exists`, `eq(null)`, and `ne(null)` — operators that don't make sense on a whole value object (`gt`, `inc`, `push`, …) are hidden.
92
+
93
+ The callable form is disabled (at the type level) downstream of replacement stages (`project`, `group`, `replaceRoot`, …) that erase the nested structure; additive stages (`match`, `sort`, `addFields`, `lookup`, …) preserve it.
94
+
95
+ - **Escape hatch** — `f.rawPath("path")`: sidesteps path validation and returns a `LeafExpression<DocField>` carrying the verbatim string path. Use when the path is intentionally outside the typed model — the canonical case is **migration authoring**, where a backfill writes to a field that is not yet in the pre-migration contract (see the retail-store example's `backfill-product-status` migration). `f.rawPath` offers the full leaf operator surface (`set`, `exists`, `inc`, `push`, …) and no IDE autocomplete. Callers can narrow the return via an explicit generic: `f.rawPath<StringField>("status").set("active")`. The method is named `rawPath` rather than `raw` so a user model with a legitimate top-level `raw` field still resolves `f.raw` to the field expression.
96
+
97
+ See [ADR 180 — Dot-path field accessor](../../../../docs/architecture%20docs/adrs/ADR%20180%20-%20Dot-path%20field%20accessor.md) for the design rationale.
98
+
99
+ ## Update operators
100
+
101
+ Write terminals accept a callback `(f) => [...]` returning an array of `TypedUpdateOp`:
102
+
103
+ ```typescript
104
+ .updateMany((f) => [
105
+ f.status.set('shipped'),
106
+ f.amount.inc(1),
107
+ f.tags.push('processed'),
108
+ ])
109
+ ```
110
+
111
+ Available operators: `.set`, `.unset`, `.inc`, `.mul`, `.min`, `.max`, `.push`, `.pull`, `.addToSet`, `.pop`, `.rename`, `.currentDate`, `.bit`.
112
+
113
+ ## Pipeline-style updates
114
+
115
+ For updates expressed as aggregation pipeline stages, use `f.stage.*`:
116
+
117
+ ```typescript
118
+ q.from('orders')
119
+ .match((f) => f.status.eq('new'))
120
+ .addFields((f) => ({ total: fn.multiply(f.quantity, f.price) }))
121
+ .updateMany() // no callback — the chain is the update pipeline
122
+ ```
123
+
124
+ Stage emitters: `f.stage.set(fields)`, `f.stage.unset(...fields)`, `f.stage.replaceRoot(expr)`, `f.stage.replaceWith(expr)`.
125
+
126
+ ## Marker gating
127
+
128
+ Some pipeline stages (`group`, `project`, `replaceRoot`, `limit`, etc.) change the chain's document shape in ways that make typed `update`/`findOneAndModify` unsound. Those terminals disappear from the type after such stages. Trust the compiler; use `.aggregate()` for untyped pipeline results.
129
+
130
+ ## `rawCommand` escape hatch
131
+
132
+ ```typescript
133
+ const plan = q.rawCommand(new InsertOneCommand('orders', { status: 'new' }));
134
+ ```
135
+
136
+ Bypasses the typed builder surface entirely. The plan still carries `meta.storageHash` from the contract, but the row type is `unknown`. Use this for commands the typed API does not yet cover.
137
+
138
+ ## Observability
139
+
140
+ All plans carry `meta.lane === 'mongo-query'`. Middleware that needs finer-grained read-vs-write discrimination can inspect `plan.command` (which is a tagged union — `instanceof AggregateCommand`, `instanceof UpdateManyCommand`, etc.).
141
+
142
+ ## Architecture
143
+
144
+ See [DEVELOPING.md](./DEVELOPING.md) for internal implementation details, module structure, and design decisions.