@prisma-next/mongo-query-builder 0.12.0 → 0.13.0-dev.2
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/exports/contract-free.d.mts +78 -0
- package/dist/exports/contract-free.d.mts.map +1 -0
- package/dist/exports/contract-free.mjs +98 -0
- package/dist/exports/contract-free.mjs.map +1 -0
- package/dist/field-accessor-BmcQj0lD.d.mts +586 -0
- package/dist/field-accessor-BmcQj0lD.d.mts.map +1 -0
- package/dist/field-accessor-Dh8GUEe7.mjs +230 -0
- package/dist/field-accessor-Dh8GUEe7.mjs.map +1 -0
- package/dist/index.d.mts +4 -567
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +8 -222
- package/dist/index.mjs.map +1 -1
- package/package.json +11 -10
- package/src/builder.ts +4 -2
- package/src/contract-free/collection.ts +178 -0
- package/src/exports/contract-free.ts +6 -0
- package/src/exports/index.ts +1 -1
- package/src/field-accessor.ts +21 -0
- package/src/lookup-builder.ts +7 -3
- package/src/resolve-path.ts +2 -2
- package/src/state-classes.ts +4 -2
package/package.json
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/mongo-query-builder",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0-dev.2",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"description": "Type-safe MongoDB query builder (reads, writes, find-and-modify, pipeline-terminal writes) with document shape tracking",
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@prisma-next/contract": "0.
|
|
10
|
-
"@prisma-next/mongo-contract": "0.
|
|
11
|
-
"@prisma-next/mongo-query-ast": "0.
|
|
12
|
-
"@prisma-next/mongo-value": "0.
|
|
13
|
-
"@prisma-next/utils": "0.
|
|
9
|
+
"@prisma-next/contract": "0.13.0-dev.2",
|
|
10
|
+
"@prisma-next/mongo-contract": "0.13.0-dev.2",
|
|
11
|
+
"@prisma-next/mongo-query-ast": "0.13.0-dev.2",
|
|
12
|
+
"@prisma-next/mongo-value": "0.13.0-dev.2",
|
|
13
|
+
"@prisma-next/utils": "0.13.0-dev.2"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
|
-
"@prisma-next/tsconfig": "0.
|
|
17
|
-
"@prisma-next/tsdown": "0.
|
|
18
|
-
"tsdown": "0.22.
|
|
16
|
+
"@prisma-next/tsconfig": "0.13.0-dev.2",
|
|
17
|
+
"@prisma-next/tsdown": "0.13.0-dev.2",
|
|
18
|
+
"tsdown": "0.22.1",
|
|
19
19
|
"typescript": "5.9.3",
|
|
20
|
-
"vitest": "4.1.
|
|
20
|
+
"vitest": "4.1.8"
|
|
21
21
|
},
|
|
22
22
|
"peerDependencies": {
|
|
23
23
|
"typescript": ">=5.9"
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"types": "./dist/index.d.mts",
|
|
35
35
|
"exports": {
|
|
36
36
|
".": "./dist/index.mjs",
|
|
37
|
+
"./contract-free": "./dist/exports/contract-free.mjs",
|
|
37
38
|
"./package.json": "./package.json"
|
|
38
39
|
},
|
|
39
40
|
"engines": {
|
package/src/builder.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { domainModelsAtDefaultNamespace, type PlanMeta } from '@prisma-next/contract/types';
|
|
2
2
|
import type {
|
|
3
3
|
ExtractMongoCodecTypes,
|
|
4
4
|
MongoContract,
|
|
@@ -820,7 +820,9 @@ export class PipelineChain<
|
|
|
820
820
|
let resultShape: MongoResultShape | undefined;
|
|
821
821
|
if (modelName !== undefined) {
|
|
822
822
|
if (pipelineSupportsFlatResultShape(this.#state.stages)) {
|
|
823
|
-
const model =
|
|
823
|
+
const model = domainModelsAtDefaultNamespace(contractNarrow.domain)[modelName] as
|
|
824
|
+
| MongoModelDefinition
|
|
825
|
+
| undefined;
|
|
824
826
|
resultShape = model ? contractModelToMongoResultShape(model) : { kind: 'unknown' as const };
|
|
825
827
|
} else {
|
|
826
828
|
resultShape = { kind: 'unknown' as const };
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AggregateCommand,
|
|
3
|
+
FindOneAndUpdateCommand,
|
|
4
|
+
InsertOneCommand,
|
|
5
|
+
MongoAndExpr,
|
|
6
|
+
type MongoFilterExpr,
|
|
7
|
+
MongoLimitStage,
|
|
8
|
+
MongoMatchStage,
|
|
9
|
+
type MongoPipelineStage,
|
|
10
|
+
MongoSortStage,
|
|
11
|
+
type MongoUpdatePipelineStage,
|
|
12
|
+
} from '@prisma-next/mongo-query-ast/execution';
|
|
13
|
+
import type { MongoValue } from '@prisma-next/mongo-value';
|
|
14
|
+
import { createFieldAccessor, type FieldAccessor } from '../field-accessor';
|
|
15
|
+
import type { DocShape } from '../types';
|
|
16
|
+
import { resolveUpdaterResult, type UpdaterResult } from '../update-ops';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Fold an array of filter expressions into a single `MongoFilterExpr`. Length-1
|
|
20
|
+
* short-circuits to avoid a redundant `$and` wrapper; the call site never writes
|
|
21
|
+
* `MongoAndExpr.of([...])` directly.
|
|
22
|
+
*/
|
|
23
|
+
function foldFilters(filters: ReadonlyArray<MongoFilterExpr>): MongoFilterExpr {
|
|
24
|
+
const first = filters[0];
|
|
25
|
+
if (first === undefined) {
|
|
26
|
+
throw new Error('foldFilters: invariant violated — empty filter list');
|
|
27
|
+
}
|
|
28
|
+
return filters.length === 1 ? first : MongoAndExpr.of(filters);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Fluent aggregate chain. Accumulates `$match` / `$sort` / `$limit` stages and
|
|
33
|
+
* produces an `AggregateCommand` via `.build()`.
|
|
34
|
+
*
|
|
35
|
+
* Instances are immutable — each stage method returns a new chain.
|
|
36
|
+
*/
|
|
37
|
+
export class AggregateChain<Shape extends DocShape> {
|
|
38
|
+
readonly #collection: string;
|
|
39
|
+
readonly #stages: ReadonlyArray<MongoPipelineStage>;
|
|
40
|
+
|
|
41
|
+
constructor(collection: string, stages: ReadonlyArray<MongoPipelineStage>) {
|
|
42
|
+
this.#collection = collection;
|
|
43
|
+
this.#stages = stages;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
match(filterFn: (fields: FieldAccessor<Shape>) => MongoFilterExpr): AggregateChain<Shape> {
|
|
47
|
+
const f = createFieldAccessor<Shape>();
|
|
48
|
+
const filter = filterFn(f);
|
|
49
|
+
return new AggregateChain<Shape>(this.#collection, [
|
|
50
|
+
...this.#stages,
|
|
51
|
+
new MongoMatchStage(filter),
|
|
52
|
+
]);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
sort(spec: Record<string, 1 | -1>): AggregateChain<Shape> {
|
|
56
|
+
return new AggregateChain<Shape>(this.#collection, [...this.#stages, new MongoSortStage(spec)]);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
limit(n: number): AggregateChain<Shape> {
|
|
60
|
+
return new AggregateChain<Shape>(this.#collection, [...this.#stages, new MongoLimitStage(n)]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
build(): AggregateCommand {
|
|
64
|
+
return new AggregateCommand(this.#collection, this.#stages);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Fluent find-and-modify chain. Holds an accumulated list of filter expressions
|
|
70
|
+
* (AND-folded into the wire command's `filter` slot) and exposes
|
|
71
|
+
* `.findOneAndUpdate(...)` as the only terminal.
|
|
72
|
+
*
|
|
73
|
+
* Multiple `.match()` calls AND-fold internally — `MongoAndExpr.of` never
|
|
74
|
+
* appears at the call site.
|
|
75
|
+
*
|
|
76
|
+
* Instances are immutable — `.match()` returns a new `FilteredBuilder`.
|
|
77
|
+
*/
|
|
78
|
+
export class FilteredBuilder<Shape extends DocShape> {
|
|
79
|
+
readonly #collection: string;
|
|
80
|
+
readonly #filters: ReadonlyArray<MongoFilterExpr>;
|
|
81
|
+
|
|
82
|
+
constructor(collection: string, filters: ReadonlyArray<MongoFilterExpr>) {
|
|
83
|
+
if (filters.length === 0) {
|
|
84
|
+
throw new Error('FilteredBuilder requires at least one filter');
|
|
85
|
+
}
|
|
86
|
+
this.#collection = collection;
|
|
87
|
+
this.#filters = filters;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
match(filterFn: (fields: FieldAccessor<Shape>) => MongoFilterExpr): FilteredBuilder<Shape> {
|
|
91
|
+
const f = createFieldAccessor<Shape>();
|
|
92
|
+
const filter = filterFn(f);
|
|
93
|
+
return new FilteredBuilder<Shape>(this.#collection, [...this.#filters, filter]);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
findOneAndUpdate(
|
|
97
|
+
updaterFn: (fields: FieldAccessor<Shape>) => UpdaterResult,
|
|
98
|
+
opts: { readonly upsert?: boolean; readonly returnDocument?: 'before' | 'after' } = {},
|
|
99
|
+
): FindOneAndUpdateCommand {
|
|
100
|
+
const filter = foldFilters(this.#filters);
|
|
101
|
+
const f = createFieldAccessor<Shape>();
|
|
102
|
+
const items = updaterFn(f);
|
|
103
|
+
const update = resolveUpdaterResult(items);
|
|
104
|
+
return new FindOneAndUpdateCommand(
|
|
105
|
+
this.#collection,
|
|
106
|
+
filter,
|
|
107
|
+
update,
|
|
108
|
+
opts.upsert ?? false,
|
|
109
|
+
undefined,
|
|
110
|
+
opts.returnDocument ?? 'after',
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Contract-free fluent Mongo collection builder. Produces the canonical
|
|
117
|
+
* `AggregateCommand` / `InsertOneCommand` / `FindOneAndUpdateCommand` command
|
|
118
|
+
* nodes without any contract coupling — parameterised by an explicit `DocShape`
|
|
119
|
+
* instead of a `MongoContract`.
|
|
120
|
+
*
|
|
121
|
+
* Mirrors SQL's `table(source, schema)` → `TableHandle` in spirit: a top-level
|
|
122
|
+
* entry point that exposes fluent query chains from which call sites never write
|
|
123
|
+
* `new MongoMatchStage(...)`, `MongoAndExpr.of([...])`, or `new AggregateCommand(...)`.
|
|
124
|
+
*
|
|
125
|
+
* ```ts
|
|
126
|
+
* const markerLedger = collection<MarkerLedgerDocShape>('_prisma_migrations');
|
|
127
|
+
*
|
|
128
|
+
* // aggregate
|
|
129
|
+
* markerLedger.aggregate().match(f => f._id.eq(space)).limit(1).build();
|
|
130
|
+
*
|
|
131
|
+
* // insertOne
|
|
132
|
+
* markerLedger.insertOne({ _id: space, space, storageHash });
|
|
133
|
+
*
|
|
134
|
+
* // findOneAndUpdate (CAS)
|
|
135
|
+
* markerLedger.match(f => f._id.eq(space)).match(f => f.storageHash.eq(expectedFrom))
|
|
136
|
+
* .findOneAndUpdate(f => [f.stage.set({ storageHash: newHash })], { upsert: false });
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
export interface CollectionBuilder<Shape extends DocShape> {
|
|
140
|
+
aggregate(): AggregateChain<Shape>;
|
|
141
|
+
insertOne(document: Record<string, MongoValue>): InsertOneCommand;
|
|
142
|
+
match(filterFn: (fields: FieldAccessor<Shape>) => MongoFilterExpr): FilteredBuilder<Shape>;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
class CollectionBuilderImpl<Shape extends DocShape> implements CollectionBuilder<Shape> {
|
|
146
|
+
readonly #name: string;
|
|
147
|
+
|
|
148
|
+
constructor(name: string) {
|
|
149
|
+
this.#name = name;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
aggregate(): AggregateChain<Shape> {
|
|
153
|
+
return new AggregateChain<Shape>(this.#name, []);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
insertOne(document: Record<string, MongoValue>): InsertOneCommand {
|
|
157
|
+
return new InsertOneCommand(this.#name, document);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
match(filterFn: (fields: FieldAccessor<Shape>) => MongoFilterExpr): FilteredBuilder<Shape> {
|
|
161
|
+
const f = createFieldAccessor<Shape>();
|
|
162
|
+
const filter = filterFn(f);
|
|
163
|
+
return new FilteredBuilder<Shape>(this.#name, [filter]);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Declare a contract-free collection builder parameterised by a `DocShape`.
|
|
169
|
+
* The collection name is bound once; field access reuses `createFieldAccessor`
|
|
170
|
+
* so the shape is typed at every call site without a contract.
|
|
171
|
+
*
|
|
172
|
+
* @param name The MongoDB collection name (e.g. `'_prisma_migrations'`)
|
|
173
|
+
*/
|
|
174
|
+
export function collection<Shape extends DocShape>(name: string): CollectionBuilder<Shape> {
|
|
175
|
+
return new CollectionBuilderImpl<Shape>(name);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export type { MongoUpdatePipelineStage };
|
package/src/exports/index.ts
CHANGED
|
@@ -13,7 +13,7 @@ export type {
|
|
|
13
13
|
LeafExpression,
|
|
14
14
|
ObjectExpression,
|
|
15
15
|
} from '../field-accessor';
|
|
16
|
-
export { createFieldAccessor } from '../field-accessor';
|
|
16
|
+
export { createFieldAccessor, expr } from '../field-accessor';
|
|
17
17
|
export type {
|
|
18
18
|
LookupBuilder,
|
|
19
19
|
LookupBuilderWithKey,
|
package/src/field-accessor.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
MongoAddFieldsStage,
|
|
8
8
|
MongoAggFieldRef,
|
|
9
9
|
MongoExistsExpr,
|
|
10
|
+
MongoExprFilter,
|
|
10
11
|
MongoFieldFilter,
|
|
11
12
|
MongoProjectStage,
|
|
12
13
|
MongoReplaceRootStage,
|
|
@@ -58,6 +59,14 @@ export interface LeafExpression<F extends DocField> extends TypedAggExpr<F> {
|
|
|
58
59
|
nin(values: ReadonlyArray<MongoValue>): MongoFilterExpr;
|
|
59
60
|
exists(flag?: boolean): MongoFilterExpr;
|
|
60
61
|
|
|
62
|
+
/**
|
|
63
|
+
* `$type` filter: `{ field: { $type: bsonType } }`. Rides
|
|
64
|
+
* `MongoFieldFilter`'s generic `op` string — no dedicated AST node. The
|
|
65
|
+
* BSON type is expressed as Mongo's alias string (e.g. `'string'`) or
|
|
66
|
+
* numeric type code; an array selects any of several types.
|
|
67
|
+
*/
|
|
68
|
+
type(bsonType: MongoValue): MongoFilterExpr;
|
|
69
|
+
|
|
61
70
|
// Update operators ($set family)
|
|
62
71
|
set(value: MongoValue): TypedUpdateOp;
|
|
63
72
|
unset(): TypedUpdateOp;
|
|
@@ -203,6 +212,17 @@ export type FieldAccessor<S extends DocShape, N extends NestedDocShape = Record<
|
|
|
203
212
|
rawPath<F extends DocField = DocField>(path: string): LeafExpression<F>;
|
|
204
213
|
};
|
|
205
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Wrap a boolean aggregation expression as an `$expr` filter
|
|
217
|
+
* (`MongoExprFilter`). Lets a `$match` express an aggregation-expression
|
|
218
|
+
* predicate — e.g. comparing two field references via `fn.eq(a, b)` — in
|
|
219
|
+
* the typed AST rather than via a raw escape hatch. Pairs with `.type()`
|
|
220
|
+
* to express filters like `{ _id: { $type: 'string' }, $expr: { $eq: ['$_id', '$space'] } }`.
|
|
221
|
+
*/
|
|
222
|
+
export function expr(predicate: TypedAggExpr<DocField>): MongoFilterExpr {
|
|
223
|
+
return MongoExprFilter.of(predicate.node);
|
|
224
|
+
}
|
|
225
|
+
|
|
206
226
|
function buildExpression<F extends DocField>(path: string): Expression<F> {
|
|
207
227
|
// The runtime object carries the full operator surface unconditionally;
|
|
208
228
|
// `ObjectExpression` is a strict subset of `LeafExpression`, so a single
|
|
@@ -223,6 +243,7 @@ function buildExpression<F extends DocField>(path: string): Expression<F> {
|
|
|
223
243
|
nin: (values: ReadonlyArray<MongoValue>) => MongoFieldFilter.nin(path, values),
|
|
224
244
|
exists: (flag?: boolean) =>
|
|
225
245
|
flag === false ? MongoExistsExpr.notExists(path) : MongoExistsExpr.exists(path),
|
|
246
|
+
type: (bsonType: MongoValue) => MongoFieldFilter.of(path, '$type', bsonType),
|
|
226
247
|
|
|
227
248
|
set: (value: MongoValue) => setOp(path, value),
|
|
228
249
|
unset: () => unsetOp(path),
|
package/src/lookup-builder.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { domainModelsAtDefaultNamespace } from '@prisma-next/contract/types';
|
|
2
2
|
import type {
|
|
3
3
|
MongoContract,
|
|
4
4
|
MongoModelDefinition,
|
|
@@ -148,7 +148,9 @@ export function createLookupFrom<
|
|
|
148
148
|
const validRoots = Object.keys(contract.roots).join(', ');
|
|
149
149
|
throw new Error(`lookup() unknown root: "${rootName}". Valid roots: ${validRoots}`);
|
|
150
150
|
}
|
|
151
|
-
const model =
|
|
151
|
+
const model = domainModelsAtDefaultNamespace(contract.domain)[modelName] as
|
|
152
|
+
| MongoModelDefinition
|
|
153
|
+
| undefined;
|
|
152
154
|
const foreignCollection = model?.storage?.collection ?? rootName;
|
|
153
155
|
return createLookupBuilder({
|
|
154
156
|
rootName,
|
|
@@ -266,7 +268,9 @@ export function extractLookupResult(
|
|
|
266
268
|
'Returning a hand-rolled options object is not supported.',
|
|
267
269
|
);
|
|
268
270
|
}
|
|
269
|
-
const model =
|
|
271
|
+
const model = domainModelsAtDefaultNamespace(contract.domain)[result._model] as
|
|
272
|
+
| MongoModelDefinition
|
|
273
|
+
| undefined;
|
|
270
274
|
const foreignCollection = model?.storage?.collection ?? result._root;
|
|
271
275
|
return {
|
|
272
276
|
foreignCollection,
|
package/src/resolve-path.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ContractValueObjectDefinitions } from '@prisma-next/contract/types';
|
|
2
2
|
import type { MongoContract, MongoModelsMap } from '@prisma-next/mongo-contract';
|
|
3
3
|
import type { DocField } from './types';
|
|
4
4
|
|
|
@@ -145,7 +145,7 @@ type TranslateField<TContract extends MongoContract, F> = F extends {
|
|
|
145
145
|
* surface stays concrete at instantiation time.
|
|
146
146
|
*/
|
|
147
147
|
type VONestedShape<TContract extends MongoContract, VOName extends string> = [
|
|
148
|
-
|
|
148
|
+
ContractValueObjectDefinitions<TContract>,
|
|
149
149
|
] extends [infer VOs extends Record<string, { readonly fields: Record<string, unknown> }>]
|
|
150
150
|
? VOName extends keyof VOs & string
|
|
151
151
|
? {
|
package/src/state-classes.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { domainModelsAtDefaultNamespace, type PlanMeta } from '@prisma-next/contract/types';
|
|
2
2
|
import type {
|
|
3
3
|
ExtractMongoCodecTypes,
|
|
4
4
|
MongoContract,
|
|
@@ -642,7 +642,9 @@ export function createCollectionHandle<
|
|
|
642
642
|
const validRoots = Object.keys(c.roots).join(', ');
|
|
643
643
|
throw new Error(`Unknown root: "${rootName}". Valid roots: ${validRoots}`);
|
|
644
644
|
}
|
|
645
|
-
const model =
|
|
645
|
+
const model = domainModelsAtDefaultNamespace(c.domain)[modelName] as
|
|
646
|
+
| MongoModelDefinition
|
|
647
|
+
| undefined;
|
|
646
648
|
if (!model) {
|
|
647
649
|
throw new Error(`Unknown model: "${modelName}" referenced by root "${rootName}".`);
|
|
648
650
|
}
|