@mongorm/orm 0.1.0 → 0.1.1-beta.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/README.md +145 -0
- package/dist/index.d.mts +414 -0
- package/dist/index.mjs +676 -0
- package/package.json +29 -2
- package/.env.example +0 -2
- package/src/api.ts +0 -47
- package/src/connection/database.ts +0 -145
- package/src/index.ts +0 -50
- package/src/model/model.ts +0 -212
- package/src/model/soft-delete.ts +0 -15
- package/src/query/cursor.ts +0 -36
- package/src/query/many-query.ts +0 -338
- package/src/query/query.ts +0 -14
- package/src/query/runtime.ts +0 -140
- package/src/query/types.ts +0 -134
- package/src/relations/definitions.ts +0 -136
- package/src/relations/registry.ts +0 -144
- package/src/schema/contracts.ts +0 -7
- package/src/schema/index.ts +0 -22
- package/src/schema/inference.ts +0 -17
- package/src/schema/scalars.ts +0 -110
- package/src/schema/schema.ts +0 -234
- package/src/validation/errors.ts +0 -39
- package/tests/connection/database.test.ts +0 -28
- package/tests/env.ts +0 -14
- package/tests/model/bulk.test.ts +0 -40
- package/tests/model/crud.integration.test.ts +0 -160
- package/tests/model/lifecycle.integration.test.ts +0 -44
- package/tests/query/runtime.test.ts +0 -59
- package/tests/relations/definitions.test.ts +0 -36
- package/tests/schema/core.test.ts +0 -45
- package/tests/schema/inference.test-d.ts +0 -232
- package/tests/schema/options.test.ts +0 -55
- package/tsconfig.json +0 -13
- package/tsdown.config.ts +0 -8
- package/vitest.config.ts +0 -8
package/src/query/many-query.ts
DELETED
|
@@ -1,338 +0,0 @@
|
|
|
1
|
-
import { ObjectId, type Collection, type Filter as MongoFilter, type Sort } from 'mongodb';
|
|
2
|
-
|
|
3
|
-
import type { Db } from '../connection/database.js';
|
|
4
|
-
import type { SchemaRelationMap, SchemaShape, ScopeDefinitions } from '../schema/index.js';
|
|
5
|
-
import { CursorQueryError, EstimatedCountError, InvalidQueryError } from '../validation/errors.js';
|
|
6
|
-
import { ModelCursor } from './cursor.js';
|
|
7
|
-
import {
|
|
8
|
-
createCursorFilter,
|
|
9
|
-
PopulationExecutor,
|
|
10
|
-
projectionFor,
|
|
11
|
-
SoftDeleteState,
|
|
12
|
-
} from './runtime.js';
|
|
13
|
-
import type {
|
|
14
|
-
CursorMethod,
|
|
15
|
-
HiddenDocumentKey,
|
|
16
|
-
ModelFilter,
|
|
17
|
-
ModelSort,
|
|
18
|
-
ModelDocument,
|
|
19
|
-
PopulateSpecs,
|
|
20
|
-
PopulatedResult,
|
|
21
|
-
PopulationMode,
|
|
22
|
-
ScopeName,
|
|
23
|
-
SelectedDocument,
|
|
24
|
-
SelectableKey,
|
|
25
|
-
StoredDocument,
|
|
26
|
-
VisibleDocument,
|
|
27
|
-
} from './types.js';
|
|
28
|
-
export type {
|
|
29
|
-
ModelFilter,
|
|
30
|
-
ModelSort,
|
|
31
|
-
PopulateSpec,
|
|
32
|
-
PopulateSpecs,
|
|
33
|
-
PopulatedResult,
|
|
34
|
-
SelectedDocument,
|
|
35
|
-
SelectableKey,
|
|
36
|
-
StoredDocument,
|
|
37
|
-
VisibleDocument,
|
|
38
|
-
} from './types.js';
|
|
39
|
-
|
|
40
|
-
export { ModelCursor } from './cursor.js';
|
|
41
|
-
|
|
42
|
-
/** A typed, awaitable MongoDB find query. */
|
|
43
|
-
export class ModelQuery<
|
|
44
|
-
Shape extends SchemaShape,
|
|
45
|
-
Result extends object = VisibleDocument<Shape>,
|
|
46
|
-
CursorReady extends boolean = true,
|
|
47
|
-
Relations extends SchemaRelationMap = {},
|
|
48
|
-
Scopes extends ScopeDefinitions = {},
|
|
49
|
-
Mode extends PopulationMode = 'none',
|
|
50
|
-
SoftDelete extends boolean = false,
|
|
51
|
-
> implements PromiseLike<Result[]> {
|
|
52
|
-
declare readonly deleted: SoftDelete extends true ? (mode: 'only' | 'include') => this : never;
|
|
53
|
-
private sortSpec: ModelSort<Shape> | undefined;
|
|
54
|
-
private skipCount: number | undefined;
|
|
55
|
-
private limitCount: number | undefined;
|
|
56
|
-
private selectedFields: readonly string[] | undefined;
|
|
57
|
-
private shownFields: readonly string[] = [];
|
|
58
|
-
private populateSpecs: PopulateSpecs<Relations> = [];
|
|
59
|
-
private populationMode: PopulationMode = 'none';
|
|
60
|
-
private readonly softDelete: SoftDeleteState<Shape>;
|
|
61
|
-
private readonly population: PopulationExecutor<Relations>;
|
|
62
|
-
readonly cursor = ((after?: ObjectId) => this.createCursor(after)) as CursorMethod<
|
|
63
|
-
Shape,
|
|
64
|
-
Result,
|
|
65
|
-
CursorReady
|
|
66
|
-
>;
|
|
67
|
-
|
|
68
|
-
constructor(
|
|
69
|
-
private readonly collection: Collection<StoredDocument<Shape>>,
|
|
70
|
-
private readonly filterSpec: ModelFilter<Shape>,
|
|
71
|
-
private readonly fields: readonly string[],
|
|
72
|
-
private readonly hiddenFields: readonly string[],
|
|
73
|
-
private readonly db: Db,
|
|
74
|
-
private readonly relations: Relations,
|
|
75
|
-
private readonly scopes: Scopes,
|
|
76
|
-
private readonly softdeleteEnabled: boolean,
|
|
77
|
-
) {
|
|
78
|
-
this.softDelete = new SoftDeleteState(softdeleteEnabled);
|
|
79
|
-
this.population = new PopulationExecutor(db, relations);
|
|
80
|
-
if (softdeleteEnabled) {
|
|
81
|
-
Object.defineProperties(this, {
|
|
82
|
-
deleted: {
|
|
83
|
-
configurable: false,
|
|
84
|
-
enumerable: false,
|
|
85
|
-
value: (mode: 'only' | 'include') => this.deletedMode(mode),
|
|
86
|
-
},
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** Include both active and soft-deleted documents in this query. */
|
|
92
|
-
private deletedMode(mode: 'only' | 'include'): this {
|
|
93
|
-
this.softDelete.deleted(mode);
|
|
94
|
-
return this;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
private effectiveFilter(): ModelFilter<Shape> {
|
|
98
|
-
return this.softDelete.effectiveFilter(this.filterSpec);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** Sort results by one or more schema fields. */
|
|
102
|
-
sort(
|
|
103
|
-
spec: ModelSort<Shape>,
|
|
104
|
-
): ModelQuery<Shape, Result, false, Relations, Scopes, Mode, SoftDelete> {
|
|
105
|
-
this.sortSpec = spec;
|
|
106
|
-
return this as unknown as ModelQuery<Shape, Result, false, Relations, Scopes, Mode, SoftDelete>;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/** Skip a non-negative number of matching documents. */
|
|
110
|
-
skip(count: number): ModelQuery<Shape, Result, false, Relations, Scopes, Mode, SoftDelete> {
|
|
111
|
-
if (!Number.isInteger(count) || count < 0) {
|
|
112
|
-
throw new RangeError('Query skip must be a non-negative integer');
|
|
113
|
-
}
|
|
114
|
-
this.skipCount = count;
|
|
115
|
-
return this as unknown as ModelQuery<Shape, Result, false, Relations, Scopes, Mode, SoftDelete>;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** Limit the number of matching documents returned. */
|
|
119
|
-
limit(count: number): this {
|
|
120
|
-
if (!Number.isInteger(count) || count < 0) {
|
|
121
|
-
throw new RangeError('Query limit must be a non-negative integer');
|
|
122
|
-
}
|
|
123
|
-
this.limitCount = count;
|
|
124
|
-
return this;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/** Return only selected fields, while retaining MongoDB's default `_id`. */
|
|
128
|
-
select<Keys extends SelectableKey<Shape> = never>(
|
|
129
|
-
fields: readonly Keys[] = [],
|
|
130
|
-
): ModelQuery<
|
|
131
|
-
Shape,
|
|
132
|
-
SelectedDocument<Shape, Keys>,
|
|
133
|
-
CursorReady,
|
|
134
|
-
Relations,
|
|
135
|
-
Scopes,
|
|
136
|
-
Mode,
|
|
137
|
-
SoftDelete
|
|
138
|
-
> {
|
|
139
|
-
this.selectedFields = fields;
|
|
140
|
-
return this as unknown as ModelQuery<
|
|
141
|
-
Shape,
|
|
142
|
-
SelectedDocument<Shape, Keys>,
|
|
143
|
-
CursorReady,
|
|
144
|
-
Relations,
|
|
145
|
-
Scopes,
|
|
146
|
-
Mode,
|
|
147
|
-
SoftDelete
|
|
148
|
-
>;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/** Include hidden fields in the query result. */
|
|
152
|
-
show<Keys extends HiddenDocumentKey<Shape>>(
|
|
153
|
-
fields: readonly Keys[],
|
|
154
|
-
): ModelQuery<
|
|
155
|
-
Shape,
|
|
156
|
-
Result & Pick<ModelDocument<Shape>, Keys>,
|
|
157
|
-
CursorReady,
|
|
158
|
-
Relations,
|
|
159
|
-
Scopes,
|
|
160
|
-
Mode,
|
|
161
|
-
SoftDelete
|
|
162
|
-
> {
|
|
163
|
-
this.shownFields = fields;
|
|
164
|
-
return this as unknown as ModelQuery<
|
|
165
|
-
Shape,
|
|
166
|
-
Result & Pick<ModelDocument<Shape>, Keys>,
|
|
167
|
-
CursorReady,
|
|
168
|
-
Relations,
|
|
169
|
-
Scopes,
|
|
170
|
-
Mode,
|
|
171
|
-
SoftDelete
|
|
172
|
-
>;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/** Populate declared one-way relations, including nested relation arrays. */
|
|
176
|
-
populate<Specs extends PopulateSpecs<Relations>>(
|
|
177
|
-
this: Mode extends 'scope'
|
|
178
|
-
? never
|
|
179
|
-
: ModelQuery<Shape, Result, CursorReady, Relations, Scopes, Mode, SoftDelete>,
|
|
180
|
-
specs: Specs,
|
|
181
|
-
): ModelQuery<
|
|
182
|
-
Shape,
|
|
183
|
-
PopulatedResult<Result, Relations, Specs>,
|
|
184
|
-
CursorReady,
|
|
185
|
-
Relations,
|
|
186
|
-
Scopes,
|
|
187
|
-
'populate',
|
|
188
|
-
SoftDelete
|
|
189
|
-
> {
|
|
190
|
-
if (this.populationMode === 'scope') {
|
|
191
|
-
throw new InvalidQueryError(
|
|
192
|
-
'A query cannot combine a population scope with explicit population',
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
this.populationMode = 'populate';
|
|
196
|
-
this.populateSpecs = specs;
|
|
197
|
-
return this as unknown as ModelQuery<
|
|
198
|
-
Shape,
|
|
199
|
-
PopulatedResult<Result, Relations, Specs>,
|
|
200
|
-
CursorReady,
|
|
201
|
-
Relations,
|
|
202
|
-
Scopes,
|
|
203
|
-
'populate',
|
|
204
|
-
SoftDelete
|
|
205
|
-
>;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/** Apply a named population scope. */
|
|
209
|
-
with<Name extends ScopeName<Scopes>>(
|
|
210
|
-
this: Mode extends 'populate'
|
|
211
|
-
? never
|
|
212
|
-
: ModelQuery<Shape, Result, CursorReady, Relations, Scopes, Mode, SoftDelete>,
|
|
213
|
-
name: Name,
|
|
214
|
-
): ModelQuery<
|
|
215
|
-
Shape,
|
|
216
|
-
PopulatedResult<Result, Relations, Scopes[Name] & PopulateSpecs<Relations>>,
|
|
217
|
-
CursorReady,
|
|
218
|
-
Relations,
|
|
219
|
-
Scopes,
|
|
220
|
-
'scope',
|
|
221
|
-
SoftDelete
|
|
222
|
-
> {
|
|
223
|
-
if (this.populationMode === 'populate') {
|
|
224
|
-
throw new InvalidQueryError(
|
|
225
|
-
'A query cannot combine explicit population with a population scope',
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
this.populationMode = 'scope';
|
|
229
|
-
this.populateSpecs = this.scopes[name] as PopulateSpecs<Relations>;
|
|
230
|
-
return this as unknown as ModelQuery<
|
|
231
|
-
Shape,
|
|
232
|
-
PopulatedResult<Result, Relations, Scopes[Name] & PopulateSpecs<Relations>>,
|
|
233
|
-
CursorReady,
|
|
234
|
-
Relations,
|
|
235
|
-
Scopes,
|
|
236
|
-
'scope',
|
|
237
|
-
SoftDelete
|
|
238
|
-
>;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/** Count matching documents, optionally using MongoDB's collection estimate. */
|
|
242
|
-
async count(estimate = false): Promise<number> {
|
|
243
|
-
if (estimate) {
|
|
244
|
-
if (Object.keys(this.filterSpec).length > 0 || this.softDelete.isFiltered()) {
|
|
245
|
-
throw new EstimatedCountError();
|
|
246
|
-
}
|
|
247
|
-
return this.collection.estimatedDocumentCount();
|
|
248
|
-
}
|
|
249
|
-
return this.collection.countDocuments(
|
|
250
|
-
this.effectiveFilter() as MongoFilter<StoredDocument<Shape>>,
|
|
251
|
-
);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/** Return one `_id`-ordered page and the cursor for the next page. */
|
|
255
|
-
private createCursor(after?: ObjectId): ModelCursor<Shape, Result> {
|
|
256
|
-
if (this.limitCount === undefined || this.limitCount === 0) {
|
|
257
|
-
throw new CursorQueryError('Cursor queries require a positive limit');
|
|
258
|
-
}
|
|
259
|
-
if (this.skipCount !== undefined) {
|
|
260
|
-
throw new CursorQueryError('Cursor queries do not support skip');
|
|
261
|
-
}
|
|
262
|
-
if (this.sortSpec) {
|
|
263
|
-
const keys = Object.keys(this.sortSpec);
|
|
264
|
-
if (keys.length !== 1 || this.sortSpec._id !== 'asc') {
|
|
265
|
-
throw new CursorQueryError('Cursor queries require the default _id ascending sort');
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const filter = createCursorFilter(this.effectiveFilter(), after);
|
|
270
|
-
return new ModelCursor<Shape, Result>(
|
|
271
|
-
() => {
|
|
272
|
-
let cursor = this.collection
|
|
273
|
-
.find(filter as MongoFilter<StoredDocument<Shape>>)
|
|
274
|
-
.sort({ _id: 1 })
|
|
275
|
-
.limit((this.limitCount as number) + 1);
|
|
276
|
-
const projection = projectionFor(
|
|
277
|
-
this.fields,
|
|
278
|
-
this.hiddenFields,
|
|
279
|
-
this.selectedFields,
|
|
280
|
-
this.shownFields,
|
|
281
|
-
);
|
|
282
|
-
if (projection) {
|
|
283
|
-
cursor = cursor.project(projection);
|
|
284
|
-
}
|
|
285
|
-
return cursor;
|
|
286
|
-
},
|
|
287
|
-
this.limitCount,
|
|
288
|
-
(documents) => this.population.apply(documents, this.populateSpecs),
|
|
289
|
-
);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
private execute(): Promise<Result[]> {
|
|
293
|
-
return this.createQueryCursor()
|
|
294
|
-
.toArray()
|
|
295
|
-
.then((documents) =>
|
|
296
|
-
this.population.apply(documents as unknown as Result[], this.populateSpecs),
|
|
297
|
-
);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
async first(): Promise<Result | null> {
|
|
301
|
-
const documents = await this.createQueryCursor().limit(1).toArray();
|
|
302
|
-
const populated = await this.population.apply(
|
|
303
|
-
documents as unknown as Result[],
|
|
304
|
-
this.populateSpecs,
|
|
305
|
-
);
|
|
306
|
-
return populated[0] ?? null;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
private createQueryCursor() {
|
|
310
|
-
let cursor = this.collection.find(this.effectiveFilter() as MongoFilter<StoredDocument<Shape>>);
|
|
311
|
-
if (this.sortSpec) {
|
|
312
|
-
cursor = cursor.sort(this.sortSpec as Sort);
|
|
313
|
-
}
|
|
314
|
-
if (this.skipCount !== undefined) {
|
|
315
|
-
cursor = cursor.skip(this.skipCount);
|
|
316
|
-
}
|
|
317
|
-
if (this.limitCount !== undefined) {
|
|
318
|
-
cursor = cursor.limit(this.limitCount);
|
|
319
|
-
}
|
|
320
|
-
const projection = projectionFor(
|
|
321
|
-
this.fields,
|
|
322
|
-
this.hiddenFields,
|
|
323
|
-
this.selectedFields,
|
|
324
|
-
this.shownFields,
|
|
325
|
-
);
|
|
326
|
-
if (projection) {
|
|
327
|
-
cursor = cursor.project(projection);
|
|
328
|
-
}
|
|
329
|
-
return cursor;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
then<TResult1 = Result[], TResult2 = never>(
|
|
333
|
-
onfulfilled?: ((value: Result[]) => TResult1 | PromiseLike<TResult1>) | null,
|
|
334
|
-
onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,
|
|
335
|
-
): PromiseLike<TResult1 | TResult2> {
|
|
336
|
-
return this.execute().then(onfulfilled, onrejected);
|
|
337
|
-
}
|
|
338
|
-
}
|
package/src/query/query.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
export { ModelQuery } from './many-query.js';
|
|
2
|
-
export { ModelCursor } from './cursor.js';
|
|
3
|
-
export type {
|
|
4
|
-
HiddenDocumentKey,
|
|
5
|
-
ModelFilter,
|
|
6
|
-
ModelSort,
|
|
7
|
-
PopulateSpec,
|
|
8
|
-
PopulateSpecs,
|
|
9
|
-
PopulatedResult,
|
|
10
|
-
SelectedDocument,
|
|
11
|
-
SelectableKey,
|
|
12
|
-
StoredDocument,
|
|
13
|
-
VisibleDocument,
|
|
14
|
-
} from './types.js';
|
package/src/query/runtime.ts
DELETED
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
import type { Collection, Filter, ObjectId } from 'mongodb';
|
|
2
|
-
|
|
3
|
-
import type { Db } from '../connection/database.js';
|
|
4
|
-
import {
|
|
5
|
-
applySoftDeleteFilter,
|
|
6
|
-
type DeletedQueryMode,
|
|
7
|
-
type SoftDeleteMode,
|
|
8
|
-
} from '../model/soft-delete.js';
|
|
9
|
-
import type { SchemaRelationMap, SchemaShape } from '../schema/index.js';
|
|
10
|
-
import type { ModelFilter, StoredDocument } from './types.js';
|
|
11
|
-
|
|
12
|
-
export type RuntimePopulateSpec = {
|
|
13
|
-
ref: string;
|
|
14
|
-
select?: readonly string[];
|
|
15
|
-
populate?: readonly RuntimePopulateSpec[];
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export type DeletedMode = SoftDeleteMode;
|
|
19
|
-
|
|
20
|
-
export const normalizeProjectionFields = (fields: readonly string[]): string[] => {
|
|
21
|
-
const unique = new Set(fields);
|
|
22
|
-
return [...unique].filter((field) => {
|
|
23
|
-
let separator = field.indexOf('.');
|
|
24
|
-
while (separator !== -1) {
|
|
25
|
-
if (unique.has(field.slice(0, separator))) return false;
|
|
26
|
-
separator = field.indexOf('.', separator + 1);
|
|
27
|
-
}
|
|
28
|
-
return true;
|
|
29
|
-
});
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
export const projectionFor = (
|
|
33
|
-
fields: readonly string[],
|
|
34
|
-
hiddenFields: readonly string[],
|
|
35
|
-
selectedFields: readonly string[] | undefined,
|
|
36
|
-
shownFields: readonly string[],
|
|
37
|
-
): Record<string, 1> | undefined => {
|
|
38
|
-
const effectiveSelectedFields = selectedFields?.length ? selectedFields : undefined;
|
|
39
|
-
if (!effectiveSelectedFields && hiddenFields.length === 0 && shownFields.length === 0) {
|
|
40
|
-
return undefined;
|
|
41
|
-
}
|
|
42
|
-
const hidden = new Set(hiddenFields);
|
|
43
|
-
const visibleFields = effectiveSelectedFields ?? fields.filter((field) => !hidden.has(field));
|
|
44
|
-
return Object.fromEntries(
|
|
45
|
-
normalizeProjectionFields([...visibleFields, ...shownFields]).map((field) => [field, 1]),
|
|
46
|
-
);
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
export class SoftDeleteState<Shape extends SchemaShape> {
|
|
50
|
-
private mode: DeletedMode = 'active';
|
|
51
|
-
|
|
52
|
-
constructor(private readonly enabled: boolean) {}
|
|
53
|
-
|
|
54
|
-
includeDeleted(): void {
|
|
55
|
-
this.mode = 'all';
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
deleted(mode: DeletedQueryMode): void {
|
|
59
|
-
this.mode = mode === 'include' ? 'all' : 'deleted';
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
isFiltered(): boolean {
|
|
63
|
-
return this.enabled && this.mode !== 'all';
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
effectiveFilter(filter: ModelFilter<Shape>): ModelFilter<Shape> {
|
|
67
|
-
return applySoftDeleteFilter(filter, this.mode);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export class PopulationExecutor<Relations extends SchemaRelationMap> {
|
|
72
|
-
constructor(
|
|
73
|
-
private readonly db: Db,
|
|
74
|
-
private readonly relations: Relations,
|
|
75
|
-
) {}
|
|
76
|
-
|
|
77
|
-
async apply<Result extends object>(
|
|
78
|
-
documents: Result[],
|
|
79
|
-
specs: readonly RuntimePopulateSpec[],
|
|
80
|
-
): Promise<Result[]> {
|
|
81
|
-
await Promise.all(
|
|
82
|
-
documents.map(async (document) => {
|
|
83
|
-
for (const spec of specs) {
|
|
84
|
-
await this.populateDocument(document as Record<string, unknown>, spec, this.relations);
|
|
85
|
-
}
|
|
86
|
-
}),
|
|
87
|
-
);
|
|
88
|
-
return documents;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
async applyOne<Result extends object>(
|
|
92
|
-
document: Result,
|
|
93
|
-
specs: readonly RuntimePopulateSpec[],
|
|
94
|
-
): Promise<Result> {
|
|
95
|
-
await this.apply([document], specs);
|
|
96
|
-
return document;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
private async populateDocument(
|
|
100
|
-
document: Record<string, unknown>,
|
|
101
|
-
spec: RuntimePopulateSpec,
|
|
102
|
-
relations: SchemaRelationMap,
|
|
103
|
-
): Promise<void> {
|
|
104
|
-
const relation = relations[spec.ref];
|
|
105
|
-
const target = relation.resolve();
|
|
106
|
-
const value = document[relation.localField];
|
|
107
|
-
const targetRelations = target.relationMap as SchemaRelationMap;
|
|
108
|
-
const nestedRelationFields = (spec.populate ?? []).map(
|
|
109
|
-
(nested) => targetRelations[nested.ref].localField,
|
|
110
|
-
);
|
|
111
|
-
const hiddenTargetFields = new Set(target.hiddenFields);
|
|
112
|
-
const projectionFields = normalizeProjectionFields([
|
|
113
|
-
...new Set(spec.select ?? target.fields.filter((field) => !hiddenTargetFields.has(field))),
|
|
114
|
-
...nestedRelationFields,
|
|
115
|
-
]);
|
|
116
|
-
const related = value
|
|
117
|
-
? await this.db
|
|
118
|
-
.collectionFor(target)
|
|
119
|
-
.findOne(
|
|
120
|
-
{ [relation.foreignField]: value },
|
|
121
|
-
{ projection: Object.fromEntries(projectionFields.map((field) => [field, 1])) },
|
|
122
|
-
)
|
|
123
|
-
: null;
|
|
124
|
-
if (related && spec.populate) {
|
|
125
|
-
for (const nested of spec.populate) {
|
|
126
|
-
await this.populateDocument(related, nested, targetRelations);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
document[spec.ref] = related;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export const createCursorFilter = <Shape extends SchemaShape>(
|
|
134
|
-
filter: ModelFilter<Shape>,
|
|
135
|
-
after?: ObjectId,
|
|
136
|
-
): ModelFilter<Shape> =>
|
|
137
|
-
(after ? { $and: [filter, { _id: { $gt: after } }] } : filter) as ModelFilter<Shape>;
|
|
138
|
-
|
|
139
|
-
export type QueryCollection<Shape extends SchemaShape> = Collection<StoredDocument<Shape>>;
|
|
140
|
-
export type MongoQueryFilter<Shape extends SchemaShape> = Filter<StoredDocument<Shape>>;
|
package/src/query/types.ts
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import { ObjectId, type Condition, type Document, type RootFilterOperators } from 'mongodb';
|
|
2
|
-
|
|
3
|
-
import type { SchemaShape } from '../schema/contracts.js';
|
|
4
|
-
import type { Infer, Schema, SchemaRelationMap } from '../schema/index.js';
|
|
5
|
-
import type { ModelCursor } from './cursor.js';
|
|
6
|
-
|
|
7
|
-
export type StoredDocument<Shape extends SchemaShape> = Infer<Schema<Shape>> & Document;
|
|
8
|
-
|
|
9
|
-
type ModelFilterForDocument<
|
|
10
|
-
DocumentShape extends Document,
|
|
11
|
-
FieldShape extends object = DocumentShape,
|
|
12
|
-
> = Partial<{
|
|
13
|
-
[Key in keyof FieldShape]: Condition<FieldShape[Key]>;
|
|
14
|
-
}> &
|
|
15
|
-
Partial<
|
|
16
|
-
Pick<
|
|
17
|
-
RootFilterOperators<DocumentShape>,
|
|
18
|
-
'$comment' | '$expr' | '$jsonSchema' | '$text' | '$where'
|
|
19
|
-
>
|
|
20
|
-
> & {
|
|
21
|
-
$and?: ModelFilterForDocument<DocumentShape, FieldShape>[];
|
|
22
|
-
$nor?: ModelFilterForDocument<DocumentShape, FieldShape>[];
|
|
23
|
-
$or?: ModelFilterForDocument<DocumentShape, FieldShape>[];
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export type ModelFilter<Shape extends SchemaShape> = ModelFilterForDocument<
|
|
27
|
-
StoredDocument<Shape>,
|
|
28
|
-
Infer<Schema<Shape>>
|
|
29
|
-
>;
|
|
30
|
-
|
|
31
|
-
type SortDirection = 'asc' | 'desc';
|
|
32
|
-
export type ModelSort<Shape extends SchemaShape> = Partial<
|
|
33
|
-
Record<Extract<keyof Infer<Schema<Shape>>, string>, SortDirection>
|
|
34
|
-
>;
|
|
35
|
-
|
|
36
|
-
export type ModelDocument<Shape extends SchemaShape> = Infer<Schema<Shape>>;
|
|
37
|
-
export type HiddenKey<Shape extends SchemaShape> = {
|
|
38
|
-
[Key in keyof Shape]: Shape[Key] extends { readonly __hidden: true } ? Key : never;
|
|
39
|
-
}[keyof Shape];
|
|
40
|
-
export type HiddenDocumentKey<Shape extends SchemaShape> = Extract<
|
|
41
|
-
HiddenKey<Shape>,
|
|
42
|
-
keyof ModelDocument<Shape>
|
|
43
|
-
> &
|
|
44
|
-
string;
|
|
45
|
-
type NestedDocumentKeys<Value, Prefix extends string = ''> = Value extends object
|
|
46
|
-
? Value extends ObjectId | Date
|
|
47
|
-
? never
|
|
48
|
-
: {
|
|
49
|
-
[Key in Extract<keyof Value, string>]: NonNullable<Value[Key]> extends object
|
|
50
|
-
? `${Prefix}${Key}` | `${Prefix}${Key}.${NestedDocumentKeys<NonNullable<Value[Key]>>}`
|
|
51
|
-
: `${Prefix}${Key}`;
|
|
52
|
-
}[Extract<keyof Value, string>]
|
|
53
|
-
: never;
|
|
54
|
-
type NestedSelectableKey<Shape extends SchemaShape> = {
|
|
55
|
-
[Key in Extract<keyof Shape, string>]: Key extends keyof ModelDocument<Shape>
|
|
56
|
-
? NonNullable<ModelDocument<Shape>[Key]> extends object
|
|
57
|
-
? `${Key}.${NestedDocumentKeys<NonNullable<ModelDocument<Shape>[Key]>>}`
|
|
58
|
-
: never
|
|
59
|
-
: never;
|
|
60
|
-
}[Extract<keyof Shape, string>];
|
|
61
|
-
export type CursorMethod<
|
|
62
|
-
Shape extends SchemaShape,
|
|
63
|
-
Result extends object,
|
|
64
|
-
Ready extends boolean,
|
|
65
|
-
> = Ready extends true ? (after?: ObjectId) => ModelCursor<Shape, Result> : undefined;
|
|
66
|
-
export type SelectableKey<Shape extends SchemaShape> =
|
|
67
|
-
| Exclude<Extract<keyof ModelDocument<Shape>, string>, '_id' | HiddenDocumentKey<Shape>>
|
|
68
|
-
| NestedSelectableKey<Shape>;
|
|
69
|
-
type PathSelection<Value, Path extends string> = Path extends `${infer Head}.${infer Tail}`
|
|
70
|
-
? Head extends keyof Value
|
|
71
|
-
? { [Key in Head]: PathSelection<NonNullable<Value[Key]>, Tail> }
|
|
72
|
-
: never
|
|
73
|
-
: Path extends keyof Value
|
|
74
|
-
? Pick<Value, Path>
|
|
75
|
-
: never;
|
|
76
|
-
type UnionToIntersection<Value> = (Value extends unknown ? (input: Value) => void : never) extends (
|
|
77
|
-
input: infer Intersection,
|
|
78
|
-
) => void
|
|
79
|
-
? Intersection
|
|
80
|
-
: never;
|
|
81
|
-
type Simplify<Value> = { [Key in keyof Value]: Value[Key] };
|
|
82
|
-
export type VisibleDocument<Shape extends SchemaShape> = Omit<
|
|
83
|
-
ModelDocument<Shape>,
|
|
84
|
-
Extract<HiddenKey<Shape>, keyof ModelDocument<Shape>>
|
|
85
|
-
>;
|
|
86
|
-
export type SelectedDocument<Shape extends SchemaShape, Key extends SelectableKey<Shape>> = [
|
|
87
|
-
Key,
|
|
88
|
-
] extends [never]
|
|
89
|
-
? VisibleDocument<Shape>
|
|
90
|
-
: Simplify<
|
|
91
|
-
Pick<ModelDocument<Shape>, '_id'> &
|
|
92
|
-
UnionToIntersection<PathSelection<ModelDocument<Shape>, Extract<Key, string>>>
|
|
93
|
-
>;
|
|
94
|
-
type RelationTarget<Relation> = Relation extends { resolve: () => infer Target } ? Target : never;
|
|
95
|
-
type RelationDocument<Relation> =
|
|
96
|
-
RelationTarget<Relation> extends Schema<infer TargetShape, any>
|
|
97
|
-
? Infer<Schema<TargetShape>>
|
|
98
|
-
: never;
|
|
99
|
-
type RelationMapOf<Relation> =
|
|
100
|
-
RelationTarget<Relation> extends { readonly relationMap: infer TargetRelations }
|
|
101
|
-
? TargetRelations extends SchemaRelationMap
|
|
102
|
-
? TargetRelations
|
|
103
|
-
: {}
|
|
104
|
-
: {};
|
|
105
|
-
export type ScopeName<Scopes> = Extract<keyof Scopes, string>;
|
|
106
|
-
export type PopulationMode = 'none' | 'populate' | 'scope';
|
|
107
|
-
type RelationSelect<Relation> =
|
|
108
|
-
RelationTarget<Relation> extends Schema<infer TargetShape, any>
|
|
109
|
-
? Exclude<SelectableKey<TargetShape>, '_id'>
|
|
110
|
-
: never;
|
|
111
|
-
|
|
112
|
-
export type PopulateSpec<Relations extends SchemaRelationMap> = {
|
|
113
|
-
[Name in Extract<keyof Relations, string>]: {
|
|
114
|
-
ref: Name;
|
|
115
|
-
select?: readonly RelationSelect<Relations[Name]>[];
|
|
116
|
-
populate?: PopulateSpecs<RelationMapOf<Relations[Name]>>;
|
|
117
|
-
};
|
|
118
|
-
}[Extract<keyof Relations, string>];
|
|
119
|
-
export type PopulateSpecs<Relations extends SchemaRelationMap> = readonly PopulateSpec<Relations>[];
|
|
120
|
-
export type PopulatedResult<
|
|
121
|
-
Result extends object,
|
|
122
|
-
Relations extends SchemaRelationMap,
|
|
123
|
-
Specs extends PopulateSpecs<Relations>,
|
|
124
|
-
> = Omit<Result, Extract<Specs[number]['ref'], keyof Result>> & {
|
|
125
|
-
[Spec in Specs[number] as Spec['ref']]: PopulatedRelation<Relations[Spec['ref']], Spec> | null;
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
type PopulatedRelation<Relation, Spec> = Spec extends {
|
|
129
|
-
populate: infer Nested extends PopulateSpecs<RelationMapOf<Relation>>;
|
|
130
|
-
}
|
|
131
|
-
? RelationDocument<Relation> extends infer PopulatedDocument extends object
|
|
132
|
-
? PopulatedResult<PopulatedDocument, RelationMapOf<Relation>, Nested>
|
|
133
|
-
: never
|
|
134
|
-
: RelationDocument<Relation>;
|