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,180 @@
|
|
|
1
|
+
export const normalizePopulateArg = (arg) => {
|
|
2
|
+
if (!arg)
|
|
3
|
+
return [];
|
|
4
|
+
if (typeof arg === "string") {
|
|
5
|
+
// Mongoose accepts "a b c" → three populates.
|
|
6
|
+
return arg
|
|
7
|
+
.split(/\s+/)
|
|
8
|
+
.filter(Boolean)
|
|
9
|
+
.map((path) => ({ path }));
|
|
10
|
+
}
|
|
11
|
+
if (Array.isArray(arg)) {
|
|
12
|
+
return arg.flatMap(normalizePopulateArg);
|
|
13
|
+
}
|
|
14
|
+
if (typeof arg === "object") {
|
|
15
|
+
const { path, select, match, populate } = arg;
|
|
16
|
+
return [
|
|
17
|
+
{
|
|
18
|
+
path,
|
|
19
|
+
select,
|
|
20
|
+
match,
|
|
21
|
+
populate: populate ? normalizePopulateArg(populate) : undefined,
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
}
|
|
25
|
+
return [];
|
|
26
|
+
};
|
|
27
|
+
export const runPopulate = async (docs, specs, opts) => {
|
|
28
|
+
for (const spec of specs) {
|
|
29
|
+
await applySpec(docs, spec, opts);
|
|
30
|
+
}
|
|
31
|
+
return docs;
|
|
32
|
+
};
|
|
33
|
+
const applySpec = async (docs, spec, opts) => {
|
|
34
|
+
const cfg = opts.ownerModel.populates[spec.path];
|
|
35
|
+
if (!cfg) {
|
|
36
|
+
// No mapping configured for this path → nothing to do. Matches
|
|
37
|
+
// mongoose's behavior of silently skipping unknown refs.
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const refModelName = opts.ownerModel.collectionToModelName.get(cfg.collection);
|
|
41
|
+
if (!refModelName)
|
|
42
|
+
return;
|
|
43
|
+
const refModel = opts.ownerModel.getModelByName(refModelName);
|
|
44
|
+
const isArrayPath = spec.path.includes(".");
|
|
45
|
+
if (isArrayPath) {
|
|
46
|
+
await populateArrayPath(docs, spec, refModel, opts);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
await populateScalarPath(docs, spec, refModel, opts);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const populateScalarPath = async (docs, spec, refModel, opts) => {
|
|
53
|
+
const ids = uniqueIds(docs.map((d) => d?.[spec.path]).filter(isPresent));
|
|
54
|
+
if (ids.length === 0)
|
|
55
|
+
return;
|
|
56
|
+
const filter = { _id: { $in: ids } };
|
|
57
|
+
if (spec.match)
|
|
58
|
+
Object.assign(filter, spec.match);
|
|
59
|
+
const findOpts = {};
|
|
60
|
+
if (spec.select)
|
|
61
|
+
findOpts.projection = normalizeProjection(spec.select);
|
|
62
|
+
if (opts.session)
|
|
63
|
+
findOpts.session = opts.session;
|
|
64
|
+
const fetched = await refModel.collection.find(filter, findOpts).toArray();
|
|
65
|
+
if (spec.populate) {
|
|
66
|
+
await runPopulate(fetched, spec.populate, {
|
|
67
|
+
ownerModel: refModel,
|
|
68
|
+
session: opts.session,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
const byId = new Map();
|
|
72
|
+
for (const f of fetched)
|
|
73
|
+
byId.set(idKey(f._id), f);
|
|
74
|
+
// Mongoose semantics: if a match filter is present, refs that don't
|
|
75
|
+
// satisfy it get nulled out (the parent doc still exists, but the
|
|
76
|
+
// populated field becomes null). Without a match, leave non-matches
|
|
77
|
+
// alone (the raw ref id stays, matching mongoose's behavior when a
|
|
78
|
+
// referenced doc has been deleted).
|
|
79
|
+
const nullifyMisses = !!spec.match;
|
|
80
|
+
for (const d of docs) {
|
|
81
|
+
const raw = d?.[spec.path];
|
|
82
|
+
if (!isPresent(raw))
|
|
83
|
+
continue;
|
|
84
|
+
const hit = byId.get(idKey(raw));
|
|
85
|
+
if (hit)
|
|
86
|
+
d[spec.path] = hit;
|
|
87
|
+
else if (nullifyMisses)
|
|
88
|
+
d[spec.path] = null;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
// "tags._id" — parent has an array of subdocs, each with an _id
|
|
92
|
+
// field that's a ref. After populate, each subdoc's _id is replaced
|
|
93
|
+
// with the full referenced doc.
|
|
94
|
+
const populateArrayPath = async (docs, spec, refModel, opts) => {
|
|
95
|
+
const [parent, child] = spec.path.split(".");
|
|
96
|
+
const allIds = [];
|
|
97
|
+
for (const d of docs) {
|
|
98
|
+
const arr = d?.[parent];
|
|
99
|
+
if (!Array.isArray(arr))
|
|
100
|
+
continue;
|
|
101
|
+
for (const sub of arr) {
|
|
102
|
+
const v = sub?.[child];
|
|
103
|
+
if (isPresent(v))
|
|
104
|
+
allIds.push(v);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const ids = uniqueIds(allIds);
|
|
108
|
+
if (ids.length === 0)
|
|
109
|
+
return;
|
|
110
|
+
const filter = { _id: { $in: ids } };
|
|
111
|
+
if (spec.match)
|
|
112
|
+
Object.assign(filter, spec.match);
|
|
113
|
+
const findOpts = {};
|
|
114
|
+
if (spec.select)
|
|
115
|
+
findOpts.projection = normalizeProjection(spec.select);
|
|
116
|
+
if (opts.session)
|
|
117
|
+
findOpts.session = opts.session;
|
|
118
|
+
const fetched = await refModel.collection.find(filter, findOpts).toArray();
|
|
119
|
+
if (spec.populate) {
|
|
120
|
+
await runPopulate(fetched, spec.populate, {
|
|
121
|
+
ownerModel: refModel,
|
|
122
|
+
session: opts.session,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
const byId = new Map();
|
|
126
|
+
for (const f of fetched)
|
|
127
|
+
byId.set(idKey(f._id), f);
|
|
128
|
+
const nullifyMisses = !!spec.match;
|
|
129
|
+
for (const d of docs) {
|
|
130
|
+
const arr = d?.[parent];
|
|
131
|
+
if (!Array.isArray(arr))
|
|
132
|
+
continue;
|
|
133
|
+
for (const sub of arr) {
|
|
134
|
+
const raw = sub?.[child];
|
|
135
|
+
if (!isPresent(raw))
|
|
136
|
+
continue;
|
|
137
|
+
const hit = byId.get(idKey(raw));
|
|
138
|
+
if (hit)
|
|
139
|
+
sub[child] = hit;
|
|
140
|
+
else if (nullifyMisses)
|
|
141
|
+
sub[child] = null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
const isPresent = (v) => v !== null && v !== undefined;
|
|
146
|
+
// ObjectId values aren't === equal even when they represent the same
|
|
147
|
+
// 24 hex chars, so key by their string form.
|
|
148
|
+
const idKey = (v) => {
|
|
149
|
+
if (v == null)
|
|
150
|
+
return "";
|
|
151
|
+
if (typeof v === "string")
|
|
152
|
+
return v;
|
|
153
|
+
if (typeof v.toHexString === "function")
|
|
154
|
+
return v.toHexString();
|
|
155
|
+
return String(v);
|
|
156
|
+
};
|
|
157
|
+
const uniqueIds = (ids) => {
|
|
158
|
+
const seen = new Set();
|
|
159
|
+
const out = [];
|
|
160
|
+
for (const id of ids) {
|
|
161
|
+
const k = idKey(id);
|
|
162
|
+
if (seen.has(k))
|
|
163
|
+
continue;
|
|
164
|
+
seen.add(k);
|
|
165
|
+
out.push(id);
|
|
166
|
+
}
|
|
167
|
+
return out;
|
|
168
|
+
};
|
|
169
|
+
const normalizeProjection = (sel) => {
|
|
170
|
+
if (typeof sel !== "string")
|
|
171
|
+
return sel;
|
|
172
|
+
const out = {};
|
|
173
|
+
for (const tok of sel.split(/\s+/).filter(Boolean)) {
|
|
174
|
+
if (tok.startsWith("-"))
|
|
175
|
+
out[tok.slice(1)] = 0;
|
|
176
|
+
else
|
|
177
|
+
out[tok] = 1;
|
|
178
|
+
}
|
|
179
|
+
return out;
|
|
180
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mongoose-killer",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "In a fight of Mongoose vs. Viper, only 1 in 10 Vipers come out on top. This is the 10th Viper. Viper is a drop-in Mongoose replacement built for speed.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Ben Thayer",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"mongodb",
|
|
9
|
+
"mongoose",
|
|
10
|
+
"odm",
|
|
11
|
+
"driver",
|
|
12
|
+
"schema"
|
|
13
|
+
],
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/benthayer/viper.git"
|
|
17
|
+
},
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/benthayer/viper/issues"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/benthayer/viper#readme",
|
|
22
|
+
"main": "./dist/cjs/index.js",
|
|
23
|
+
"module": "./dist/esm/index.js",
|
|
24
|
+
"types": "./dist/cjs/index.d.ts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/cjs/index.d.ts",
|
|
28
|
+
"import": "./dist/esm/index.js",
|
|
29
|
+
"require": "./dist/cjs/index.js",
|
|
30
|
+
"default": "./dist/cjs/index.js"
|
|
31
|
+
},
|
|
32
|
+
"./package.json": "./package.json"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"README.md",
|
|
37
|
+
"LICENSE"
|
|
38
|
+
],
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=18"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "rm -rf dist && yarn build:cjs && yarn build:esm && yarn build:esm:pkgjson",
|
|
47
|
+
"build:cjs": "tsc -p tsconfig.cjs.json",
|
|
48
|
+
"build:esm": "tsc -p tsconfig.esm.json",
|
|
49
|
+
"build:esm:pkgjson": "node -e \"require('fs').writeFileSync('dist/esm/package.json', JSON.stringify({type:'module'}) + '\\n')\"",
|
|
50
|
+
"prepare": "yarn build",
|
|
51
|
+
"prepublishOnly": "yarn build && yarn test",
|
|
52
|
+
"test": "vitest run",
|
|
53
|
+
"test:watch": "vitest",
|
|
54
|
+
"test:real": "MONGOOSE_BACKEND=real vitest run",
|
|
55
|
+
"test:fake": "MONGOOSE_BACKEND=fake vitest run",
|
|
56
|
+
"test:both": "MONGOOSE_BACKEND=real vitest run && MONGOOSE_BACKEND=fake vitest run",
|
|
57
|
+
"typecheck": "tsc --noEmit"
|
|
58
|
+
},
|
|
59
|
+
"peerDependencies": {
|
|
60
|
+
"mongodb": "^6.0.0"
|
|
61
|
+
},
|
|
62
|
+
"dependencies": {
|
|
63
|
+
"bson": "^6.8.0"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"@types/node": "^22.0.0",
|
|
67
|
+
"mongodb": "^6.9.0",
|
|
68
|
+
"mongoose": "^8.7.0",
|
|
69
|
+
"tsx": "^4.19.0",
|
|
70
|
+
"typescript": "^5.5.0",
|
|
71
|
+
"vitest": "^2.1.0"
|
|
72
|
+
},
|
|
73
|
+
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
|
74
|
+
}
|