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.
- package/LICENSE +21 -0
- package/README.md +198 -0
- package/dist/cjs/CastIdsConflictError.d.ts +11 -0
- package/dist/cjs/CastIdsConflictError.d.ts.map +1 -0
- package/dist/cjs/CastIdsConflictError.js +27 -0
- package/dist/cjs/Model.d.ts +51 -0
- package/dist/cjs/Model.d.ts.map +1 -0
- package/dist/cjs/Model.js +205 -0
- package/dist/cjs/Query.d.ts +85 -0
- package/dist/cjs/Query.d.ts.map +1 -0
- package/dist/cjs/Query.js +439 -0
- package/dist/cjs/castFilter.d.ts +25 -0
- package/dist/cjs/castFilter.d.ts.map +1 -0
- package/dist/cjs/castFilter.js +100 -0
- package/dist/cjs/createGetModel.d.ts +3 -0
- package/dist/cjs/createGetModel.d.ts.map +1 -0
- package/dist/cjs/createGetModel.js +47 -0
- package/dist/cjs/index.d.ts +4 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +12 -0
- package/dist/cjs/populate.d.ts +23 -0
- package/dist/cjs/populate.d.ts.map +1 -0
- package/dist/cjs/populate.js +185 -0
- package/dist/cjs/types.d.ts +14 -0
- package/dist/cjs/types.d.ts.map +1 -0
- package/dist/cjs/types.js +2 -0
- package/dist/esm/CastIdsConflictError.js +23 -0
- package/dist/esm/Model.js +201 -0
- package/dist/esm/Query.js +434 -0
- package/dist/esm/castFilter.js +96 -0
- package/dist/esm/createGetModel.js +43 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/populate.js +180 -0
- package/dist/esm/types.js +1 -0
- package/package.json +74 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runPopulate = exports.normalizePopulateArg = void 0;
|
|
4
|
+
const normalizePopulateArg = (arg) => {
|
|
5
|
+
if (!arg)
|
|
6
|
+
return [];
|
|
7
|
+
if (typeof arg === "string") {
|
|
8
|
+
// Mongoose accepts "a b c" → three populates.
|
|
9
|
+
return arg
|
|
10
|
+
.split(/\s+/)
|
|
11
|
+
.filter(Boolean)
|
|
12
|
+
.map((path) => ({ path }));
|
|
13
|
+
}
|
|
14
|
+
if (Array.isArray(arg)) {
|
|
15
|
+
return arg.flatMap(exports.normalizePopulateArg);
|
|
16
|
+
}
|
|
17
|
+
if (typeof arg === "object") {
|
|
18
|
+
const { path, select, match, populate } = arg;
|
|
19
|
+
return [
|
|
20
|
+
{
|
|
21
|
+
path,
|
|
22
|
+
select,
|
|
23
|
+
match,
|
|
24
|
+
populate: populate ? (0, exports.normalizePopulateArg)(populate) : undefined,
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
return [];
|
|
29
|
+
};
|
|
30
|
+
exports.normalizePopulateArg = normalizePopulateArg;
|
|
31
|
+
const runPopulate = async (docs, specs, opts) => {
|
|
32
|
+
for (const spec of specs) {
|
|
33
|
+
await applySpec(docs, spec, opts);
|
|
34
|
+
}
|
|
35
|
+
return docs;
|
|
36
|
+
};
|
|
37
|
+
exports.runPopulate = runPopulate;
|
|
38
|
+
const applySpec = async (docs, spec, opts) => {
|
|
39
|
+
const cfg = opts.ownerModel.populates[spec.path];
|
|
40
|
+
if (!cfg) {
|
|
41
|
+
// No mapping configured for this path → nothing to do. Matches
|
|
42
|
+
// mongoose's behavior of silently skipping unknown refs.
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const refModelName = opts.ownerModel.collectionToModelName.get(cfg.collection);
|
|
46
|
+
if (!refModelName)
|
|
47
|
+
return;
|
|
48
|
+
const refModel = opts.ownerModel.getModelByName(refModelName);
|
|
49
|
+
const isArrayPath = spec.path.includes(".");
|
|
50
|
+
if (isArrayPath) {
|
|
51
|
+
await populateArrayPath(docs, spec, refModel, opts);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
await populateScalarPath(docs, spec, refModel, opts);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const populateScalarPath = async (docs, spec, refModel, opts) => {
|
|
58
|
+
const ids = uniqueIds(docs.map((d) => d?.[spec.path]).filter(isPresent));
|
|
59
|
+
if (ids.length === 0)
|
|
60
|
+
return;
|
|
61
|
+
const filter = { _id: { $in: ids } };
|
|
62
|
+
if (spec.match)
|
|
63
|
+
Object.assign(filter, spec.match);
|
|
64
|
+
const findOpts = {};
|
|
65
|
+
if (spec.select)
|
|
66
|
+
findOpts.projection = normalizeProjection(spec.select);
|
|
67
|
+
if (opts.session)
|
|
68
|
+
findOpts.session = opts.session;
|
|
69
|
+
const fetched = await refModel.collection.find(filter, findOpts).toArray();
|
|
70
|
+
if (spec.populate) {
|
|
71
|
+
await (0, exports.runPopulate)(fetched, spec.populate, {
|
|
72
|
+
ownerModel: refModel,
|
|
73
|
+
session: opts.session,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
const byId = new Map();
|
|
77
|
+
for (const f of fetched)
|
|
78
|
+
byId.set(idKey(f._id), f);
|
|
79
|
+
// Mongoose semantics: if a match filter is present, refs that don't
|
|
80
|
+
// satisfy it get nulled out (the parent doc still exists, but the
|
|
81
|
+
// populated field becomes null). Without a match, leave non-matches
|
|
82
|
+
// alone (the raw ref id stays, matching mongoose's behavior when a
|
|
83
|
+
// referenced doc has been deleted).
|
|
84
|
+
const nullifyMisses = !!spec.match;
|
|
85
|
+
for (const d of docs) {
|
|
86
|
+
const raw = d?.[spec.path];
|
|
87
|
+
if (!isPresent(raw))
|
|
88
|
+
continue;
|
|
89
|
+
const hit = byId.get(idKey(raw));
|
|
90
|
+
if (hit)
|
|
91
|
+
d[spec.path] = hit;
|
|
92
|
+
else if (nullifyMisses)
|
|
93
|
+
d[spec.path] = null;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
// "tags._id" — parent has an array of subdocs, each with an _id
|
|
97
|
+
// field that's a ref. After populate, each subdoc's _id is replaced
|
|
98
|
+
// with the full referenced doc.
|
|
99
|
+
const populateArrayPath = async (docs, spec, refModel, opts) => {
|
|
100
|
+
const [parent, child] = spec.path.split(".");
|
|
101
|
+
const allIds = [];
|
|
102
|
+
for (const d of docs) {
|
|
103
|
+
const arr = d?.[parent];
|
|
104
|
+
if (!Array.isArray(arr))
|
|
105
|
+
continue;
|
|
106
|
+
for (const sub of arr) {
|
|
107
|
+
const v = sub?.[child];
|
|
108
|
+
if (isPresent(v))
|
|
109
|
+
allIds.push(v);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const ids = uniqueIds(allIds);
|
|
113
|
+
if (ids.length === 0)
|
|
114
|
+
return;
|
|
115
|
+
const filter = { _id: { $in: ids } };
|
|
116
|
+
if (spec.match)
|
|
117
|
+
Object.assign(filter, spec.match);
|
|
118
|
+
const findOpts = {};
|
|
119
|
+
if (spec.select)
|
|
120
|
+
findOpts.projection = normalizeProjection(spec.select);
|
|
121
|
+
if (opts.session)
|
|
122
|
+
findOpts.session = opts.session;
|
|
123
|
+
const fetched = await refModel.collection.find(filter, findOpts).toArray();
|
|
124
|
+
if (spec.populate) {
|
|
125
|
+
await (0, exports.runPopulate)(fetched, spec.populate, {
|
|
126
|
+
ownerModel: refModel,
|
|
127
|
+
session: opts.session,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
const byId = new Map();
|
|
131
|
+
for (const f of fetched)
|
|
132
|
+
byId.set(idKey(f._id), f);
|
|
133
|
+
const nullifyMisses = !!spec.match;
|
|
134
|
+
for (const d of docs) {
|
|
135
|
+
const arr = d?.[parent];
|
|
136
|
+
if (!Array.isArray(arr))
|
|
137
|
+
continue;
|
|
138
|
+
for (const sub of arr) {
|
|
139
|
+
const raw = sub?.[child];
|
|
140
|
+
if (!isPresent(raw))
|
|
141
|
+
continue;
|
|
142
|
+
const hit = byId.get(idKey(raw));
|
|
143
|
+
if (hit)
|
|
144
|
+
sub[child] = hit;
|
|
145
|
+
else if (nullifyMisses)
|
|
146
|
+
sub[child] = null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
const isPresent = (v) => v !== null && v !== undefined;
|
|
151
|
+
// ObjectId values aren't === equal even when they represent the same
|
|
152
|
+
// 24 hex chars, so key by their string form.
|
|
153
|
+
const idKey = (v) => {
|
|
154
|
+
if (v == null)
|
|
155
|
+
return "";
|
|
156
|
+
if (typeof v === "string")
|
|
157
|
+
return v;
|
|
158
|
+
if (typeof v.toHexString === "function")
|
|
159
|
+
return v.toHexString();
|
|
160
|
+
return String(v);
|
|
161
|
+
};
|
|
162
|
+
const uniqueIds = (ids) => {
|
|
163
|
+
const seen = new Set();
|
|
164
|
+
const out = [];
|
|
165
|
+
for (const id of ids) {
|
|
166
|
+
const k = idKey(id);
|
|
167
|
+
if (seen.has(k))
|
|
168
|
+
continue;
|
|
169
|
+
seen.add(k);
|
|
170
|
+
out.push(id);
|
|
171
|
+
}
|
|
172
|
+
return out;
|
|
173
|
+
};
|
|
174
|
+
const normalizeProjection = (sel) => {
|
|
175
|
+
if (typeof sel !== "string")
|
|
176
|
+
return sel;
|
|
177
|
+
const out = {};
|
|
178
|
+
for (const tok of sel.split(/\s+/).filter(Boolean)) {
|
|
179
|
+
if (tok.startsWith("-"))
|
|
180
|
+
out[tok.slice(1)] = 0;
|
|
181
|
+
else
|
|
182
|
+
out[tok] = 1;
|
|
183
|
+
}
|
|
184
|
+
return out;
|
|
185
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Db } from "mongodb";
|
|
2
|
+
export type PopulateConfig = Record<string, Record<string, {
|
|
3
|
+
collection: string;
|
|
4
|
+
}>>;
|
|
5
|
+
export type CastIdsConflictPolicy = "throw" | "firstWins" | "lastWins" | "defaultWins";
|
|
6
|
+
export type CreateGetModelOpts = {
|
|
7
|
+
db: Db;
|
|
8
|
+
models: Record<string, string>;
|
|
9
|
+
populates?: PopulateConfig;
|
|
10
|
+
autoCastIds?: boolean;
|
|
11
|
+
castIdsConflictPolicy?: CastIdsConflictPolicy;
|
|
12
|
+
};
|
|
13
|
+
export type GetModel = (name: string) => any;
|
|
14
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAElC,MAAM,MAAM,cAAc,GAAG,MAAM,CACjC,MAAM,EACN,MAAM,CAAC,MAAM,EAAE;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,CACvC,CAAC;AAUF,MAAM,MAAM,qBAAqB,GAC7B,OAAO,GACP,WAAW,GACX,UAAU,GACV,aAAa,CAAC;AAElB,MAAM,MAAM,kBAAkB,GAAG;IAC/B,EAAE,EAAE,EAAE,CAAC;IACP,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,SAAS,CAAC,EAAE,cAAc,CAAC;IAS3B,WAAW,CAAC,EAAE,OAAO,CAAC;IAGtB,qBAAqB,CAAC,EAAE,qBAAqB,CAAC;CAC/C,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,GAAG,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Thrown at query exec time when a single Query has had both
|
|
2
|
+
// `.castIds()` and `.skipCastIds()` called on it AND the configured
|
|
3
|
+
// conflict policy is "throw".
|
|
4
|
+
//
|
|
5
|
+
// Exported as a named class so callers can do `instanceof` in error
|
|
6
|
+
// handlers. The static `is(err)` helper is here for callers that
|
|
7
|
+
// receive errors across module-boundaries (different bundles can
|
|
8
|
+
// produce instanceof-incompatible classes with the same name).
|
|
9
|
+
export class CastIdsConflictError extends Error {
|
|
10
|
+
name = "CastIdsConflictError";
|
|
11
|
+
castIdsCallCount;
|
|
12
|
+
skipCastIdsCallCount;
|
|
13
|
+
constructor(opts) {
|
|
14
|
+
super(`Conflicting calls on the same Query: ${opts.castIdsCallCount}× .castIds() and ${opts.skipCastIdsCallCount}× .skipCastIds(). ` +
|
|
15
|
+
`The configured castIdsConflictPolicy is "throw" — pass createGetModel({ castIdsConflictPolicy: "lastWins" | "firstWins" | "defaultWins" }) to resolve conflicts automatically, or remove one of the calls.`);
|
|
16
|
+
this.castIdsCallCount = opts.castIdsCallCount;
|
|
17
|
+
this.skipCastIdsCallCount = opts.skipCastIdsCallCount;
|
|
18
|
+
}
|
|
19
|
+
static is(err) {
|
|
20
|
+
return (err instanceof Error &&
|
|
21
|
+
err.name === "CastIdsConflictError");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { ObjectId } from "bson";
|
|
2
|
+
import { Query } from "./Query.js";
|
|
3
|
+
import { runPopulate, normalizePopulateArg } from "./populate.js";
|
|
4
|
+
// Mongoose `Model` is half class, half magic. We mimic the surface the
|
|
5
|
+
// codebase actually uses (per inventory): the static entry verbs.
|
|
6
|
+
// Instances are not a thing — call sites never `new Model()`.
|
|
7
|
+
export class Model {
|
|
8
|
+
name;
|
|
9
|
+
collection;
|
|
10
|
+
populates;
|
|
11
|
+
collectionToModelName;
|
|
12
|
+
getModelByName;
|
|
13
|
+
autoCastIds;
|
|
14
|
+
castIdsConflictPolicy;
|
|
15
|
+
// Mongoose exposes Model.db as a Connection-like object, and
|
|
16
|
+
// Connection.db is the native Db handle. Some libraries reach
|
|
17
|
+
// through `Model.db.db` to grab the native Db and issue a raw
|
|
18
|
+
// `db.collection(name).findOne({})` — bypassing the ODM (often
|
|
19
|
+
// to avoid lossy type handling like mongoose's historical
|
|
20
|
+
// Decimal128 quirks).
|
|
21
|
+
//
|
|
22
|
+
// We mimic that two-hop shape: `model.db` is a wrapper object whose
|
|
23
|
+
// `.db` is the native Db. Anything else hanging off Connection is
|
|
24
|
+
// not currently used by any call site, so we leave it unset.
|
|
25
|
+
db;
|
|
26
|
+
constructor(init) {
|
|
27
|
+
this.name = init.name;
|
|
28
|
+
this.collection = init.collection;
|
|
29
|
+
this.populates = init.populates;
|
|
30
|
+
this.collectionToModelName = init.collectionToModelName;
|
|
31
|
+
this.getModelByName = init.getModelByName;
|
|
32
|
+
this.autoCastIds = init.autoCastIds;
|
|
33
|
+
this.castIdsConflictPolicy = init.castIdsConflictPolicy;
|
|
34
|
+
this.db = { db: init.db };
|
|
35
|
+
}
|
|
36
|
+
// ---- query entry verbs --------------------------------------------
|
|
37
|
+
find(filter = {}, projection, options) {
|
|
38
|
+
const q = new Query({ model: this, op: "find", filter, options });
|
|
39
|
+
if (projection)
|
|
40
|
+
q.select(projection);
|
|
41
|
+
if (options?.sort)
|
|
42
|
+
q.sort(options.sort);
|
|
43
|
+
if (options?.limit !== undefined)
|
|
44
|
+
q.limit(options.limit);
|
|
45
|
+
if (options?.skip !== undefined)
|
|
46
|
+
q.skip(options.skip);
|
|
47
|
+
return q;
|
|
48
|
+
}
|
|
49
|
+
findOne(filter = {}, projection, options) {
|
|
50
|
+
const q = new Query({ model: this, op: "findOne", filter, options });
|
|
51
|
+
if (projection)
|
|
52
|
+
q.select(projection);
|
|
53
|
+
return q;
|
|
54
|
+
}
|
|
55
|
+
findById(id, projection, options) {
|
|
56
|
+
return this.findOne({ _id: coerceId(id) }, projection, options);
|
|
57
|
+
}
|
|
58
|
+
findOneAndUpdate(filter, update, options) {
|
|
59
|
+
return new Query({
|
|
60
|
+
model: this,
|
|
61
|
+
op: "findOneAndUpdate",
|
|
62
|
+
filter,
|
|
63
|
+
update,
|
|
64
|
+
options,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
findByIdAndUpdate(id, update, options) {
|
|
68
|
+
return this.findOneAndUpdate({ _id: coerceId(id) }, update, options);
|
|
69
|
+
}
|
|
70
|
+
findOneAndDelete(filter, options) {
|
|
71
|
+
return new Query({
|
|
72
|
+
model: this,
|
|
73
|
+
op: "findOneAndDelete",
|
|
74
|
+
filter,
|
|
75
|
+
options,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
findByIdAndDelete(id, options) {
|
|
79
|
+
return this.findOneAndDelete({ _id: coerceId(id) }, options);
|
|
80
|
+
}
|
|
81
|
+
updateOne(filter, update, options) {
|
|
82
|
+
return new Query({
|
|
83
|
+
model: this,
|
|
84
|
+
op: "updateOne",
|
|
85
|
+
filter,
|
|
86
|
+
update,
|
|
87
|
+
options,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
updateMany(filter, update, options) {
|
|
91
|
+
return new Query({
|
|
92
|
+
model: this,
|
|
93
|
+
op: "updateMany",
|
|
94
|
+
filter,
|
|
95
|
+
update,
|
|
96
|
+
options,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
deleteOne(filter, options) {
|
|
100
|
+
return new Query({
|
|
101
|
+
model: this,
|
|
102
|
+
op: "deleteOne",
|
|
103
|
+
filter,
|
|
104
|
+
options,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
deleteMany(filter, options) {
|
|
108
|
+
return new Query({
|
|
109
|
+
model: this,
|
|
110
|
+
op: "deleteMany",
|
|
111
|
+
filter,
|
|
112
|
+
options,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
countDocuments(filter = {}, options) {
|
|
116
|
+
return new Query({
|
|
117
|
+
model: this,
|
|
118
|
+
op: "countDocuments",
|
|
119
|
+
filter,
|
|
120
|
+
options,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
distinct(field, filter = {}, options) {
|
|
124
|
+
return new Query({
|
|
125
|
+
model: this,
|
|
126
|
+
op: "distinct",
|
|
127
|
+
filter,
|
|
128
|
+
distinctField: field,
|
|
129
|
+
options,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
exists(filter, options) {
|
|
133
|
+
return new Query({
|
|
134
|
+
model: this,
|
|
135
|
+
op: "exists",
|
|
136
|
+
filter,
|
|
137
|
+
options,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
aggregate(pipeline = [], options) {
|
|
141
|
+
return new Query({
|
|
142
|
+
model: this,
|
|
143
|
+
op: "aggregate",
|
|
144
|
+
pipeline,
|
|
145
|
+
options,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
// ---- writes that aren't chains ------------------------------------
|
|
149
|
+
// Mongoose's create returns the inserted doc(s) with _id set.
|
|
150
|
+
// Inventory uses both forms: create(doc) and create([docs], options).
|
|
151
|
+
async create(input, options) {
|
|
152
|
+
if (Array.isArray(input)) {
|
|
153
|
+
const docs = input.map(prepInsert);
|
|
154
|
+
if (docs.length === 0)
|
|
155
|
+
return [];
|
|
156
|
+
await this.collection.insertMany(docs, options ?? {});
|
|
157
|
+
return docs;
|
|
158
|
+
}
|
|
159
|
+
const doc = prepInsert(input);
|
|
160
|
+
await this.collection.insertOne(doc, options ?? {});
|
|
161
|
+
return doc;
|
|
162
|
+
}
|
|
163
|
+
async insertMany(docs, options) {
|
|
164
|
+
const prepared = docs.map(prepInsert);
|
|
165
|
+
if (prepared.length === 0)
|
|
166
|
+
return [];
|
|
167
|
+
await this.collection.insertMany(prepared, options ?? {});
|
|
168
|
+
return prepared;
|
|
169
|
+
}
|
|
170
|
+
async bulkWrite(ops, options) {
|
|
171
|
+
return this.collection.bulkWrite(ops, options ?? {});
|
|
172
|
+
}
|
|
173
|
+
// Static populate: takes pre-fetched doc(s) and decorates them.
|
|
174
|
+
// Used in a handful of inventory sites for ad-hoc populate after
|
|
175
|
+
// a custom aggregation.
|
|
176
|
+
async populate(docs, arg) {
|
|
177
|
+
const isArr = Array.isArray(docs);
|
|
178
|
+
const arr = isArr ? docs : [docs];
|
|
179
|
+
if (arr.length === 0)
|
|
180
|
+
return docs;
|
|
181
|
+
const specs = normalizePopulateArg(arg);
|
|
182
|
+
const populated = await runPopulate(arr, specs, {
|
|
183
|
+
ownerModel: this,
|
|
184
|
+
session: undefined,
|
|
185
|
+
});
|
|
186
|
+
return isArr ? populated : populated[0];
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// Mongoose auto-assigns _id on insert; the driver does the same, but
|
|
190
|
+
// only if _id is missing. We let the driver handle it — no work here.
|
|
191
|
+
// Kept as a hook in case we ever need to deep-clone or strip Mongoose-
|
|
192
|
+
// only fields.
|
|
193
|
+
const prepInsert = (doc) => doc;
|
|
194
|
+
const coerceId = (id) => {
|
|
195
|
+
if (id instanceof ObjectId)
|
|
196
|
+
return id;
|
|
197
|
+
if (typeof id === "string" && /^[0-9a-fA-F]{24}$/.test(id)) {
|
|
198
|
+
return new ObjectId(id);
|
|
199
|
+
}
|
|
200
|
+
return id;
|
|
201
|
+
};
|