mongoose-killer 0.1.0

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,439 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.addIdVirtual = exports.Query = void 0;
4
+ const populate_js_1 = require("./populate.js");
5
+ const castFilter_js_1 = require("./castFilter.js");
6
+ const CastIdsConflictError_js_1 = require("./CastIdsConflictError.js");
7
+ class Query {
8
+ model;
9
+ op;
10
+ filter;
11
+ update;
12
+ pipeline;
13
+ distinctField;
14
+ // Chained options. We accumulate them and push to the driver at exec.
15
+ _sort;
16
+ _limit;
17
+ _skip;
18
+ _select;
19
+ _hint;
20
+ _comment;
21
+ _session;
22
+ _readPref;
23
+ _populates = [];
24
+ _lean = false;
25
+ // Ordered log of cast-id overrides on this chain.
26
+ // "on" — user called .castIds()
27
+ // "off" — user called .skipCastIds()
28
+ // Empty = neither was called; use model's autoCastIds default.
29
+ // Both present at exec time = conflict, resolve per policy.
30
+ _castIdsOps = [];
31
+ // Options bag set by entry verb (e.g. { new, upsert, returnDocument }).
32
+ // Stays mostly opaque — we forward what the driver understands.
33
+ opOptions;
34
+ // Memoize result so a Query (like mongoose) can be awaited / .then'd
35
+ // exactly once and re-runs throw the mongoose error string.
36
+ _executed = false;
37
+ _execPromise = null;
38
+ constructor(init) {
39
+ this.model = init.model;
40
+ this.op = init.op;
41
+ this.filter = init.filter;
42
+ this.update = init.update;
43
+ this.pipeline = init.pipeline ?? [];
44
+ this.distinctField = init.distinctField;
45
+ this.opOptions = init.options ?? {};
46
+ }
47
+ // ---- chain methods ------------------------------------------------
48
+ lean(on = true) {
49
+ this._lean = on;
50
+ return this;
51
+ }
52
+ sort(spec) {
53
+ this._sort = spec;
54
+ return this;
55
+ }
56
+ limit(n) {
57
+ this._limit = n;
58
+ return this;
59
+ }
60
+ skip(n) {
61
+ this._skip = n;
62
+ return this;
63
+ }
64
+ select(spec) {
65
+ // Mongoose accepts strings ("name -_id") and objects ({ name: 1 }).
66
+ // The driver only takes objects, so normalize.
67
+ this._select = typeof spec === "string" ? parseSelectString(spec) : spec;
68
+ return this;
69
+ }
70
+ hint(h) {
71
+ this._hint = h;
72
+ return this;
73
+ }
74
+ comment(c) {
75
+ this._comment = c;
76
+ return this;
77
+ }
78
+ session(s) {
79
+ this._session = s ?? undefined;
80
+ return this;
81
+ }
82
+ read(pref) {
83
+ this._readPref = pref;
84
+ return this;
85
+ }
86
+ // Force ObjectId auto-cast ON for this query, regardless of the
87
+ // constructor's `autoCastIds` setting. See README + SECURITY.md.
88
+ castIds() {
89
+ this._castIdsOps.push("on");
90
+ return this;
91
+ }
92
+ // Force ObjectId auto-cast OFF for this query, regardless of the
93
+ // constructor's `autoCastIds` setting.
94
+ skipCastIds() {
95
+ this._castIdsOps.push("off");
96
+ return this;
97
+ }
98
+ populate(arg) {
99
+ // Mongoose semantics: bare `.populate()` populates every ref in the
100
+ // schema. We mirror that by populating every path in the model's
101
+ // populates map.
102
+ if (arg === undefined) {
103
+ const allPaths = Object.keys(this.model.populates);
104
+ this._populates.push(...allPaths.map((path) => ({ path })));
105
+ return this;
106
+ }
107
+ const specs = (0, populate_js_1.normalizePopulateArg)(arg);
108
+ this._populates.push(...specs);
109
+ return this;
110
+ }
111
+ where(field, value) {
112
+ // Mongoose-style .where('foo').equals(bar) / .where('foo', bar).
113
+ // Inventory shows minimal usage; just support the two-arg form
114
+ // and merge into filter.
115
+ this.filter = this.filter ?? {};
116
+ if (arguments.length === 2) {
117
+ this.filter[field] = value;
118
+ }
119
+ return this;
120
+ }
121
+ // Mongoose-style filter combinators. Merge into the existing filter
122
+ // exactly the way mongoose does — push onto the existing $or/$and
123
+ // array if one exists, else create it.
124
+ or(conditions) {
125
+ this.filter = this.filter ?? {};
126
+ this.filter.$or = [...(this.filter.$or ?? []), ...conditions];
127
+ return this;
128
+ }
129
+ and(conditions) {
130
+ this.filter = this.filter ?? {};
131
+ this.filter.$and = [...(this.filter.$and ?? []), ...conditions];
132
+ return this;
133
+ }
134
+ nor(conditions) {
135
+ this.filter = this.filter ?? {};
136
+ this.filter.$nor = [...(this.filter.$nor ?? []), ...conditions];
137
+ return this;
138
+ }
139
+ // Aggregate-only: matches mongoose's Query.option (loose passthrough).
140
+ option(o) {
141
+ this.opOptions = { ...this.opOptions, ...o };
142
+ return this;
143
+ }
144
+ // ---- thenable terminus ---------------------------------------------
145
+ exec() {
146
+ if (this._executed) {
147
+ // Match mongoose's actual behavior on re-execution.
148
+ return Promise.reject(new Error("Query was already executed: re-run not supported"));
149
+ }
150
+ this._executed = true;
151
+ this._execPromise = this.run();
152
+ return this._execPromise;
153
+ }
154
+ then(onResolve, onReject) {
155
+ return this.exec().then(onResolve, onReject);
156
+ }
157
+ catch(onReject) {
158
+ return this.exec().catch(onReject);
159
+ }
160
+ finally(onFinally) {
161
+ return this.exec().finally(onFinally);
162
+ }
163
+ // ---- dispatch ------------------------------------------------------
164
+ async run() {
165
+ switch (this.op) {
166
+ case "find":
167
+ return this.runFind();
168
+ case "findOne":
169
+ return this.runFindOne();
170
+ case "findOneAndUpdate":
171
+ return this.runFindOneAndUpdate();
172
+ case "findOneAndDelete":
173
+ return this.runFindOneAndDelete();
174
+ case "updateOne":
175
+ return this.runUpdate("updateOne");
176
+ case "updateMany":
177
+ return this.runUpdate("updateMany");
178
+ case "deleteOne":
179
+ return this.runDelete("deleteOne");
180
+ case "deleteMany":
181
+ return this.runDelete("deleteMany");
182
+ case "countDocuments":
183
+ return this.runCount();
184
+ case "distinct":
185
+ return this.runDistinct();
186
+ case "exists":
187
+ return this.runExists();
188
+ case "aggregate":
189
+ return this.runAggregate();
190
+ }
191
+ }
192
+ // Apply 24-hex-string → ObjectId coercion, IF the resolved
193
+ // per-query setting says we should. Done once per exec, at the
194
+ // boundary just before handing the filter to the driver.
195
+ //
196
+ // The cast is on by default for Mongoose parity (Mongoose does the
197
+ // same silently from the schema). Opt out globally with
198
+ // `autoCastIds: false`, or per-query with `.skipCastIds()` when the
199
+ // filter value happens to look like a hex id but isn't.
200
+ castedFilter() {
201
+ const filter = this.filter ?? {};
202
+ return this.resolveCastIds() ? (0, castFilter_js_1.castFilter)(filter) : filter;
203
+ }
204
+ // Resolve the per-query cast-id setting:
205
+ // 0 calls of either → constructor default
206
+ // only .castIds() was called → true
207
+ // only .skipCastIds() was called → false
208
+ // both were called → consult conflict policy
209
+ resolveCastIds() {
210
+ const ops = this._castIdsOps;
211
+ const hasOn = ops.includes("on");
212
+ const hasOff = ops.includes("off");
213
+ if (!hasOn && !hasOff)
214
+ return this.model.autoCastIds;
215
+ if (hasOn && !hasOff)
216
+ return true;
217
+ if (!hasOn && hasOff)
218
+ return false;
219
+ // Conflict: both were called at least once.
220
+ switch (this.model.castIdsConflictPolicy) {
221
+ case "throw":
222
+ throw new CastIdsConflictError_js_1.CastIdsConflictError({
223
+ castIdsCallCount: ops.filter((o) => o === "on").length,
224
+ skipCastIdsCallCount: ops.filter((o) => o === "off").length,
225
+ });
226
+ case "firstWins":
227
+ return ops[0] === "on";
228
+ case "lastWins":
229
+ return ops[ops.length - 1] === "on";
230
+ case "defaultWins":
231
+ return this.model.autoCastIds;
232
+ }
233
+ }
234
+ findOptions() {
235
+ const o = {};
236
+ if (this._sort)
237
+ o.sort = this._sort;
238
+ if (this._limit !== undefined)
239
+ o.limit = this._limit;
240
+ if (this._skip !== undefined)
241
+ o.skip = this._skip;
242
+ if (this._select)
243
+ o.projection = this._select;
244
+ if (this._hint)
245
+ o.hint = this._hint;
246
+ if (this._comment)
247
+ o.comment = this._comment;
248
+ if (this._session)
249
+ o.session = this._session;
250
+ if (this._readPref)
251
+ o.readPreference = this._readPref;
252
+ return o;
253
+ }
254
+ writeOptions() {
255
+ const o = { ...this.opOptions };
256
+ if (this._hint)
257
+ o.hint = this._hint;
258
+ if (this._comment)
259
+ o.comment = this._comment;
260
+ if (this._session)
261
+ o.session = this._session;
262
+ return o;
263
+ }
264
+ async runFind() {
265
+ const cursor = this.model.collection.find(this.castedFilter(), this.findOptions());
266
+ let docs = await cursor.toArray();
267
+ if (this._populates.length) {
268
+ docs = await (0, populate_js_1.runPopulate)(docs, this._populates, {
269
+ ownerModel: this.model,
270
+ session: this._session,
271
+ });
272
+ }
273
+ if (!this._lean)
274
+ docs.forEach(exports.addIdVirtual);
275
+ return docs;
276
+ }
277
+ async runFindOne() {
278
+ const doc = await this.model.collection.findOne(this.castedFilter(), this.findOptions());
279
+ if (!doc)
280
+ return null;
281
+ if (this._populates.length) {
282
+ const [populated] = await (0, populate_js_1.runPopulate)([doc], this._populates, {
283
+ ownerModel: this.model,
284
+ session: this._session,
285
+ });
286
+ if (!this._lean)
287
+ (0, exports.addIdVirtual)(populated);
288
+ return populated;
289
+ }
290
+ if (!this._lean)
291
+ (0, exports.addIdVirtual)(doc);
292
+ return doc;
293
+ }
294
+ async runFindOneAndUpdate() {
295
+ // Mongoose default: { new: false, upsert: false }, returns the doc
296
+ // (pre-update unless new: true). Driver default in modern driver is
297
+ // returnDocument: 'before' which matches mongoose. We only translate
298
+ // { new: true } → returnDocument: 'after'.
299
+ const o = this.writeOptions();
300
+ if (o.new === true)
301
+ o.returnDocument = "after";
302
+ delete o.new;
303
+ if (this._select)
304
+ o.projection = this._select;
305
+ if (this._sort)
306
+ o.sort = this._sort;
307
+ const res = await this.model.collection.findOneAndUpdate(this.castedFilter(), wrapInSetIfPlain(this.update), o);
308
+ // Driver returns the document directly (not { value }) in modern
309
+ // versions. Mongoose unwraps to the doc too.
310
+ const doc = res && typeof res === "object" && "value" in res
311
+ ? res.value
312
+ : res;
313
+ if (!doc)
314
+ return null;
315
+ if (this._populates.length) {
316
+ const [populated] = await (0, populate_js_1.runPopulate)([doc], this._populates, {
317
+ ownerModel: this.model,
318
+ session: this._session,
319
+ });
320
+ if (!this._lean)
321
+ (0, exports.addIdVirtual)(populated);
322
+ return populated;
323
+ }
324
+ if (!this._lean)
325
+ (0, exports.addIdVirtual)(doc);
326
+ return doc;
327
+ }
328
+ async runFindOneAndDelete() {
329
+ const o = this.writeOptions();
330
+ if (this._select)
331
+ o.projection = this._select;
332
+ if (this._sort)
333
+ o.sort = this._sort;
334
+ const res = await this.model.collection.findOneAndDelete(this.castedFilter(), o);
335
+ const doc = res && typeof res === "object" && "value" in res
336
+ ? res.value
337
+ : res;
338
+ if (doc && !this._lean)
339
+ (0, exports.addIdVirtual)(doc);
340
+ return doc;
341
+ }
342
+ async runUpdate(kind) {
343
+ return this.model.collection[kind](this.castedFilter(), wrapInSetIfPlain(this.update), this.writeOptions());
344
+ }
345
+ async runDelete(kind) {
346
+ return this.model.collection[kind](this.castedFilter(), this.writeOptions());
347
+ }
348
+ async runCount() {
349
+ return this.model.collection.countDocuments(this.castedFilter(), this.findOptions());
350
+ }
351
+ async runDistinct() {
352
+ return this.model.collection.distinct(this.distinctField, this.castedFilter(), this.findOptions());
353
+ }
354
+ async runExists() {
355
+ const doc = await this.model.collection.findOne(this.castedFilter(), {
356
+ projection: { _id: 1 },
357
+ session: this._session,
358
+ });
359
+ return doc ? { _id: doc._id } : null;
360
+ }
361
+ async runAggregate() {
362
+ const opts = {};
363
+ if (this._session)
364
+ opts.session = this._session;
365
+ if (this._hint)
366
+ opts.hint = this._hint;
367
+ if (this._comment)
368
+ opts.comment = this._comment;
369
+ if (this.opOptions.allowDiskUse)
370
+ opts.allowDiskUse = this.opOptions.allowDiskUse;
371
+ // Cast hex strings in $match stages — but only if auto-cast is
372
+ // resolved-on for this query, same gating as the top-level
373
+ // filter. Other stages may contain expressions, but only $match
374
+ // is a true "filter" position.
375
+ const castOn = this.resolveCastIds();
376
+ const pipeline = this.pipeline.map((stage) => {
377
+ if (castOn && stage && typeof stage === "object" && "$match" in stage) {
378
+ return { ...stage, $match: (0, castFilter_js_1.castFilter)(stage.$match) };
379
+ }
380
+ return stage;
381
+ });
382
+ return this.model.collection.aggregate(pipeline, opts).toArray();
383
+ }
384
+ }
385
+ exports.Query = Query;
386
+ // Mongoose silently wraps a plain object update in `$set`. The native
387
+ // driver requires atomic operators or a pipeline. Pipeline updates are
388
+ // Mongoose hydrated docs expose `.id` as a getter that returns
389
+ // `this._id.toString()`. Lean docs don't (they're plain objects with
390
+ // just _id). We mirror that: every non-lean return path runs each
391
+ // doc through addIdVirtual to attach a non-enumerable `id` getter.
392
+ //
393
+ // Non-enumerable so JSON.stringify / spread / Object.keys see the
394
+ // same shape as a lean doc. Idempotent: skip if `id` is already an
395
+ // own property (already set, already populated, doc came from
396
+ // somewhere else).
397
+ const addIdVirtual = (doc) => {
398
+ if (!doc || typeof doc !== "object")
399
+ return;
400
+ if (Object.prototype.hasOwnProperty.call(doc, "id"))
401
+ return;
402
+ Object.defineProperty(doc, "id", {
403
+ get() {
404
+ // Mongoose's `.id` returns `null` when `_id` is missing (rather
405
+ // than `undefined`). Matching that exactly — drop-in compat.
406
+ return this._id == null ? null : this._id.toString();
407
+ },
408
+ enumerable: false,
409
+ configurable: true,
410
+ });
411
+ };
412
+ exports.addIdVirtual = addIdVirtual;
413
+ // arrays — pass through. Updates that already have at least one
414
+ // top-level `$`-key go through. Anything else gets wrapped.
415
+ const wrapInSetIfPlain = (update) => {
416
+ if (update == null)
417
+ return update;
418
+ if (Array.isArray(update))
419
+ return update;
420
+ if (typeof update !== "object")
421
+ return update;
422
+ const keys = Object.keys(update);
423
+ if (keys.length === 0)
424
+ return update;
425
+ const hasOperator = keys.some((k) => k.startsWith("$"));
426
+ if (hasOperator)
427
+ return update;
428
+ return { $set: update };
429
+ };
430
+ const parseSelectString = (s) => {
431
+ const out = {};
432
+ for (const tok of s.split(/\s+/).filter(Boolean)) {
433
+ if (tok.startsWith("-"))
434
+ out[tok.slice(1)] = 0;
435
+ else
436
+ out[tok] = 1;
437
+ }
438
+ return out;
439
+ };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Auto-cast filter values that look like ObjectIds.
3
+ *
4
+ * Mongoose does this via schema knowledge ("this field is an ObjectId
5
+ * ref, so cast the string"). We don't have schemas, so we use the
6
+ * structurally atomic rule:
7
+ *
8
+ * A 24-character hex string in a filter position is an ObjectId.
9
+ *
10
+ * This is the boundary where call sites stop having to remember to
11
+ * wrap with `new ObjectId(...)`. Without it, queries like
12
+ * `{ authorID: user.id }` silently match nothing because the stored
13
+ * value is an ObjectId but the filter is a string.
14
+ *
15
+ * Scope:
16
+ * - Only the filter argument. Updates ($set/$push/$inc payloads) are
17
+ * left alone — the user controls what gets written.
18
+ * - Recursive into nested objects so $and/$or/$nor work, as do
19
+ * positional operators like `{ items.0.ownerID: "..." }`.
20
+ * - $in arrays: each element is cast individually.
21
+ * - $regex / $text / $expr / $where / $jsonSchema / $where are
22
+ * skipped — those operators take strings, not ObjectIds.
23
+ */
24
+ export declare const castFilter: (filter: any) => any;
25
+ //# sourceMappingURL=castFilter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"castFilter.d.ts","sourceRoot":"","sources":["../../src/castFilter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AA2BH,eAAO,MAAM,UAAU,GAAI,QAAQ,GAAG,KAAG,GAgBxC,CAAC"}
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ /**
3
+ * Auto-cast filter values that look like ObjectIds.
4
+ *
5
+ * Mongoose does this via schema knowledge ("this field is an ObjectId
6
+ * ref, so cast the string"). We don't have schemas, so we use the
7
+ * structurally atomic rule:
8
+ *
9
+ * A 24-character hex string in a filter position is an ObjectId.
10
+ *
11
+ * This is the boundary where call sites stop having to remember to
12
+ * wrap with `new ObjectId(...)`. Without it, queries like
13
+ * `{ authorID: user.id }` silently match nothing because the stored
14
+ * value is an ObjectId but the filter is a string.
15
+ *
16
+ * Scope:
17
+ * - Only the filter argument. Updates ($set/$push/$inc payloads) are
18
+ * left alone — the user controls what gets written.
19
+ * - Recursive into nested objects so $and/$or/$nor work, as do
20
+ * positional operators like `{ items.0.ownerID: "..." }`.
21
+ * - $in arrays: each element is cast individually.
22
+ * - $regex / $text / $expr / $where / $jsonSchema / $where are
23
+ * skipped — those operators take strings, not ObjectIds.
24
+ */
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.castFilter = void 0;
27
+ const bson_1 = require("bson");
28
+ const HEX_24 = /^[a-f0-9]{24}$/i;
29
+ // Operators whose values are intentional strings/expressions; never
30
+ // cast their contents.
31
+ const STRING_OPERATORS = new Set([
32
+ "$regex",
33
+ "$options",
34
+ "$text",
35
+ "$where",
36
+ "$expr",
37
+ "$jsonSchema",
38
+ "$comment",
39
+ "$search",
40
+ "$language",
41
+ "$caseSensitive",
42
+ "$diacriticSensitive",
43
+ ]);
44
+ const isHex24 = (v) => typeof v === "string" && HEX_24.test(v);
45
+ const toOidIfHex = (v) => (isHex24(v) ? new bson_1.ObjectId(v) : v);
46
+ const castFilter = (filter) => {
47
+ if (filter == null)
48
+ return filter;
49
+ if (Array.isArray(filter))
50
+ return filter.map(exports.castFilter);
51
+ if (typeof filter !== "object")
52
+ return filter;
53
+ // Leave ObjectId / Date / RegExp / etc. untouched.
54
+ if (isBsonLike(filter))
55
+ return filter;
56
+ const out = {};
57
+ for (const [key, value] of Object.entries(filter)) {
58
+ if (STRING_OPERATORS.has(key)) {
59
+ out[key] = value;
60
+ continue;
61
+ }
62
+ out[key] = castValue(value);
63
+ }
64
+ return out;
65
+ };
66
+ exports.castFilter = castFilter;
67
+ const castValue = (value) => {
68
+ if (isHex24(value))
69
+ return new bson_1.ObjectId(value);
70
+ if (value == null)
71
+ return value;
72
+ if (Array.isArray(value))
73
+ return value.map(castValue);
74
+ if (typeof value !== "object")
75
+ return value;
76
+ if (isBsonLike(value))
77
+ return value;
78
+ // Object — could be an operator spec ({ $in: [...] }) or a nested
79
+ // subdocument. Either way, recurse with the same rules.
80
+ const out = {};
81
+ for (const [k, v] of Object.entries(value)) {
82
+ if (STRING_OPERATORS.has(k)) {
83
+ out[k] = v;
84
+ continue;
85
+ }
86
+ out[k] = castValue(v);
87
+ }
88
+ return out;
89
+ };
90
+ // True for values we should pass through untouched (ObjectId, Date,
91
+ // Decimal128, Buffer, RegExp, etc.). Heuristic: non-plain objects.
92
+ const isBsonLike = (v) => {
93
+ if (v instanceof Date)
94
+ return true;
95
+ if (v instanceof RegExp)
96
+ return true;
97
+ // bson types and similar — anything whose prototype isn't plain Object
98
+ const proto = Object.getPrototypeOf(v);
99
+ return proto !== Object.prototype && proto !== null;
100
+ };
@@ -0,0 +1,3 @@
1
+ import type { CreateGetModelOpts, GetModel } from "./types.js";
2
+ export declare const createGetModel: (opts: CreateGetModelOpts) => GetModel;
3
+ //# sourceMappingURL=createGetModel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createGetModel.d.ts","sourceRoot":"","sources":["../../src/createGetModel.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAK/D,eAAO,MAAM,cAAc,GAAI,MAAM,kBAAkB,KAAG,QA0CzD,CAAC"}
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createGetModel = void 0;
4
+ const Model_js_1 = require("./Model.js");
5
+ // Build a registry of Model instances keyed by name. The returned
6
+ // getModel is the only thing call sites see; they never touch Model
7
+ // directly.
8
+ const createGetModel = (opts) => {
9
+ const registry = new Map();
10
+ // collection name -> model name, so populate can resolve refs by
11
+ // collection (matches how the test fixtures express them).
12
+ const collectionToModelName = new Map();
13
+ for (const [name, collection] of Object.entries(opts.models)) {
14
+ collectionToModelName.set(collection, name);
15
+ }
16
+ const getModelByName = (name) => {
17
+ const m = registry.get(name);
18
+ if (!m)
19
+ throw new Error(`Unknown model: ${name}`);
20
+ return m;
21
+ };
22
+ // Resolve defaults once, here, so each Model gets a frozen copy.
23
+ // autoCastIds defaults to ON for Mongoose-parity — Mongoose silently
24
+ // coerces 24-char hex strings to ObjectId, and viper is a drop-in
25
+ // replacement. Set autoCastIds: false to opt out.
26
+ const autoCastIds = opts.autoCastIds !== false;
27
+ // Conflict resolution defaults to "throw" so .castIds() and
28
+ // .skipCastIds() colliding on the same query surfaces the bug
29
+ // instead of silently picking one.
30
+ const castIdsConflictPolicy = opts.castIdsConflictPolicy ?? "throw";
31
+ for (const [name, collection] of Object.entries(opts.models)) {
32
+ const populates = opts.populates?.[name];
33
+ const model = new Model_js_1.Model({
34
+ name,
35
+ collection: opts.db.collection(collection),
36
+ db: opts.db,
37
+ populates: populates ?? {},
38
+ collectionToModelName,
39
+ getModelByName,
40
+ autoCastIds,
41
+ castIdsConflictPolicy,
42
+ });
43
+ registry.set(name, model);
44
+ }
45
+ return (name) => getModelByName(name);
46
+ };
47
+ exports.createGetModel = createGetModel;
@@ -0,0 +1,4 @@
1
+ export { createGetModel } from "./createGetModel.js";
2
+ export { CastIdsConflictError } from "./CastIdsConflictError.js";
3
+ export type { CreateGetModelOpts, GetModel, PopulateConfig, CastIdsConflictPolicy, } from "./types.js";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACjE,YAAY,EACV,kBAAkB,EAClB,QAAQ,EACR,cAAc,EACd,qBAAqB,GACtB,MAAM,YAAY,CAAC"}
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ // Public surface. Just createGetModel — no Schema, no Types, no
3
+ // connection management, no Model export. The caller wires up the
4
+ // MongoClient + Db themselves and hands us the Db.
5
+ //
6
+ // See README.md for the full design rationale.
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.CastIdsConflictError = exports.createGetModel = void 0;
9
+ var createGetModel_js_1 = require("./createGetModel.js");
10
+ Object.defineProperty(exports, "createGetModel", { enumerable: true, get: function () { return createGetModel_js_1.createGetModel; } });
11
+ var CastIdsConflictError_js_1 = require("./CastIdsConflictError.js");
12
+ Object.defineProperty(exports, "CastIdsConflictError", { enumerable: true, get: function () { return CastIdsConflictError_js_1.CastIdsConflictError; } });
@@ -0,0 +1,23 @@
1
+ import type { ClientSession } from "mongodb";
2
+ export type PopulateSpec = {
3
+ path: string;
4
+ select?: any;
5
+ match?: any;
6
+ populate?: PopulateSpec[];
7
+ };
8
+ type ModelLike = {
9
+ name: string;
10
+ populates: Record<string, {
11
+ collection: string;
12
+ }>;
13
+ collectionToModelName: Map<string, string>;
14
+ getModelByName: (name: string) => any;
15
+ };
16
+ type RunOpts = {
17
+ ownerModel: ModelLike;
18
+ session: ClientSession | undefined;
19
+ };
20
+ export declare const normalizePopulateArg: (arg: any) => PopulateSpec[];
21
+ export declare const runPopulate: (docs: any[], specs: PopulateSpec[], opts: RunOpts) => Promise<any[]>;
22
+ export {};
23
+ //# sourceMappingURL=populate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"populate.d.ts","sourceRoot":"","sources":["../../src/populate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAU7C,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,GAAG,CAAC;IACb,KAAK,CAAC,EAAE,GAAG,CAAC;IACZ,QAAQ,CAAC,EAAE,YAAY,EAAE,CAAC;CAC3B,CAAC;AAEF,KAAK,SAAS,GAAG;IACf,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAClD,qBAAqB,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3C,cAAc,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,GAAG,CAAC;CACvC,CAAC;AAEF,KAAK,OAAO,GAAG;IACb,UAAU,EAAE,SAAS,CAAC;IACtB,OAAO,EAAE,aAAa,GAAG,SAAS,CAAC;CACpC,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAAI,KAAK,GAAG,KAAG,YAAY,EAwB3D,CAAC;AAEF,eAAO,MAAM,WAAW,GACtB,MAAM,GAAG,EAAE,EACX,OAAO,YAAY,EAAE,EACrB,MAAM,OAAO,KACZ,OAAO,CAAC,GAAG,EAAE,CAKf,CAAC"}