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,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,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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
+ };