Thin MongoDB ODM built for Standard Schema
mongodb zod deno

add update validation

knotbin.com 85074949 f64e27b8

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