tangled
alpha
login
or
join now
knotbin.com
/
nozzle
0
fork
atom
Thin MongoDB ODM built for Standard Schema
mongodb
zod
deno
0
fork
atom
overview
issues
pulls
pipelines
add update validation
knotbin.com
3 months ago
85074949
f64e27b8
verified
This commit was signed with the committer's
known signature
.
knotbin.com
SSH Key Fingerprint:
SHA256:cz+cxLxCL/B8cV6riZjeEPSqiRA5+YAQM9XfjxPWTWE=
+656
-923
10 changed files
expand all
collapse all
unified
split
deno.json
deno.lock
model.ts
scripts
test.ts
tests
crud_test.ts
features_test.ts
main_test.ts
mock_test.ts
utils.ts
validation_test.ts
+4
-5
deno.json
···
3
3
"version": "0.1.0",
4
4
"exports": "./mod.ts",
5
5
"license": "MIT",
6
6
-
"tasks": {
7
7
-
"test:mock": "deno test tests/mock_test.ts",
8
8
-
"test:watch": "deno run -A scripts/test.ts --mock --watch"
9
9
-
},
10
6
"imports": {
11
7
"@standard-schema/spec": "jsr:@standard-schema/spec@^1.0.0",
12
12
-
"mongodb": "npm:mongodb@^6.18.0"
8
8
+
"@std/assert": "jsr:@std/assert@^1.0.16",
9
9
+
"@zod/zod": "jsr:@zod/zod@^4.1.13",
10
10
+
"mongodb": "npm:mongodb@^6.18.0",
11
11
+
"mongodb-memory-server-core": "npm:mongodb-memory-server-core@^10.3.0"
13
12
}
14
13
}
+189
deno.lock
···
4
4
"jsr:@standard-schema/spec@1": "1.0.0",
5
5
"jsr:@std/assert@*": "1.0.13",
6
6
"jsr:@std/assert@^1.0.13": "1.0.13",
7
7
+
"jsr:@std/assert@^1.0.16": "1.0.16",
7
8
"jsr:@std/internal@^1.0.10": "1.0.10",
9
9
+
"jsr:@std/internal@^1.0.12": "1.0.12",
8
10
"jsr:@std/internal@^1.0.6": "1.0.10",
9
11
"jsr:@std/testing@*": "1.0.15",
12
12
+
"jsr:@zod/zod@*": "4.1.13",
13
13
+
"jsr:@zod/zod@^4.1.13": "4.1.13",
10
14
"npm:@types/node@*": "22.15.15",
15
15
+
"npm:mongodb-memory-server-core@^10.3.0": "10.3.0",
11
16
"npm:mongodb@^6.18.0": "6.18.0"
12
17
},
13
18
"jsr": {
···
20
25
"jsr:@std/internal@^1.0.6"
21
26
]
22
27
},
28
28
+
"@std/assert@1.0.16": {
29
29
+
"integrity": "6a7272ed1eaa77defe76e5ff63ca705d9c495077e2d5fd0126d2b53fc5bd6532",
30
30
+
"dependencies": [
31
31
+
"jsr:@std/internal@^1.0.12"
32
32
+
]
33
33
+
},
23
34
"@std/internal@1.0.10": {
24
35
"integrity": "e3be62ce42cab0e177c27698e5d9800122f67b766a0bea6ca4867886cbde8cf7"
25
36
},
37
37
+
"@std/internal@1.0.12": {
38
38
+
"integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027"
39
39
+
},
26
40
"@std/testing@1.0.15": {
27
41
"integrity": "a490169f5ccb0f3ae9c94fbc69d2cd43603f2cffb41713a85f99bbb0e3087cbc",
28
42
"dependencies": [
29
43
"jsr:@std/assert@^1.0.13",
30
44
"jsr:@std/internal@^1.0.10"
31
45
]
46
46
+
},
47
47
+
"@zod/zod@4.1.13": {
48
48
+
"integrity": "fef799152d630583b248645fcac03abedd13e39fd2b752d9466b905d73619bfd"
32
49
}
33
50
},
34
51
"npm": {
···
53
70
"@types/webidl-conversions"
54
71
]
55
72
},
73
73
+
"agent-base@7.1.4": {
74
74
+
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="
75
75
+
},
76
76
+
"async-mutex@0.5.0": {
77
77
+
"integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==",
78
78
+
"dependencies": [
79
79
+
"tslib"
80
80
+
]
81
81
+
},
82
82
+
"b4a@1.7.3": {
83
83
+
"integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q=="
84
84
+
},
85
85
+
"bare-events@2.8.2": {
86
86
+
"integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ=="
87
87
+
},
56
88
"bson@6.10.4": {
57
89
"integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng=="
58
90
},
91
91
+
"buffer-crc32@0.2.13": {
92
92
+
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="
93
93
+
},
94
94
+
"camelcase@6.3.0": {
95
95
+
"integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="
96
96
+
},
97
97
+
"commondir@1.0.1": {
98
98
+
"integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="
99
99
+
},
100
100
+
"debug@4.4.3": {
101
101
+
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
102
102
+
"dependencies": [
103
103
+
"ms"
104
104
+
]
105
105
+
},
106
106
+
"events-universal@1.0.1": {
107
107
+
"integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
108
108
+
"dependencies": [
109
109
+
"bare-events"
110
110
+
]
111
111
+
},
112
112
+
"fast-fifo@1.3.2": {
113
113
+
"integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="
114
114
+
},
115
115
+
"find-cache-dir@3.3.2": {
116
116
+
"integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==",
117
117
+
"dependencies": [
118
118
+
"commondir",
119
119
+
"make-dir",
120
120
+
"pkg-dir"
121
121
+
]
122
122
+
},
123
123
+
"find-up@4.1.0": {
124
124
+
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
125
125
+
"dependencies": [
126
126
+
"locate-path",
127
127
+
"path-exists"
128
128
+
]
129
129
+
},
130
130
+
"follow-redirects@1.15.11": {
131
131
+
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="
132
132
+
},
133
133
+
"https-proxy-agent@7.0.6": {
134
134
+
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
135
135
+
"dependencies": [
136
136
+
"agent-base",
137
137
+
"debug"
138
138
+
]
139
139
+
},
140
140
+
"locate-path@5.0.0": {
141
141
+
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
142
142
+
"dependencies": [
143
143
+
"p-locate"
144
144
+
]
145
145
+
},
146
146
+
"make-dir@3.1.0": {
147
147
+
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
148
148
+
"dependencies": [
149
149
+
"semver@6.3.1"
150
150
+
]
151
151
+
},
59
152
"memory-pager@1.5.0": {
60
153
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="
61
154
},
···
66
159
"whatwg-url"
67
160
]
68
161
},
162
162
+
"mongodb-memory-server-core@10.3.0": {
163
163
+
"integrity": "sha512-tp+ZfTBAPqHXjROhAFg6HcVVzXaEhh/iHcbY7QPOIiLwr94OkBFAw4pixyGSfP5wI2SZeEA13lXyRmBAhugWgA==",
164
164
+
"dependencies": [
165
165
+
"async-mutex",
166
166
+
"camelcase",
167
167
+
"debug",
168
168
+
"find-cache-dir",
169
169
+
"follow-redirects",
170
170
+
"https-proxy-agent",
171
171
+
"mongodb",
172
172
+
"new-find-package-json",
173
173
+
"semver@7.7.3",
174
174
+
"tar-stream",
175
175
+
"tslib",
176
176
+
"yauzl"
177
177
+
]
178
178
+
},
69
179
"mongodb@6.18.0": {
70
180
"integrity": "sha512-fO5ttN9VC8P0F5fqtQmclAkgXZxbIkYRTUi1j8JO6IYwvamkhtYDilJr35jOPELR49zqCJgXZWwCtW7B+TM8vQ==",
71
181
"dependencies": [
···
74
184
"mongodb-connection-string-url"
75
185
]
76
186
},
187
187
+
"ms@2.1.3": {
188
188
+
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
189
189
+
},
190
190
+
"new-find-package-json@2.0.0": {
191
191
+
"integrity": "sha512-lDcBsjBSMlj3LXH2v/FW3txlh2pYTjmbOXPYJD93HI5EwuLzI11tdHSIpUMmfq/IOsldj4Ps8M8flhm+pCK4Ew==",
192
192
+
"dependencies": [
193
193
+
"debug"
194
194
+
]
195
195
+
},
196
196
+
"p-limit@2.3.0": {
197
197
+
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
198
198
+
"dependencies": [
199
199
+
"p-try"
200
200
+
]
201
201
+
},
202
202
+
"p-locate@4.1.0": {
203
203
+
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
204
204
+
"dependencies": [
205
205
+
"p-limit"
206
206
+
]
207
207
+
},
208
208
+
"p-try@2.2.0": {
209
209
+
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
210
210
+
},
211
211
+
"path-exists@4.0.0": {
212
212
+
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="
213
213
+
},
214
214
+
"pend@1.2.0": {
215
215
+
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="
216
216
+
},
217
217
+
"pkg-dir@4.2.0": {
218
218
+
"integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
219
219
+
"dependencies": [
220
220
+
"find-up"
221
221
+
]
222
222
+
},
77
223
"punycode@2.3.1": {
78
224
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="
225
225
+
},
226
226
+
"semver@6.3.1": {
227
227
+
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
228
228
+
"bin": true
229
229
+
},
230
230
+
"semver@7.7.3": {
231
231
+
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
232
232
+
"bin": true
79
233
},
80
234
"sparse-bitfield@3.0.3": {
81
235
"integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
···
83
237
"memory-pager"
84
238
]
85
239
},
240
240
+
"streamx@2.23.0": {
241
241
+
"integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==",
242
242
+
"dependencies": [
243
243
+
"events-universal",
244
244
+
"fast-fifo",
245
245
+
"text-decoder"
246
246
+
]
247
247
+
},
248
248
+
"tar-stream@3.1.7": {
249
249
+
"integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==",
250
250
+
"dependencies": [
251
251
+
"b4a",
252
252
+
"fast-fifo",
253
253
+
"streamx"
254
254
+
]
255
255
+
},
256
256
+
"text-decoder@1.2.3": {
257
257
+
"integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==",
258
258
+
"dependencies": [
259
259
+
"b4a"
260
260
+
]
261
261
+
},
86
262
"tr46@5.1.1": {
87
263
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
88
264
"dependencies": [
89
265
"punycode"
90
266
]
91
267
},
268
268
+
"tslib@2.8.1": {
269
269
+
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
270
270
+
},
92
271
"undici-types@6.21.0": {
93
272
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="
94
273
},
···
101
280
"tr46",
102
281
"webidl-conversions"
103
282
]
283
283
+
},
284
284
+
"yauzl@3.2.0": {
285
285
+
"integrity": "sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w==",
286
286
+
"dependencies": [
287
287
+
"buffer-crc32",
288
288
+
"pend"
289
289
+
]
104
290
}
105
291
},
106
292
"workspace": {
107
293
"dependencies": [
108
294
"jsr:@standard-schema/spec@1",
295
295
+
"jsr:@std/assert@^1.0.16",
296
296
+
"jsr:@zod/zod@^4.1.13",
297
297
+
"npm:mongodb-memory-server-core@^10.3.0",
109
298
"npm:mongodb@^6.18.0"
110
299
]
111
300
}
+30
-2
model.ts
···
30
30
return result.value;
31
31
}
32
32
33
33
+
// Helper function to validate partial update data
34
34
+
// Uses schema.partial() if available (e.g., Zod)
35
35
+
function parsePartial<T extends Schema>(
36
36
+
schema: T,
37
37
+
data: Partial<Infer<T>>,
38
38
+
): Partial<Infer<T>> {
39
39
+
// Get partial schema if available
40
40
+
const partialSchema = (
41
41
+
typeof schema === "object" &&
42
42
+
schema !== null &&
43
43
+
"partial" in schema &&
44
44
+
typeof (schema as { partial?: () => unknown }).partial === "function"
45
45
+
)
46
46
+
? (schema as { partial: () => T }).partial()
47
47
+
: schema;
48
48
+
49
49
+
const result = partialSchema["~standard"].validate(data);
50
50
+
if (result instanceof Promise) {
51
51
+
throw new Error("Async validation not supported");
52
52
+
}
53
53
+
if (result.issues) {
54
54
+
throw new Error(`Update validation failed: ${JSON.stringify(result.issues)}`);
55
55
+
}
56
56
+
return result.value as Partial<Infer<T>>;
57
57
+
}
58
58
+
33
59
export class Model<T extends Schema> {
34
60
private collection: Collection<Infer<T>>;
35
61
private schema: T;
···
70
96
query: Filter<Infer<T>>,
71
97
data: Partial<Infer<T>>,
72
98
): Promise<UpdateResult> {
73
73
-
return await this.collection.updateMany(query, { $set: data });
99
99
+
const validatedData = parsePartial(this.schema, data);
100
100
+
return await this.collection.updateMany(query, { $set: validatedData });
74
101
}
75
102
76
103
async updateOne(
77
104
query: Filter<Infer<T>>,
78
105
data: Partial<Infer<T>>,
79
106
): Promise<UpdateResult> {
80
80
-
return await this.collection.updateOne(query, { $set: data });
107
107
+
const validatedData = parsePartial(this.schema, data);
108
108
+
return await this.collection.updateOne(query, { $set: validatedData });
81
109
}
82
110
83
111
async replaceOne(
-235
scripts/test.ts
···
1
1
-
#!/usr/bin/env -S deno run --allow-run --allow-read
2
2
-
3
3
-
/**
4
4
-
* Test runner script for Nozzle ORM
5
5
-
*
6
6
-
* Usage:
7
7
-
* deno run --allow-run --allow-read scripts/test.ts [options]
8
8
-
*
9
9
-
* Options:
10
10
-
* --mock Run only mock tests (no MongoDB required)
11
11
-
* --real Run integration tests (requires MongoDB)
12
12
-
* --all Run all tests (default)
13
13
-
* --bdd Use BDD-style output for mock tests
14
14
-
* --filter Filter tests by name pattern
15
15
-
* --watch Watch for file changes and re-run tests
16
16
-
* --help Show this help message
17
17
-
*/
18
18
-
19
19
-
interface TestOptions {
20
20
-
mock?: boolean;
21
21
-
real?: boolean;
22
22
-
all?: boolean;
23
23
-
bdd?: boolean;
24
24
-
filter?: string;
25
25
-
watch?: boolean;
26
26
-
help?: boolean;
27
27
-
}
28
28
-
29
29
-
function parseArgs(): TestOptions {
30
30
-
const args = Deno.args;
31
31
-
const options: TestOptions = {};
32
32
-
33
33
-
for (let i = 0; i < args.length; i++) {
34
34
-
const arg = args[i];
35
35
-
switch (arg) {
36
36
-
case "--mock":
37
37
-
options.mock = true;
38
38
-
break;
39
39
-
case "--real":
40
40
-
options.real = true;
41
41
-
break;
42
42
-
case "--all":
43
43
-
options.all = true;
44
44
-
break;
45
45
-
case "--bdd":
46
46
-
options.bdd = true;
47
47
-
break;
48
48
-
case "--filter":
49
49
-
options.filter = args[++i];
50
50
-
break;
51
51
-
case "--watch":
52
52
-
options.watch = true;
53
53
-
break;
54
54
-
case "--help":
55
55
-
options.help = true;
56
56
-
break;
57
57
-
}
58
58
-
}
59
59
-
60
60
-
// Default to all tests if no specific option is provided
61
61
-
if (!options.mock && !options.real && !options.help) {
62
62
-
options.all = true;
63
63
-
}
64
64
-
65
65
-
return options;
66
66
-
}
67
67
-
68
68
-
function showHelp() {
69
69
-
console.log(`
70
70
-
🧪 Nozzle ORM Test Runner
71
71
-
72
72
-
Usage:
73
73
-
deno run --allow-run --allow-read scripts/test.ts [options]
74
74
-
75
75
-
Options:
76
76
-
--mock Run only mock tests (no MongoDB required)
77
77
-
--real Run integration tests (requires MongoDB)
78
78
-
--all Run all tests (default)
79
79
-
--bdd Use BDD-style output for mock tests
80
80
-
--filter Filter tests by name pattern
81
81
-
--watch Watch for file changes and re-run tests
82
82
-
--help Show this help message
83
83
-
84
84
-
Examples:
85
85
-
scripts/test.ts # Run all tests
86
86
-
scripts/test.ts --mock # Run only mock tests
87
87
-
scripts/test.ts --real # Run only integration tests
88
88
-
scripts/test.ts --mock --bdd # Run mock tests with BDD output
89
89
-
scripts/test.ts --filter "Insert" # Run tests matching "Insert"
90
90
-
scripts/test.ts --watch --mock # Watch and run mock tests
91
91
-
92
92
-
Prerequisites for integration tests:
93
93
-
- MongoDB running on localhost:27017
94
94
-
- Or update connection string in tests/main_test.ts
95
95
-
`);
96
96
-
}
97
97
-
98
98
-
async function runCommand(cmd: string[]) {
99
99
-
const process = new Deno.Command(cmd[0], {
100
100
-
args: cmd.slice(1),
101
101
-
stdout: "inherit",
102
102
-
stderr: "inherit",
103
103
-
});
104
104
-
105
105
-
const { success, code } = await process.output();
106
106
-
return { success, code };
107
107
-
}
108
108
-
109
109
-
async function runTests(options: TestOptions) {
110
110
-
const baseCmd = ["deno", "test"];
111
111
-
const permissions = [
112
112
-
"--allow-net",
113
113
-
"--allow-read",
114
114
-
"--allow-write",
115
115
-
"--allow-env",
116
116
-
"--allow-sys",
117
117
-
];
118
118
-
119
119
-
if (options.help) {
120
120
-
showHelp();
121
121
-
return;
122
122
-
}
123
123
-
124
124
-
console.log("🚀 Starting Nozzle Tests...\n");
125
125
-
126
126
-
if (options.mock) {
127
127
-
console.log("📋 Running mock tests (no MongoDB required)...");
128
128
-
const cmd = [...baseCmd, "tests/mock_test.ts"];
129
129
-
if (options.bdd) {
130
130
-
cmd.push("--reporter", "pretty");
131
131
-
}
132
132
-
if (options.filter) {
133
133
-
cmd.push("--filter", options.filter);
134
134
-
}
135
135
-
if (options.watch) {
136
136
-
cmd.push("--watch");
137
137
-
}
138
138
-
139
139
-
const result = await runCommand(cmd);
140
140
-
if (!result.success) {
141
141
-
console.error("❌ Mock tests failed");
142
142
-
Deno.exit(result.code);
143
143
-
} else {
144
144
-
console.log("✅ Mock tests passed!");
145
145
-
}
146
146
-
}
147
147
-
148
148
-
if (options.real) {
149
149
-
console.log("🗄️ Running integration tests (MongoDB required)...");
150
150
-
console.log("⚠️ Make sure MongoDB is running on localhost:27017\n");
151
151
-
152
152
-
const cmd = [...baseCmd, ...permissions, "tests/main_test.ts"];
153
153
-
if (options.filter) {
154
154
-
cmd.push("--filter", options.filter);
155
155
-
}
156
156
-
if (options.watch) {
157
157
-
cmd.push("--watch");
158
158
-
}
159
159
-
160
160
-
const result = await runCommand(cmd);
161
161
-
if (!result.success) {
162
162
-
console.error("❌ Integration tests failed");
163
163
-
if (result.code === 1) {
164
164
-
console.log("\n💡 Troubleshooting tips:");
165
165
-
console.log(
166
166
-
" • Ensure MongoDB is running: brew services start mongodb-community",
167
167
-
);
168
168
-
console.log(
169
169
-
" • Or start with Docker: docker run -p 27017:27017 -d mongo",
170
170
-
);
171
171
-
console.log(" • Check connection at: mongodb://localhost:27017");
172
172
-
}
173
173
-
Deno.exit(result.code);
174
174
-
} else {
175
175
-
console.log("✅ Integration tests passed!");
176
176
-
}
177
177
-
}
178
178
-
179
179
-
if (options.all) {
180
180
-
console.log("🎯 Running all tests...\n");
181
181
-
182
182
-
// Run mock tests first
183
183
-
console.log("1️⃣ Running mock tests...");
184
184
-
const mockCmd = [...baseCmd, "tests/mock_test.ts"];
185
185
-
if (options.bdd) {
186
186
-
mockCmd.push("--reporter", "pretty");
187
187
-
}
188
188
-
if (options.filter) {
189
189
-
mockCmd.push("--filter", options.filter);
190
190
-
}
191
191
-
192
192
-
const mockResult = await runCommand(mockCmd);
193
193
-
if (mockResult.success) {
194
194
-
console.log("✅ Mock tests passed!\n");
195
195
-
} else {
196
196
-
console.error("❌ Mock tests failed\n");
197
197
-
}
198
198
-
199
199
-
// Run integration tests
200
200
-
console.log("2️⃣ Running integration tests...");
201
201
-
console.log("⚠️ Make sure MongoDB is running on localhost:27017\n");
202
202
-
203
203
-
const integrationCmd = [...baseCmd, ...permissions, "tests/main_test.ts"];
204
204
-
if (options.filter) {
205
205
-
integrationCmd.push("--filter", options.filter);
206
206
-
}
207
207
-
if (options.watch) {
208
208
-
integrationCmd.push("--watch");
209
209
-
}
210
210
-
211
211
-
const integrationResult = await runCommand(integrationCmd);
212
212
-
213
213
-
if (mockResult.success && integrationResult.success) {
214
214
-
console.log("\n🎉 All tests passed!");
215
215
-
} else {
216
216
-
console.error("\n💥 Some tests failed!");
217
217
-
if (!integrationResult.success) {
218
218
-
console.log("\n💡 Integration test troubleshooting:");
219
219
-
console.log(
220
220
-
" • Ensure MongoDB is running: brew services start mongodb-community",
221
221
-
);
222
222
-
console.log(
223
223
-
" • Or start with Docker: docker run -p 27017:27017 -d mongo",
224
224
-
);
225
225
-
console.log(" • Check connection at: mongodb://localhost:27017");
226
226
-
}
227
227
-
Deno.exit(Math.max(mockResult.code, integrationResult.code));
228
228
-
}
229
229
-
}
230
230
-
}
231
231
-
232
232
-
if (import.meta.main) {
233
233
-
const options = parseArgs();
234
234
-
await runTests(options);
235
235
-
}
+161
tests/crud_test.ts
···
1
1
+
import { assertEquals, assertExists } from "@std/assert";
2
2
+
import { ObjectId } from "mongodb";
3
3
+
import {
4
4
+
cleanupCollection,
5
5
+
createUserModel,
6
6
+
setupTestDb,
7
7
+
teardownTestDb,
8
8
+
type UserInsert,
9
9
+
type userSchema,
10
10
+
} from "./utils.ts";
11
11
+
import type { Model } from "../mod.ts";
12
12
+
13
13
+
let UserModel: Model<typeof userSchema>;
14
14
+
15
15
+
Deno.test.beforeAll(async () => {
16
16
+
await setupTestDb();
17
17
+
UserModel = createUserModel();
18
18
+
});
19
19
+
20
20
+
Deno.test.beforeEach(async () => {
21
21
+
await cleanupCollection(UserModel);
22
22
+
});
23
23
+
24
24
+
Deno.test.afterAll(async () => {
25
25
+
await teardownTestDb();
26
26
+
});
27
27
+
28
28
+
Deno.test({
29
29
+
name: "CRUD: Insert - should insert a new user successfully",
30
30
+
async fn() {
31
31
+
32
32
+
const newUser: UserInsert = {
33
33
+
name: "Test User",
34
34
+
email: "test@example.com",
35
35
+
age: 25,
36
36
+
};
37
37
+
38
38
+
const insertResult = await UserModel.insertOne(newUser);
39
39
+
40
40
+
assertExists(insertResult.insertedId);
41
41
+
console.log("User inserted with ID:", insertResult.insertedId);
42
42
+
},
43
43
+
sanitizeResources: false,
44
44
+
sanitizeOps: false,
45
45
+
});
46
46
+
47
47
+
Deno.test({
48
48
+
name: "CRUD: Find - should find the inserted user",
49
49
+
async fn() {
50
50
+
51
51
+
// First insert a user for this test
52
52
+
const newUser: UserInsert = {
53
53
+
name: "Find Test User",
54
54
+
email: "findtest@example.com",
55
55
+
age: 30,
56
56
+
};
57
57
+
const insertResult = await UserModel.insertOne(newUser);
58
58
+
assertExists(insertResult.insertedId);
59
59
+
60
60
+
const foundUser = await UserModel.findOne({
61
61
+
_id: new ObjectId(insertResult.insertedId),
62
62
+
});
63
63
+
64
64
+
assertExists(foundUser);
65
65
+
assertEquals(foundUser.email, "findtest@example.com");
66
66
+
assertEquals(foundUser.name, "Find Test User");
67
67
+
assertEquals(foundUser.age, 30);
68
68
+
},
69
69
+
sanitizeResources: false,
70
70
+
sanitizeOps: false,
71
71
+
});
72
72
+
73
73
+
Deno.test({
74
74
+
name: "CRUD: Update - should update user data",
75
75
+
async fn() {
76
76
+
77
77
+
// Insert a user for this test
78
78
+
const newUser: UserInsert = {
79
79
+
name: "Update Test User",
80
80
+
email: "updatetest@example.com",
81
81
+
age: 25,
82
82
+
};
83
83
+
const insertResult = await UserModel.insertOne(newUser);
84
84
+
assertExists(insertResult.insertedId);
85
85
+
86
86
+
// Update the user
87
87
+
const updateResult = await UserModel.update(
88
88
+
{ _id: new ObjectId(insertResult.insertedId) },
89
89
+
{ age: 26 },
90
90
+
);
91
91
+
92
92
+
assertEquals(updateResult.modifiedCount, 1);
93
93
+
94
94
+
// Verify the update
95
95
+
const updatedUser = await UserModel.findOne({
96
96
+
_id: new ObjectId(insertResult.insertedId),
97
97
+
});
98
98
+
99
99
+
assertExists(updatedUser);
100
100
+
assertEquals(updatedUser.age, 26);
101
101
+
},
102
102
+
sanitizeResources: false,
103
103
+
sanitizeOps: false,
104
104
+
});
105
105
+
106
106
+
Deno.test({
107
107
+
name: "CRUD: Delete - should delete user successfully",
108
108
+
async fn() {
109
109
+
110
110
+
// Insert a user for this test
111
111
+
const newUser: UserInsert = {
112
112
+
name: "Delete Test User",
113
113
+
email: "deletetest@example.com",
114
114
+
age: 35,
115
115
+
};
116
116
+
const insertResult = await UserModel.insertOne(newUser);
117
117
+
assertExists(insertResult.insertedId);
118
118
+
119
119
+
// Delete the user
120
120
+
const deleteResult = await UserModel.delete({
121
121
+
_id: new ObjectId(insertResult.insertedId),
122
122
+
});
123
123
+
124
124
+
assertEquals(deleteResult.deletedCount, 1);
125
125
+
126
126
+
// Verify deletion
127
127
+
const deletedUser = await UserModel.findOne({
128
128
+
_id: new ObjectId(insertResult.insertedId),
129
129
+
});
130
130
+
131
131
+
assertEquals(deletedUser, null);
132
132
+
},
133
133
+
sanitizeResources: false,
134
134
+
sanitizeOps: false,
135
135
+
});
136
136
+
137
137
+
Deno.test({
138
138
+
name: "CRUD: Find Multiple - should find multiple users",
139
139
+
async fn() {
140
140
+
141
141
+
// Insert multiple users
142
142
+
const users: UserInsert[] = [
143
143
+
{ name: "User 1", email: "user1@example.com", age: 20 },
144
144
+
{ name: "User 2", email: "user2@example.com", age: 25 },
145
145
+
{ name: "User 3", email: "user3@example.com", age: 30 },
146
146
+
];
147
147
+
148
148
+
for (const user of users) {
149
149
+
await UserModel.insertOne(user);
150
150
+
}
151
151
+
152
152
+
// Find all users with age >= 25
153
153
+
const foundUsers = await UserModel.find({ age: { $gte: 25 } });
154
154
+
155
155
+
assertEquals(foundUsers.length >= 2, true);
156
156
+
},
157
157
+
sanitizeResources: false,
158
158
+
sanitizeOps: false,
159
159
+
});
160
160
+
161
161
+
+53
tests/features_test.ts
···
1
1
+
import { assertEquals, assertExists } from "@std/assert";
2
2
+
import { ObjectId } from "mongodb";
3
3
+
import {
4
4
+
cleanupCollection,
5
5
+
createUserModel,
6
6
+
setupTestDb,
7
7
+
teardownTestDb,
8
8
+
type UserInsert,
9
9
+
type userSchema,
10
10
+
} from "./utils.ts";
11
11
+
import type { Model } from "../mod.ts";
12
12
+
13
13
+
let UserModel: Model<typeof userSchema>;
14
14
+
15
15
+
Deno.test.beforeAll(async () => {
16
16
+
await setupTestDb();
17
17
+
UserModel = createUserModel();
18
18
+
});
19
19
+
20
20
+
Deno.test.beforeEach(async () => {
21
21
+
await cleanupCollection(UserModel);
22
22
+
});
23
23
+
24
24
+
Deno.test.afterAll(async () => {
25
25
+
await teardownTestDb();
26
26
+
});
27
27
+
28
28
+
Deno.test({
29
29
+
name: "Features: Default Values - should handle default createdAt",
30
30
+
async fn() {
31
31
+
32
32
+
const newUser: UserInsert = {
33
33
+
name: "Default Test User",
34
34
+
email: "default@example.com",
35
35
+
// No createdAt provided - should use default
36
36
+
};
37
37
+
38
38
+
const insertResult = await UserModel.insertOne(newUser);
39
39
+
assertExists(insertResult.insertedId);
40
40
+
41
41
+
const foundUser = await UserModel.findOne({
42
42
+
_id: new ObjectId(insertResult.insertedId),
43
43
+
});
44
44
+
45
45
+
assertExists(foundUser);
46
46
+
assertExists(foundUser.createdAt);
47
47
+
assertEquals(foundUser.createdAt instanceof Date, true);
48
48
+
},
49
49
+
sanitizeResources: false,
50
50
+
sanitizeOps: false,
51
51
+
});
52
52
+
53
53
+
-229
tests/main_test.ts
···
1
1
-
import { assertEquals, assertExists, assertRejects } from "jsr:@std/assert";
2
2
-
import { z } from "jsr:@zod/zod";
3
3
-
import { connect, disconnect, type InsertType, Model } from "../mod.ts";
4
4
-
import { ObjectId } from "mongodb";
5
5
-
6
6
-
const userSchema = z.object({
7
7
-
name: z.string(),
8
8
-
email: z.email(),
9
9
-
age: z.number().int().positive().optional(),
10
10
-
createdAt: z.date().default(() => new Date()),
11
11
-
});
12
12
-
13
13
-
type UserInsert = InsertType<typeof userSchema>;
14
14
-
15
15
-
let UserModel: Model<typeof userSchema>;
16
16
-
let isSetup = false;
17
17
-
18
18
-
async function setup() {
19
19
-
if (!isSetup) {
20
20
-
await connect("mongodb://localhost:27017", "mizzleorm_test_db");
21
21
-
UserModel = new Model("users", userSchema);
22
22
-
isSetup = true;
23
23
-
}
24
24
-
// Clean up before each test
25
25
-
await UserModel.delete({});
26
26
-
}
27
27
-
28
28
-
async function teardown() {
29
29
-
if (isSetup) {
30
30
-
await disconnect();
31
31
-
isSetup = false;
32
32
-
}
33
33
-
}
34
34
-
35
35
-
Deno.test({
36
36
-
name: "Insert - should insert a new user successfully",
37
37
-
async fn() {
38
38
-
await setup();
39
39
-
40
40
-
const newUser: UserInsert = {
41
41
-
name: "Test User",
42
42
-
email: "test@example.com",
43
43
-
age: 25,
44
44
-
};
45
45
-
46
46
-
const insertResult = await UserModel.insertOne(newUser);
47
47
-
48
48
-
assertExists(insertResult.insertedId);
49
49
-
console.log("User inserted with ID:", insertResult.insertedId);
50
50
-
},
51
51
-
sanitizeResources: false,
52
52
-
sanitizeOps: false,
53
53
-
});
54
54
-
55
55
-
Deno.test({
56
56
-
name: "Find - should find the inserted user",
57
57
-
async fn() {
58
58
-
await setup();
59
59
-
60
60
-
// First insert a user for this test
61
61
-
const newUser: UserInsert = {
62
62
-
name: "Find Test User",
63
63
-
email: "findtest@example.com",
64
64
-
age: 30,
65
65
-
};
66
66
-
const insertResult = await UserModel.insertOne(newUser);
67
67
-
assertExists(insertResult.insertedId);
68
68
-
69
69
-
const foundUser = await UserModel.findOne({
70
70
-
_id: new ObjectId(insertResult.insertedId),
71
71
-
});
72
72
-
73
73
-
assertExists(foundUser);
74
74
-
assertEquals(foundUser.email, "findtest@example.com");
75
75
-
assertEquals(foundUser.name, "Find Test User");
76
76
-
assertEquals(foundUser.age, 30);
77
77
-
},
78
78
-
sanitizeResources: false,
79
79
-
sanitizeOps: false,
80
80
-
});
81
81
-
82
82
-
Deno.test({
83
83
-
name: "Update - should update user data",
84
84
-
async fn() {
85
85
-
await setup();
86
86
-
87
87
-
// Insert a user for this test
88
88
-
const newUser: UserInsert = {
89
89
-
name: "Update Test User",
90
90
-
email: "updatetest@example.com",
91
91
-
age: 25,
92
92
-
};
93
93
-
const insertResult = await UserModel.insertOne(newUser);
94
94
-
assertExists(insertResult.insertedId);
95
95
-
96
96
-
// Update the user
97
97
-
const updateResult = await UserModel.update(
98
98
-
{ _id: new ObjectId(insertResult.insertedId) },
99
99
-
{ age: 26 },
100
100
-
);
101
101
-
102
102
-
assertEquals(updateResult.modifiedCount, 1);
103
103
-
104
104
-
// Verify the update
105
105
-
const updatedUser = await UserModel.findOne({
106
106
-
_id: new ObjectId(insertResult.insertedId),
107
107
-
});
108
108
-
109
109
-
assertExists(updatedUser);
110
110
-
assertEquals(updatedUser.age, 26);
111
111
-
},
112
112
-
sanitizeResources: false,
113
113
-
sanitizeOps: false,
114
114
-
});
115
115
-
116
116
-
Deno.test({
117
117
-
name: "Delete - should delete user successfully",
118
118
-
async fn() {
119
119
-
await setup();
120
120
-
121
121
-
// Insert a user for this test
122
122
-
const newUser: UserInsert = {
123
123
-
name: "Delete Test User",
124
124
-
email: "deletetest@example.com",
125
125
-
age: 35,
126
126
-
};
127
127
-
const insertResult = await UserModel.insertOne(newUser);
128
128
-
assertExists(insertResult.insertedId);
129
129
-
130
130
-
// Delete the user
131
131
-
const deleteResult = await UserModel.delete({
132
132
-
_id: new ObjectId(insertResult.insertedId),
133
133
-
});
134
134
-
135
135
-
assertEquals(deleteResult.deletedCount, 1);
136
136
-
137
137
-
// Verify deletion
138
138
-
const deletedUser = await UserModel.findOne({
139
139
-
_id: new ObjectId(insertResult.insertedId),
140
140
-
});
141
141
-
142
142
-
assertEquals(deletedUser, null);
143
143
-
},
144
144
-
sanitizeResources: false,
145
145
-
sanitizeOps: false,
146
146
-
});
147
147
-
148
148
-
Deno.test({
149
149
-
name: "Schema Validation - should validate user data",
150
150
-
async fn() {
151
151
-
await setup();
152
152
-
153
153
-
const invalidUser = {
154
154
-
name: "Invalid User",
155
155
-
email: "not-an-email", // Invalid email
156
156
-
age: -5, // Negative age
157
157
-
};
158
158
-
159
159
-
// This should throw an error due to schema validation
160
160
-
await assertRejects(
161
161
-
async () => {
162
162
-
await UserModel.insertOne(invalidUser as UserInsert);
163
163
-
},
164
164
-
Error,
165
165
-
);
166
166
-
},
167
167
-
sanitizeResources: false,
168
168
-
sanitizeOps: false,
169
169
-
});
170
170
-
171
171
-
Deno.test({
172
172
-
name: "Find Multiple - should find multiple users",
173
173
-
async fn() {
174
174
-
await setup();
175
175
-
176
176
-
// Insert multiple users
177
177
-
const users: UserInsert[] = [
178
178
-
{ name: "User 1", email: "user1@example.com", age: 20 },
179
179
-
{ name: "User 2", email: "user2@example.com", age: 25 },
180
180
-
{ name: "User 3", email: "user3@example.com", age: 30 },
181
181
-
];
182
182
-
183
183
-
for (const user of users) {
184
184
-
await UserModel.insertOne(user);
185
185
-
}
186
186
-
187
187
-
// Find all users with age >= 25
188
188
-
const foundUsers = await UserModel.find({ age: { $gte: 25 } });
189
189
-
190
190
-
assertEquals(foundUsers.length >= 2, true);
191
191
-
},
192
192
-
sanitizeResources: false,
193
193
-
sanitizeOps: false,
194
194
-
});
195
195
-
196
196
-
Deno.test({
197
197
-
name: "Default Values - should handle default createdAt",
198
198
-
async fn() {
199
199
-
await setup();
200
200
-
201
201
-
const newUser: UserInsert = {
202
202
-
name: "Default Test User",
203
203
-
email: "default@example.com",
204
204
-
// No createdAt provided - should use default
205
205
-
};
206
206
-
207
207
-
const insertResult = await UserModel.insertOne(newUser);
208
208
-
assertExists(insertResult.insertedId);
209
209
-
210
210
-
const foundUser = await UserModel.findOne({
211
211
-
_id: new ObjectId(insertResult.insertedId),
212
212
-
});
213
213
-
214
214
-
assertExists(foundUser);
215
215
-
assertExists(foundUser.createdAt);
216
216
-
assertEquals(foundUser.createdAt instanceof Date, true);
217
217
-
},
218
218
-
sanitizeResources: false,
219
219
-
sanitizeOps: false,
220
220
-
});
221
221
-
222
222
-
Deno.test({
223
223
-
name: "Teardown - Clean up and disconnect",
224
224
-
async fn() {
225
225
-
await teardown();
226
226
-
},
227
227
-
sanitizeResources: false,
228
228
-
sanitizeOps: false,
229
229
-
});
-452
tests/mock_test.ts
···
1
1
-
import { afterEach, beforeEach, describe, it } from "jsr:@std/testing/bdd";
2
2
-
import { assertEquals, assertExists, assertRejects } from "jsr:@std/assert";
3
3
-
import { z } from "jsr:@zod/zod";
4
4
-
5
5
-
// Mock implementation for demonstration
6
6
-
class MockModel<T> {
7
7
-
private data: Array<T & { _id: number }> = [];
8
8
-
private idCounter = 1;
9
9
-
10
10
-
constructor(private collection: string, private schema: z.ZodSchema<T>) {}
11
11
-
12
12
-
insertOne(doc: z.input<z.ZodSchema<T>>) {
13
13
-
// Validate with schema
14
14
-
const validated = this.schema.parse(doc);
15
15
-
const withId = { ...validated, _id: this.idCounter++ };
16
16
-
this.data.push(withId);
17
17
-
return { insertedId: withId._id };
18
18
-
}
19
19
-
20
20
-
findOne(filter: Partial<T & { _id: number }>) {
21
21
-
if (filter._id) {
22
22
-
return this.data.find((item) => item._id === filter._id) || null;
23
23
-
}
24
24
-
return this.data.find((item) =>
25
25
-
Object.entries(filter).every(([key, value]) =>
26
26
-
(item as Record<string, unknown>)[key] === value
27
27
-
)
28
28
-
) || null;
29
29
-
}
30
30
-
31
31
-
find(filter: Record<string, unknown> = {}) {
32
32
-
return this.data.filter((item) => {
33
33
-
return Object.entries(filter).every(([key, value]) => {
34
34
-
if (typeof value === "object" && value !== null && "$gte" in value) {
35
35
-
const itemValue = (item as Record<string, unknown>)[key];
36
36
-
const gteValue = (value as { $gte: unknown }).$gte;
37
37
-
return typeof itemValue === "number" &&
38
38
-
typeof gteValue === "number" && itemValue >= gteValue;
39
39
-
}
40
40
-
return (item as Record<string, unknown>)[key] === value;
41
41
-
});
42
42
-
});
43
43
-
}
44
44
-
45
45
-
update(filter: Partial<T & { _id: number }>, update: Partial<T>) {
46
46
-
let modifiedCount = 0;
47
47
-
this.data = this.data.map((item) => {
48
48
-
if (filter._id && (item as T & { _id: number })._id === filter._id) {
49
49
-
modifiedCount++;
50
50
-
return { ...item, ...update };
51
51
-
}
52
52
-
return item;
53
53
-
});
54
54
-
return { modifiedCount };
55
55
-
}
56
56
-
57
57
-
delete(filter: Partial<T & { _id: number }>) {
58
58
-
const initialLength = this.data.length;
59
59
-
if (Object.keys(filter).length === 0) {
60
60
-
// Delete all
61
61
-
this.data = [];
62
62
-
return { deletedCount: initialLength };
63
63
-
}
64
64
-
65
65
-
this.data = this.data.filter((item) => {
66
66
-
if (filter._id) {
67
67
-
return (item as T & { _id: number })._id !== filter._id;
68
68
-
}
69
69
-
return !Object.entries(filter).every(([key, value]) =>
70
70
-
(item as Record<string, unknown>)[key] === value
71
71
-
);
72
72
-
});
73
73
-
74
74
-
return { deletedCount: initialLength - this.data.length };
75
75
-
}
76
76
-
77
77
-
// Helper method to clear data
78
78
-
clear() {
79
79
-
this.data = [];
80
80
-
this.idCounter = 1;
81
81
-
}
82
82
-
}
83
83
-
84
84
-
// Schema definition
85
85
-
const userSchema = z.object({
86
86
-
name: z.string(),
87
87
-
email: z.string().email(),
88
88
-
age: z.number().int().positive().optional(),
89
89
-
createdAt: z.date().default(() => new Date()),
90
90
-
});
91
91
-
92
92
-
type User = z.infer<typeof userSchema>;
93
93
-
type UserInsert = z.input<typeof userSchema>;
94
94
-
95
95
-
let UserModel: MockModel<User>;
96
96
-
97
97
-
describe("Mock User Model Tests", () => {
98
98
-
beforeEach(() => {
99
99
-
UserModel = new MockModel("users", userSchema);
100
100
-
});
101
101
-
102
102
-
afterEach(() => {
103
103
-
UserModel?.clear();
104
104
-
});
105
105
-
106
106
-
describe("Insert Operations", () => {
107
107
-
it("should insert a new user successfully", async () => {
108
108
-
const newUser: UserInsert = {
109
109
-
name: "Test User",
110
110
-
email: "test@example.com",
111
111
-
age: 25,
112
112
-
};
113
113
-
114
114
-
const insertResult = await UserModel.insertOne(newUser);
115
115
-
116
116
-
assertExists(insertResult.insertedId);
117
117
-
assertEquals(typeof insertResult.insertedId, "number");
118
118
-
});
119
119
-
120
120
-
it("should handle user without optional age", async () => {
121
121
-
const newUser: UserInsert = {
122
122
-
name: "User Without Age",
123
123
-
email: "noage@example.com",
124
124
-
};
125
125
-
126
126
-
const insertResult = await UserModel.insertOne(newUser);
127
127
-
assertExists(insertResult.insertedId);
128
128
-
129
129
-
const foundUser = await UserModel.findOne({
130
130
-
_id: insertResult.insertedId,
131
131
-
});
132
132
-
assertEquals(foundUser?.name, "User Without Age");
133
133
-
assertEquals(foundUser?.age, undefined);
134
134
-
});
135
135
-
136
136
-
it("should apply default createdAt value", async () => {
137
137
-
const newUser: UserInsert = {
138
138
-
name: "Default Test User",
139
139
-
email: "default@example.com",
140
140
-
};
141
141
-
142
142
-
const insertResult = await UserModel.insertOne(newUser);
143
143
-
const foundUser = await UserModel.findOne({
144
144
-
_id: insertResult.insertedId,
145
145
-
});
146
146
-
147
147
-
assertExists(foundUser);
148
148
-
assertExists(foundUser.createdAt);
149
149
-
assertEquals(foundUser.createdAt instanceof Date, true);
150
150
-
});
151
151
-
});
152
152
-
153
153
-
describe("Find Operations", () => {
154
154
-
it("should find user by ID", async () => {
155
155
-
const newUser: UserInsert = {
156
156
-
name: "Find Test User",
157
157
-
email: "findtest@example.com",
158
158
-
age: 30,
159
159
-
};
160
160
-
const insertResult = await UserModel.insertOne(newUser);
161
161
-
162
162
-
const foundUser = await UserModel.findOne({
163
163
-
_id: insertResult.insertedId,
164
164
-
});
165
165
-
166
166
-
assertExists(foundUser);
167
167
-
assertEquals(foundUser.email, "findtest@example.com");
168
168
-
assertEquals(foundUser.name, "Find Test User");
169
169
-
assertEquals(foundUser.age, 30);
170
170
-
});
171
171
-
172
172
-
it("should find user by email", async () => {
173
173
-
await UserModel.insertOne({
174
174
-
name: "Email Test User",
175
175
-
email: "email@test.com",
176
176
-
age: 28,
177
177
-
});
178
178
-
179
179
-
const foundUser = await UserModel.findOne({
180
180
-
email: "email@test.com",
181
181
-
});
182
182
-
183
183
-
assertExists(foundUser);
184
184
-
assertEquals(foundUser.name, "Email Test User");
185
185
-
});
186
186
-
187
187
-
it("should return null for non-existent user", async () => {
188
188
-
const foundUser = await UserModel.findOne({
189
189
-
_id: 999,
190
190
-
});
191
191
-
192
192
-
assertEquals(foundUser, null);
193
193
-
});
194
194
-
195
195
-
it("should find multiple users with filters", async () => {
196
196
-
const users: UserInsert[] = [
197
197
-
{ name: "User 1", email: "user1@example.com", age: 20 },
198
198
-
{ name: "User 2", email: "user2@example.com", age: 25 },
199
199
-
{ name: "User 3", email: "user3@example.com", age: 30 },
200
200
-
];
201
201
-
202
202
-
for (const user of users) {
203
203
-
await UserModel.insertOne(user);
204
204
-
}
205
205
-
206
206
-
const foundUsers = await UserModel.find({ age: { $gte: 25 } });
207
207
-
208
208
-
assertEquals(foundUsers.length, 2);
209
209
-
assertEquals(
210
210
-
foundUsers.every((user) => user.age !== undefined && user.age >= 25),
211
211
-
true,
212
212
-
);
213
213
-
});
214
214
-
215
215
-
it("should find all users with empty filter", async () => {
216
216
-
await UserModel.insertOne({
217
217
-
name: "User A",
218
218
-
email: "a@test.com",
219
219
-
age: 20,
220
220
-
});
221
221
-
await UserModel.insertOne({
222
222
-
name: "User B",
223
223
-
email: "b@test.com",
224
224
-
age: 25,
225
225
-
});
226
226
-
227
227
-
const allUsers = await UserModel.find();
228
228
-
229
229
-
assertEquals(allUsers.length, 2);
230
230
-
});
231
231
-
});
232
232
-
233
233
-
describe("Update Operations", () => {
234
234
-
it("should update user data", async () => {
235
235
-
const newUser: UserInsert = {
236
236
-
name: "Update Test User",
237
237
-
email: "updatetest@example.com",
238
238
-
age: 25,
239
239
-
};
240
240
-
const insertResult = await UserModel.insertOne(newUser);
241
241
-
242
242
-
const updateResult = await UserModel.update(
243
243
-
{ _id: insertResult.insertedId },
244
244
-
{ age: 26 },
245
245
-
);
246
246
-
247
247
-
assertEquals(updateResult.modifiedCount, 1);
248
248
-
249
249
-
const updatedUser = await UserModel.findOne({
250
250
-
_id: insertResult.insertedId,
251
251
-
});
252
252
-
assertEquals(updatedUser?.age, 26);
253
253
-
});
254
254
-
255
255
-
it("should update multiple fields", async () => {
256
256
-
const newUser: UserInsert = {
257
257
-
name: "Multi Update User",
258
258
-
email: "multi@example.com",
259
259
-
age: 30,
260
260
-
};
261
261
-
const insertResult = await UserModel.insertOne(newUser);
262
262
-
263
263
-
await UserModel.update(
264
264
-
{ _id: insertResult.insertedId },
265
265
-
{ name: "Updated Name", age: 35 },
266
266
-
);
267
267
-
268
268
-
const updatedUser = await UserModel.findOne({
269
269
-
_id: insertResult.insertedId,
270
270
-
});
271
271
-
assertEquals(updatedUser?.name, "Updated Name");
272
272
-
assertEquals(updatedUser?.age, 35);
273
273
-
});
274
274
-
275
275
-
it("should return 0 modified count for non-existent user", async () => {
276
276
-
const updateResult = await UserModel.update(
277
277
-
{ _id: 999 },
278
278
-
{ age: 100 },
279
279
-
);
280
280
-
281
281
-
assertEquals(updateResult.modifiedCount, 0);
282
282
-
});
283
283
-
});
284
284
-
285
285
-
describe("Delete Operations", () => {
286
286
-
it("should delete user successfully", async () => {
287
287
-
const newUser: UserInsert = {
288
288
-
name: "Delete Test User",
289
289
-
email: "deletetest@example.com",
290
290
-
age: 35,
291
291
-
};
292
292
-
const insertResult = await UserModel.insertOne(newUser);
293
293
-
294
294
-
const deleteResult = await UserModel.delete({
295
295
-
_id: insertResult.insertedId,
296
296
-
});
297
297
-
298
298
-
assertEquals(deleteResult.deletedCount, 1);
299
299
-
300
300
-
const deletedUser = await UserModel.findOne({
301
301
-
_id: insertResult.insertedId,
302
302
-
});
303
303
-
assertEquals(deletedUser, null);
304
304
-
});
305
305
-
306
306
-
it("should delete all users with empty filter", async () => {
307
307
-
await UserModel.insertOne({
308
308
-
name: "User 1",
309
309
-
email: "user1@test.com",
310
310
-
});
311
311
-
await UserModel.insertOne({
312
312
-
name: "User 2",
313
313
-
email: "user2@test.com",
314
314
-
});
315
315
-
316
316
-
const deleteResult = await UserModel.delete({});
317
317
-
318
318
-
assertEquals(deleteResult.deletedCount, 2);
319
319
-
320
320
-
const remainingUsers = await UserModel.find();
321
321
-
assertEquals(remainingUsers.length, 0);
322
322
-
});
323
323
-
324
324
-
it("should return 0 deleted count for non-existent user", async () => {
325
325
-
const deleteResult = await UserModel.delete({
326
326
-
_id: 999,
327
327
-
});
328
328
-
329
329
-
assertEquals(deleteResult.deletedCount, 0);
330
330
-
});
331
331
-
});
332
332
-
333
333
-
describe("Schema Validation", () => {
334
334
-
it("should validate user data and reject invalid email", async () => {
335
335
-
const invalidUser = {
336
336
-
name: "Invalid User",
337
337
-
email: "not-an-email",
338
338
-
age: 25,
339
339
-
};
340
340
-
341
341
-
await assertRejects(
342
342
-
async () => {
343
343
-
await UserModel.insertOne(invalidUser as UserInsert);
344
344
-
},
345
345
-
z.ZodError,
346
346
-
);
347
347
-
});
348
348
-
349
349
-
it("should reject negative age", async () => {
350
350
-
const invalidUser = {
351
351
-
name: "Invalid Age User",
352
352
-
email: "valid@example.com",
353
353
-
age: -5,
354
354
-
};
355
355
-
356
356
-
await assertRejects(
357
357
-
async () => {
358
358
-
await UserModel.insertOne(invalidUser as UserInsert);
359
359
-
},
360
360
-
z.ZodError,
361
361
-
);
362
362
-
});
363
363
-
364
364
-
it("should reject missing required fields", async () => {
365
365
-
const invalidUser = {
366
366
-
age: 25,
367
367
-
// Missing name and email
368
368
-
};
369
369
-
370
370
-
await assertRejects(
371
371
-
async () => {
372
372
-
await UserModel.insertOne(invalidUser as UserInsert);
373
373
-
},
374
374
-
z.ZodError,
375
375
-
);
376
376
-
});
377
377
-
});
378
378
-
379
379
-
describe("Complex Scenarios", () => {
380
380
-
it("should handle multiple operations in sequence", async () => {
381
381
-
// Insert multiple users
382
382
-
const user1 = await UserModel.insertOne({
383
383
-
name: "Alice",
384
384
-
email: "alice@example.com",
385
385
-
age: 28,
386
386
-
});
387
387
-
388
388
-
const user2 = await UserModel.insertOne({
389
389
-
name: "Bob",
390
390
-
email: "bob@example.com",
391
391
-
age: 32,
392
392
-
});
393
393
-
394
394
-
// Find all users
395
395
-
const allUsers = await UserModel.find({});
396
396
-
assertEquals(allUsers.length, 2);
397
397
-
398
398
-
// Update one user
399
399
-
await UserModel.update({ _id: user1.insertedId }, { age: 29 });
400
400
-
401
401
-
// Delete one user
402
402
-
await UserModel.delete({ _id: user2.insertedId });
403
403
-
404
404
-
// Verify final state
405
405
-
const finalUsers = await UserModel.find({});
406
406
-
assertEquals(finalUsers.length, 1);
407
407
-
assertEquals(finalUsers[0].name, "Alice");
408
408
-
assertEquals(finalUsers[0].age, 29);
409
409
-
});
410
410
-
411
411
-
it("should maintain data isolation between operations", async () => {
412
412
-
// This test ensures that operations don't interfere with each other
413
413
-
const user1 = await UserModel.insertOne({
414
414
-
name: "Isolation Test 1",
415
415
-
email: "iso1@test.com",
416
416
-
age: 20,
417
417
-
});
418
418
-
419
419
-
const user2 = await UserModel.insertOne({
420
420
-
name: "Isolation Test 2",
421
421
-
email: "iso2@test.com",
422
422
-
age: 30,
423
423
-
});
424
424
-
425
425
-
// Update user1 shouldn't affect user2
426
426
-
await UserModel.update({ _id: user1.insertedId }, {
427
427
-
name: "Updated User 1",
428
428
-
});
429
429
-
430
430
-
const foundUser2 = await UserModel.findOne({ _id: user2.insertedId });
431
431
-
assertEquals(foundUser2?.name, "Isolation Test 2"); // Should remain unchanged
432
432
-
});
433
433
-
434
434
-
it("should handle concurrent-like operations", async () => {
435
435
-
const insertPromises = [
436
436
-
UserModel.insertOne({ name: "Concurrent 1", email: "con1@test.com" }),
437
437
-
UserModel.insertOne({ name: "Concurrent 2", email: "con2@test.com" }),
438
438
-
UserModel.insertOne({ name: "Concurrent 3", email: "con3@test.com" }),
439
439
-
];
440
440
-
441
441
-
const results = await Promise.all(insertPromises);
442
442
-
443
443
-
assertEquals(results.length, 3);
444
444
-
results.forEach((result) => {
445
445
-
assertExists(result.insertedId);
446
446
-
});
447
447
-
448
448
-
const allUsers = await UserModel.find({});
449
449
-
assertEquals(allUsers.length, 3);
450
450
-
});
451
451
-
});
452
452
-
});
+47
tests/utils.ts
···
1
1
+
import { z } from "@zod/zod";
2
2
+
import { connect, disconnect, type InsertType, Model } from "../mod.ts";
3
3
+
import { MongoMemoryServer } from "mongodb-memory-server-core";
4
4
+
5
5
+
export const userSchema = z.object({
6
6
+
name: z.string(),
7
7
+
email: z.email(),
8
8
+
age: z.number().int().positive().optional(),
9
9
+
createdAt: z.date().default(() => new Date()),
10
10
+
});
11
11
+
12
12
+
export type UserInsert = InsertType<typeof userSchema>;
13
13
+
14
14
+
let mongoServer: MongoMemoryServer | null = null;
15
15
+
let isSetup = false;
16
16
+
17
17
+
export async function setupTestDb() {
18
18
+
if (!isSetup) {
19
19
+
// Start MongoDB Memory Server
20
20
+
mongoServer = await MongoMemoryServer.create();
21
21
+
const uri = mongoServer.getUri();
22
22
+
23
23
+
// Connect to the in-memory database
24
24
+
await connect(uri, "test_db");
25
25
+
isSetup = true;
26
26
+
}
27
27
+
}
28
28
+
29
29
+
export async function teardownTestDb() {
30
30
+
if (isSetup) {
31
31
+
await disconnect();
32
32
+
if (mongoServer) {
33
33
+
await mongoServer.stop();
34
34
+
mongoServer = null;
35
35
+
}
36
36
+
isSetup = false;
37
37
+
}
38
38
+
}
39
39
+
40
40
+
export function createUserModel(): Model<typeof userSchema> {
41
41
+
return new Model("users", userSchema);
42
42
+
}
43
43
+
44
44
+
export async function cleanupCollection(model: Model<typeof userSchema>) {
45
45
+
await model.delete({});
46
46
+
}
47
47
+
+172
tests/validation_test.ts
···
1
1
+
import { assertEquals, assertExists, assertRejects } from "@std/assert";
2
2
+
import { ObjectId } from "mongodb";
3
3
+
import {
4
4
+
cleanupCollection,
5
5
+
createUserModel,
6
6
+
setupTestDb,
7
7
+
teardownTestDb,
8
8
+
type UserInsert,
9
9
+
type userSchema,
10
10
+
} from "./utils.ts";
11
11
+
import type { Model } from "../mod.ts";
12
12
+
13
13
+
let UserModel: Model<typeof userSchema>;
14
14
+
15
15
+
Deno.test.beforeAll(async () => {
16
16
+
await setupTestDb();
17
17
+
UserModel = createUserModel();
18
18
+
});
19
19
+
20
20
+
Deno.test.beforeEach(async () => {
21
21
+
await cleanupCollection(UserModel);
22
22
+
});
23
23
+
24
24
+
Deno.test.afterAll(async () => {
25
25
+
await teardownTestDb();
26
26
+
});
27
27
+
28
28
+
Deno.test({
29
29
+
name: "Validation: Schema - should validate user data on insert",
30
30
+
async fn() {
31
31
+
32
32
+
const invalidUser = {
33
33
+
name: "Invalid User",
34
34
+
email: "not-an-email", // Invalid email
35
35
+
age: -5, // Negative age
36
36
+
};
37
37
+
38
38
+
// This should throw an error due to schema validation
39
39
+
await assertRejects(
40
40
+
async () => {
41
41
+
await UserModel.insertOne(invalidUser as UserInsert);
42
42
+
},
43
43
+
Error,
44
44
+
);
45
45
+
},
46
46
+
sanitizeResources: false,
47
47
+
sanitizeOps: false,
48
48
+
});
49
49
+
50
50
+
Deno.test({
51
51
+
name: "Validation: Update - should reject invalid email in update",
52
52
+
async fn() {
53
53
+
54
54
+
// Insert a user for this test
55
55
+
const newUser: UserInsert = {
56
56
+
name: "Validation Test User",
57
57
+
email: "valid@example.com",
58
58
+
age: 25,
59
59
+
};
60
60
+
const insertResult = await UserModel.insertOne(newUser);
61
61
+
assertExists(insertResult.insertedId);
62
62
+
63
63
+
// Try to update with invalid email - should throw error
64
64
+
await assertRejects(
65
65
+
async () => {
66
66
+
await UserModel.update(
67
67
+
{ _id: new ObjectId(insertResult.insertedId) },
68
68
+
{ email: "not-an-email" },
69
69
+
);
70
70
+
},
71
71
+
Error,
72
72
+
"Update validation failed",
73
73
+
);
74
74
+
},
75
75
+
sanitizeResources: false,
76
76
+
sanitizeOps: false,
77
77
+
});
78
78
+
79
79
+
Deno.test({
80
80
+
name: "Validation: Update - should reject negative age in update",
81
81
+
async fn() {
82
82
+
83
83
+
// Insert a user for this test
84
84
+
const newUser: UserInsert = {
85
85
+
name: "Age Validation Test User",
86
86
+
email: "age@example.com",
87
87
+
age: 25,
88
88
+
};
89
89
+
const insertResult = await UserModel.insertOne(newUser);
90
90
+
assertExists(insertResult.insertedId);
91
91
+
92
92
+
// Try to update with negative age - should throw error
93
93
+
await assertRejects(
94
94
+
async () => {
95
95
+
await UserModel.updateOne(
96
96
+
{ _id: new ObjectId(insertResult.insertedId) },
97
97
+
{ age: -5 },
98
98
+
);
99
99
+
},
100
100
+
Error,
101
101
+
"Update validation failed",
102
102
+
);
103
103
+
},
104
104
+
sanitizeResources: false,
105
105
+
sanitizeOps: false,
106
106
+
});
107
107
+
108
108
+
Deno.test({
109
109
+
name: "Validation: Update - should reject invalid name type in update",
110
110
+
async fn() {
111
111
+
112
112
+
// Insert a user for this test
113
113
+
const newUser: UserInsert = {
114
114
+
name: "Type Validation Test User",
115
115
+
email: "type@example.com",
116
116
+
age: 25,
117
117
+
};
118
118
+
const insertResult = await UserModel.insertOne(newUser);
119
119
+
assertExists(insertResult.insertedId);
120
120
+
121
121
+
// Try to update with invalid name type (number instead of string) - should throw error
122
122
+
await assertRejects(
123
123
+
async () => {
124
124
+
await UserModel.updateOne(
125
125
+
{ _id: new ObjectId(insertResult.insertedId) },
126
126
+
{ name: 123 as unknown as string },
127
127
+
);
128
128
+
},
129
129
+
Error,
130
130
+
"Update validation failed",
131
131
+
);
132
132
+
},
133
133
+
sanitizeResources: false,
134
134
+
sanitizeOps: false,
135
135
+
});
136
136
+
137
137
+
Deno.test({
138
138
+
name: "Validation: Update - should accept valid partial updates",
139
139
+
async fn() {
140
140
+
141
141
+
// Insert a user for this test
142
142
+
const newUser: UserInsert = {
143
143
+
name: "Valid Update Test User",
144
144
+
email: "validupdate@example.com",
145
145
+
age: 25,
146
146
+
};
147
147
+
const insertResult = await UserModel.insertOne(newUser);
148
148
+
assertExists(insertResult.insertedId);
149
149
+
150
150
+
// Update with valid data - should succeed
151
151
+
const updateResult = await UserModel.updateOne(
152
152
+
{ _id: new ObjectId(insertResult.insertedId) },
153
153
+
{ age: 30, email: "newemail@example.com" },
154
154
+
);
155
155
+
156
156
+
assertEquals(updateResult.modifiedCount, 1);
157
157
+
158
158
+
// Verify the update
159
159
+
const updatedUser = await UserModel.findOne({
160
160
+
_id: new ObjectId(insertResult.insertedId),
161
161
+
});
162
162
+
163
163
+
assertExists(updatedUser);
164
164
+
assertEquals(updatedUser.age, 30);
165
165
+
assertEquals(updatedUser.email, "newemail@example.com");
166
166
+
assertEquals(updatedUser.name, "Valid Update Test User"); // Should remain unchanged
167
167
+
},
168
168
+
sanitizeResources: false,
169
169
+
sanitizeOps: false,
170
170
+
});
171
171
+
172
172
+