Thin MongoDB ODM built for Standard Schema
mongodb zod deno

add update validation

knotbin.com 85074949 f64e27b8

verified
+4 -5
deno.json
···
"version": "0.1.0",
"exports": "./mod.ts",
"license": "MIT",
-
"tasks": {
-
"test:mock": "deno test tests/mock_test.ts",
-
"test:watch": "deno run -A scripts/test.ts --mock --watch"
-
},
"imports": {
"@standard-schema/spec": "jsr:@standard-schema/spec@^1.0.0",
-
"mongodb": "npm:mongodb@^6.18.0"
+
"@std/assert": "jsr:@std/assert@^1.0.16",
+
"@zod/zod": "jsr:@zod/zod@^4.1.13",
+
"mongodb": "npm:mongodb@^6.18.0",
+
"mongodb-memory-server-core": "npm:mongodb-memory-server-core@^10.3.0"
}
}
+189
deno.lock
···
"jsr:@standard-schema/spec@1": "1.0.0",
"jsr:@std/assert@*": "1.0.13",
"jsr:@std/assert@^1.0.13": "1.0.13",
+
"jsr:@std/assert@^1.0.16": "1.0.16",
"jsr:@std/internal@^1.0.10": "1.0.10",
+
"jsr:@std/internal@^1.0.12": "1.0.12",
"jsr:@std/internal@^1.0.6": "1.0.10",
"jsr:@std/testing@*": "1.0.15",
+
"jsr:@zod/zod@*": "4.1.13",
+
"jsr:@zod/zod@^4.1.13": "4.1.13",
"npm:@types/node@*": "22.15.15",
+
"npm:mongodb-memory-server-core@^10.3.0": "10.3.0",
"npm:mongodb@^6.18.0": "6.18.0"
},
"jsr": {
···
"jsr:@std/internal@^1.0.6"
]
},
+
"@std/assert@1.0.16": {
+
"integrity": "6a7272ed1eaa77defe76e5ff63ca705d9c495077e2d5fd0126d2b53fc5bd6532",
+
"dependencies": [
+
"jsr:@std/internal@^1.0.12"
+
]
+
},
"@std/internal@1.0.10": {
"integrity": "e3be62ce42cab0e177c27698e5d9800122f67b766a0bea6ca4867886cbde8cf7"
},
+
"@std/internal@1.0.12": {
+
"integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027"
+
},
"@std/testing@1.0.15": {
"integrity": "a490169f5ccb0f3ae9c94fbc69d2cd43603f2cffb41713a85f99bbb0e3087cbc",
"dependencies": [
"jsr:@std/assert@^1.0.13",
"jsr:@std/internal@^1.0.10"
]
+
},
+
"@zod/zod@4.1.13": {
+
"integrity": "fef799152d630583b248645fcac03abedd13e39fd2b752d9466b905d73619bfd"
}
},
"npm": {
···
"@types/webidl-conversions"
]
},
+
"agent-base@7.1.4": {
+
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="
+
},
+
"async-mutex@0.5.0": {
+
"integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==",
+
"dependencies": [
+
"tslib"
+
]
+
},
+
"b4a@1.7.3": {
+
"integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q=="
+
},
+
"bare-events@2.8.2": {
+
"integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ=="
+
},
"bson@6.10.4": {
"integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng=="
},
+
"buffer-crc32@0.2.13": {
+
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="
+
},
+
"camelcase@6.3.0": {
+
"integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="
+
},
+
"commondir@1.0.1": {
+
"integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="
+
},
+
"debug@4.4.3": {
+
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+
"dependencies": [
+
"ms"
+
]
+
},
+
"events-universal@1.0.1": {
+
"integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
+
"dependencies": [
+
"bare-events"
+
]
+
},
+
"fast-fifo@1.3.2": {
+
"integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="
+
},
+
"find-cache-dir@3.3.2": {
+
"integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==",
+
"dependencies": [
+
"commondir",
+
"make-dir",
+
"pkg-dir"
+
]
+
},
+
"find-up@4.1.0": {
+
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+
"dependencies": [
+
"locate-path",
+
"path-exists"
+
]
+
},
+
"follow-redirects@1.15.11": {
+
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="
+
},
+
"https-proxy-agent@7.0.6": {
+
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+
"dependencies": [
+
"agent-base",
+
"debug"
+
]
+
},
+
"locate-path@5.0.0": {
+
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+
"dependencies": [
+
"p-locate"
+
]
+
},
+
"make-dir@3.1.0": {
+
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+
"dependencies": [
+
"semver@6.3.1"
+
]
+
},
"memory-pager@1.5.0": {
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="
},
···
"whatwg-url"
]
},
+
"mongodb-memory-server-core@10.3.0": {
+
"integrity": "sha512-tp+ZfTBAPqHXjROhAFg6HcVVzXaEhh/iHcbY7QPOIiLwr94OkBFAw4pixyGSfP5wI2SZeEA13lXyRmBAhugWgA==",
+
"dependencies": [
+
"async-mutex",
+
"camelcase",
+
"debug",
+
"find-cache-dir",
+
"follow-redirects",
+
"https-proxy-agent",
+
"mongodb",
+
"new-find-package-json",
+
"semver@7.7.3",
+
"tar-stream",
+
"tslib",
+
"yauzl"
+
]
+
},
"mongodb@6.18.0": {
"integrity": "sha512-fO5ttN9VC8P0F5fqtQmclAkgXZxbIkYRTUi1j8JO6IYwvamkhtYDilJr35jOPELR49zqCJgXZWwCtW7B+TM8vQ==",
"dependencies": [
···
"mongodb-connection-string-url"
]
},
+
"ms@2.1.3": {
+
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+
},
+
"new-find-package-json@2.0.0": {
+
"integrity": "sha512-lDcBsjBSMlj3LXH2v/FW3txlh2pYTjmbOXPYJD93HI5EwuLzI11tdHSIpUMmfq/IOsldj4Ps8M8flhm+pCK4Ew==",
+
"dependencies": [
+
"debug"
+
]
+
},
+
"p-limit@2.3.0": {
+
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+
"dependencies": [
+
"p-try"
+
]
+
},
+
"p-locate@4.1.0": {
+
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+
"dependencies": [
+
"p-limit"
+
]
+
},
+
"p-try@2.2.0": {
+
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
+
},
+
"path-exists@4.0.0": {
+
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="
+
},
+
"pend@1.2.0": {
+
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="
+
},
+
"pkg-dir@4.2.0": {
+
"integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
+
"dependencies": [
+
"find-up"
+
]
+
},
"punycode@2.3.1": {
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="
+
},
+
"semver@6.3.1": {
+
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+
"bin": true
+
},
+
"semver@7.7.3": {
+
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+
"bin": true
},
"sparse-bitfield@3.0.3": {
"integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
···
"memory-pager"
]
},
+
"streamx@2.23.0": {
+
"integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==",
+
"dependencies": [
+
"events-universal",
+
"fast-fifo",
+
"text-decoder"
+
]
+
},
+
"tar-stream@3.1.7": {
+
"integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==",
+
"dependencies": [
+
"b4a",
+
"fast-fifo",
+
"streamx"
+
]
+
},
+
"text-decoder@1.2.3": {
+
"integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==",
+
"dependencies": [
+
"b4a"
+
]
+
},
"tr46@5.1.1": {
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
"dependencies": [
"punycode"
]
},
+
"tslib@2.8.1": {
+
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
+
},
"undici-types@6.21.0": {
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="
},
···
"tr46",
"webidl-conversions"
]
+
},
+
"yauzl@3.2.0": {
+
"integrity": "sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w==",
+
"dependencies": [
+
"buffer-crc32",
+
"pend"
+
]
}
},
"workspace": {
"dependencies": [
"jsr:@standard-schema/spec@1",
+
"jsr:@std/assert@^1.0.16",
+
"jsr:@zod/zod@^4.1.13",
+
"npm:mongodb-memory-server-core@^10.3.0",
"npm:mongodb@^6.18.0"
]
}
+30 -2
model.ts
···
return result.value;
}
+
// Helper function to validate partial update data
+
// Uses schema.partial() if available (e.g., Zod)
+
function parsePartial<T extends Schema>(
+
schema: T,
+
data: Partial<Infer<T>>,
+
): Partial<Infer<T>> {
+
// Get partial schema if available
+
const partialSchema = (
+
typeof schema === "object" &&
+
schema !== null &&
+
"partial" in schema &&
+
typeof (schema as { partial?: () => unknown }).partial === "function"
+
)
+
? (schema as { partial: () => T }).partial()
+
: schema;
+
+
const result = partialSchema["~standard"].validate(data);
+
if (result instanceof Promise) {
+
throw new Error("Async validation not supported");
+
}
+
if (result.issues) {
+
throw new Error(`Update validation failed: ${JSON.stringify(result.issues)}`);
+
}
+
return result.value as Partial<Infer<T>>;
+
}
+
export class Model<T extends Schema> {
private collection: Collection<Infer<T>>;
private schema: T;
···
query: Filter<Infer<T>>,
data: Partial<Infer<T>>,
): Promise<UpdateResult> {
-
return await this.collection.updateMany(query, { $set: data });
+
const validatedData = parsePartial(this.schema, data);
+
return await this.collection.updateMany(query, { $set: validatedData });
}
async updateOne(
query: Filter<Infer<T>>,
data: Partial<Infer<T>>,
): Promise<UpdateResult> {
-
return await this.collection.updateOne(query, { $set: data });
+
const validatedData = parsePartial(this.schema, data);
+
return await this.collection.updateOne(query, { $set: validatedData });
}
async replaceOne(
-235
scripts/test.ts
···
-
#!/usr/bin/env -S deno run --allow-run --allow-read
-
-
/**
-
* Test runner script for Nozzle ORM
-
*
-
* Usage:
-
* deno run --allow-run --allow-read scripts/test.ts [options]
-
*
-
* Options:
-
* --mock Run only mock tests (no MongoDB required)
-
* --real Run integration tests (requires MongoDB)
-
* --all Run all tests (default)
-
* --bdd Use BDD-style output for mock tests
-
* --filter Filter tests by name pattern
-
* --watch Watch for file changes and re-run tests
-
* --help Show this help message
-
*/
-
-
interface TestOptions {
-
mock?: boolean;
-
real?: boolean;
-
all?: boolean;
-
bdd?: boolean;
-
filter?: string;
-
watch?: boolean;
-
help?: boolean;
-
}
-
-
function parseArgs(): TestOptions {
-
const args = Deno.args;
-
const options: TestOptions = {};
-
-
for (let i = 0; i < args.length; i++) {
-
const arg = args[i];
-
switch (arg) {
-
case "--mock":
-
options.mock = true;
-
break;
-
case "--real":
-
options.real = true;
-
break;
-
case "--all":
-
options.all = true;
-
break;
-
case "--bdd":
-
options.bdd = true;
-
break;
-
case "--filter":
-
options.filter = args[++i];
-
break;
-
case "--watch":
-
options.watch = true;
-
break;
-
case "--help":
-
options.help = true;
-
break;
-
}
-
}
-
-
// Default to all tests if no specific option is provided
-
if (!options.mock && !options.real && !options.help) {
-
options.all = true;
-
}
-
-
return options;
-
}
-
-
function showHelp() {
-
console.log(`
-
🧪 Nozzle ORM Test Runner
-
-
Usage:
-
deno run --allow-run --allow-read scripts/test.ts [options]
-
-
Options:
-
--mock Run only mock tests (no MongoDB required)
-
--real Run integration tests (requires MongoDB)
-
--all Run all tests (default)
-
--bdd Use BDD-style output for mock tests
-
--filter Filter tests by name pattern
-
--watch Watch for file changes and re-run tests
-
--help Show this help message
-
-
Examples:
-
scripts/test.ts # Run all tests
-
scripts/test.ts --mock # Run only mock tests
-
scripts/test.ts --real # Run only integration tests
-
scripts/test.ts --mock --bdd # Run mock tests with BDD output
-
scripts/test.ts --filter "Insert" # Run tests matching "Insert"
-
scripts/test.ts --watch --mock # Watch and run mock tests
-
-
Prerequisites for integration tests:
-
- MongoDB running on localhost:27017
-
- Or update connection string in tests/main_test.ts
-
`);
-
}
-
-
async function runCommand(cmd: string[]) {
-
const process = new Deno.Command(cmd[0], {
-
args: cmd.slice(1),
-
stdout: "inherit",
-
stderr: "inherit",
-
});
-
-
const { success, code } = await process.output();
-
return { success, code };
-
}
-
-
async function runTests(options: TestOptions) {
-
const baseCmd = ["deno", "test"];
-
const permissions = [
-
"--allow-net",
-
"--allow-read",
-
"--allow-write",
-
"--allow-env",
-
"--allow-sys",
-
];
-
-
if (options.help) {
-
showHelp();
-
return;
-
}
-
-
console.log("🚀 Starting Nozzle Tests...\n");
-
-
if (options.mock) {
-
console.log("📋 Running mock tests (no MongoDB required)...");
-
const cmd = [...baseCmd, "tests/mock_test.ts"];
-
if (options.bdd) {
-
cmd.push("--reporter", "pretty");
-
}
-
if (options.filter) {
-
cmd.push("--filter", options.filter);
-
}
-
if (options.watch) {
-
cmd.push("--watch");
-
}
-
-
const result = await runCommand(cmd);
-
if (!result.success) {
-
console.error("❌ Mock tests failed");
-
Deno.exit(result.code);
-
} else {
-
console.log("✅ Mock tests passed!");
-
}
-
}
-
-
if (options.real) {
-
console.log("🗄️ Running integration tests (MongoDB required)...");
-
console.log("⚠️ Make sure MongoDB is running on localhost:27017\n");
-
-
const cmd = [...baseCmd, ...permissions, "tests/main_test.ts"];
-
if (options.filter) {
-
cmd.push("--filter", options.filter);
-
}
-
if (options.watch) {
-
cmd.push("--watch");
-
}
-
-
const result = await runCommand(cmd);
-
if (!result.success) {
-
console.error("❌ Integration tests failed");
-
if (result.code === 1) {
-
console.log("\n💡 Troubleshooting tips:");
-
console.log(
-
" • Ensure MongoDB is running: brew services start mongodb-community",
-
);
-
console.log(
-
" • Or start with Docker: docker run -p 27017:27017 -d mongo",
-
);
-
console.log(" • Check connection at: mongodb://localhost:27017");
-
}
-
Deno.exit(result.code);
-
} else {
-
console.log("✅ Integration tests passed!");
-
}
-
}
-
-
if (options.all) {
-
console.log("🎯 Running all tests...\n");
-
-
// Run mock tests first
-
console.log("1️⃣ Running mock tests...");
-
const mockCmd = [...baseCmd, "tests/mock_test.ts"];
-
if (options.bdd) {
-
mockCmd.push("--reporter", "pretty");
-
}
-
if (options.filter) {
-
mockCmd.push("--filter", options.filter);
-
}
-
-
const mockResult = await runCommand(mockCmd);
-
if (mockResult.success) {
-
console.log("✅ Mock tests passed!\n");
-
} else {
-
console.error("❌ Mock tests failed\n");
-
}
-
-
// Run integration tests
-
console.log("2️⃣ Running integration tests...");
-
console.log("⚠️ Make sure MongoDB is running on localhost:27017\n");
-
-
const integrationCmd = [...baseCmd, ...permissions, "tests/main_test.ts"];
-
if (options.filter) {
-
integrationCmd.push("--filter", options.filter);
-
}
-
if (options.watch) {
-
integrationCmd.push("--watch");
-
}
-
-
const integrationResult = await runCommand(integrationCmd);
-
-
if (mockResult.success && integrationResult.success) {
-
console.log("\n🎉 All tests passed!");
-
} else {
-
console.error("\n💥 Some tests failed!");
-
if (!integrationResult.success) {
-
console.log("\n💡 Integration test troubleshooting:");
-
console.log(
-
" • Ensure MongoDB is running: brew services start mongodb-community",
-
);
-
console.log(
-
" • Or start with Docker: docker run -p 27017:27017 -d mongo",
-
);
-
console.log(" • Check connection at: mongodb://localhost:27017");
-
}
-
Deno.exit(Math.max(mockResult.code, integrationResult.code));
-
}
-
}
-
}
-
-
if (import.meta.main) {
-
const options = parseArgs();
-
await runTests(options);
-
}
+161
tests/crud_test.ts
···
+
import { assertEquals, assertExists } from "@std/assert";
+
import { ObjectId } from "mongodb";
+
import {
+
cleanupCollection,
+
createUserModel,
+
setupTestDb,
+
teardownTestDb,
+
type UserInsert,
+
type userSchema,
+
} from "./utils.ts";
+
import type { Model } from "../mod.ts";
+
+
let UserModel: Model<typeof userSchema>;
+
+
Deno.test.beforeAll(async () => {
+
await setupTestDb();
+
UserModel = createUserModel();
+
});
+
+
Deno.test.beforeEach(async () => {
+
await cleanupCollection(UserModel);
+
});
+
+
Deno.test.afterAll(async () => {
+
await teardownTestDb();
+
});
+
+
Deno.test({
+
name: "CRUD: Insert - should insert a new user successfully",
+
async fn() {
+
+
const newUser: UserInsert = {
+
name: "Test User",
+
email: "test@example.com",
+
age: 25,
+
};
+
+
const insertResult = await UserModel.insertOne(newUser);
+
+
assertExists(insertResult.insertedId);
+
console.log("User inserted with ID:", insertResult.insertedId);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "CRUD: Find - should find the inserted user",
+
async fn() {
+
+
// First insert a user for this test
+
const newUser: UserInsert = {
+
name: "Find Test User",
+
email: "findtest@example.com",
+
age: 30,
+
};
+
const insertResult = await UserModel.insertOne(newUser);
+
assertExists(insertResult.insertedId);
+
+
const foundUser = await UserModel.findOne({
+
_id: new ObjectId(insertResult.insertedId),
+
});
+
+
assertExists(foundUser);
+
assertEquals(foundUser.email, "findtest@example.com");
+
assertEquals(foundUser.name, "Find Test User");
+
assertEquals(foundUser.age, 30);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "CRUD: Update - should update user data",
+
async fn() {
+
+
// Insert a user for this test
+
const newUser: UserInsert = {
+
name: "Update Test User",
+
email: "updatetest@example.com",
+
age: 25,
+
};
+
const insertResult = await UserModel.insertOne(newUser);
+
assertExists(insertResult.insertedId);
+
+
// Update the user
+
const updateResult = await UserModel.update(
+
{ _id: new ObjectId(insertResult.insertedId) },
+
{ age: 26 },
+
);
+
+
assertEquals(updateResult.modifiedCount, 1);
+
+
// Verify the update
+
const updatedUser = await UserModel.findOne({
+
_id: new ObjectId(insertResult.insertedId),
+
});
+
+
assertExists(updatedUser);
+
assertEquals(updatedUser.age, 26);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "CRUD: Delete - should delete user successfully",
+
async fn() {
+
+
// Insert a user for this test
+
const newUser: UserInsert = {
+
name: "Delete Test User",
+
email: "deletetest@example.com",
+
age: 35,
+
};
+
const insertResult = await UserModel.insertOne(newUser);
+
assertExists(insertResult.insertedId);
+
+
// Delete the user
+
const deleteResult = await UserModel.delete({
+
_id: new ObjectId(insertResult.insertedId),
+
});
+
+
assertEquals(deleteResult.deletedCount, 1);
+
+
// Verify deletion
+
const deletedUser = await UserModel.findOne({
+
_id: new ObjectId(insertResult.insertedId),
+
});
+
+
assertEquals(deletedUser, null);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "CRUD: Find Multiple - should find multiple users",
+
async fn() {
+
+
// Insert multiple users
+
const users: UserInsert[] = [
+
{ name: "User 1", email: "user1@example.com", age: 20 },
+
{ name: "User 2", email: "user2@example.com", age: 25 },
+
{ name: "User 3", email: "user3@example.com", age: 30 },
+
];
+
+
for (const user of users) {
+
await UserModel.insertOne(user);
+
}
+
+
// Find all users with age >= 25
+
const foundUsers = await UserModel.find({ age: { $gte: 25 } });
+
+
assertEquals(foundUsers.length >= 2, true);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
+53
tests/features_test.ts
···
+
import { assertEquals, assertExists } from "@std/assert";
+
import { ObjectId } from "mongodb";
+
import {
+
cleanupCollection,
+
createUserModel,
+
setupTestDb,
+
teardownTestDb,
+
type UserInsert,
+
type userSchema,
+
} from "./utils.ts";
+
import type { Model } from "../mod.ts";
+
+
let UserModel: Model<typeof userSchema>;
+
+
Deno.test.beforeAll(async () => {
+
await setupTestDb();
+
UserModel = createUserModel();
+
});
+
+
Deno.test.beforeEach(async () => {
+
await cleanupCollection(UserModel);
+
});
+
+
Deno.test.afterAll(async () => {
+
await teardownTestDb();
+
});
+
+
Deno.test({
+
name: "Features: Default Values - should handle default createdAt",
+
async fn() {
+
+
const newUser: UserInsert = {
+
name: "Default Test User",
+
email: "default@example.com",
+
// No createdAt provided - should use default
+
};
+
+
const insertResult = await UserModel.insertOne(newUser);
+
assertExists(insertResult.insertedId);
+
+
const foundUser = await UserModel.findOne({
+
_id: new ObjectId(insertResult.insertedId),
+
});
+
+
assertExists(foundUser);
+
assertExists(foundUser.createdAt);
+
assertEquals(foundUser.createdAt instanceof Date, true);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
-229
tests/main_test.ts
···
-
import { assertEquals, assertExists, assertRejects } from "jsr:@std/assert";
-
import { z } from "jsr:@zod/zod";
-
import { connect, disconnect, type InsertType, Model } from "../mod.ts";
-
import { ObjectId } from "mongodb";
-
-
const userSchema = z.object({
-
name: z.string(),
-
email: z.email(),
-
age: z.number().int().positive().optional(),
-
createdAt: z.date().default(() => new Date()),
-
});
-
-
type UserInsert = InsertType<typeof userSchema>;
-
-
let UserModel: Model<typeof userSchema>;
-
let isSetup = false;
-
-
async function setup() {
-
if (!isSetup) {
-
await connect("mongodb://localhost:27017", "mizzleorm_test_db");
-
UserModel = new Model("users", userSchema);
-
isSetup = true;
-
}
-
// Clean up before each test
-
await UserModel.delete({});
-
}
-
-
async function teardown() {
-
if (isSetup) {
-
await disconnect();
-
isSetup = false;
-
}
-
}
-
-
Deno.test({
-
name: "Insert - should insert a new user successfully",
-
async fn() {
-
await setup();
-
-
const newUser: UserInsert = {
-
name: "Test User",
-
email: "test@example.com",
-
age: 25,
-
};
-
-
const insertResult = await UserModel.insertOne(newUser);
-
-
assertExists(insertResult.insertedId);
-
console.log("User inserted with ID:", insertResult.insertedId);
-
},
-
sanitizeResources: false,
-
sanitizeOps: false,
-
});
-
-
Deno.test({
-
name: "Find - should find the inserted user",
-
async fn() {
-
await setup();
-
-
// First insert a user for this test
-
const newUser: UserInsert = {
-
name: "Find Test User",
-
email: "findtest@example.com",
-
age: 30,
-
};
-
const insertResult = await UserModel.insertOne(newUser);
-
assertExists(insertResult.insertedId);
-
-
const foundUser = await UserModel.findOne({
-
_id: new ObjectId(insertResult.insertedId),
-
});
-
-
assertExists(foundUser);
-
assertEquals(foundUser.email, "findtest@example.com");
-
assertEquals(foundUser.name, "Find Test User");
-
assertEquals(foundUser.age, 30);
-
},
-
sanitizeResources: false,
-
sanitizeOps: false,
-
});
-
-
Deno.test({
-
name: "Update - should update user data",
-
async fn() {
-
await setup();
-
-
// Insert a user for this test
-
const newUser: UserInsert = {
-
name: "Update Test User",
-
email: "updatetest@example.com",
-
age: 25,
-
};
-
const insertResult = await UserModel.insertOne(newUser);
-
assertExists(insertResult.insertedId);
-
-
// Update the user
-
const updateResult = await UserModel.update(
-
{ _id: new ObjectId(insertResult.insertedId) },
-
{ age: 26 },
-
);
-
-
assertEquals(updateResult.modifiedCount, 1);
-
-
// Verify the update
-
const updatedUser = await UserModel.findOne({
-
_id: new ObjectId(insertResult.insertedId),
-
});
-
-
assertExists(updatedUser);
-
assertEquals(updatedUser.age, 26);
-
},
-
sanitizeResources: false,
-
sanitizeOps: false,
-
});
-
-
Deno.test({
-
name: "Delete - should delete user successfully",
-
async fn() {
-
await setup();
-
-
// Insert a user for this test
-
const newUser: UserInsert = {
-
name: "Delete Test User",
-
email: "deletetest@example.com",
-
age: 35,
-
};
-
const insertResult = await UserModel.insertOne(newUser);
-
assertExists(insertResult.insertedId);
-
-
// Delete the user
-
const deleteResult = await UserModel.delete({
-
_id: new ObjectId(insertResult.insertedId),
-
});
-
-
assertEquals(deleteResult.deletedCount, 1);
-
-
// Verify deletion
-
const deletedUser = await UserModel.findOne({
-
_id: new ObjectId(insertResult.insertedId),
-
});
-
-
assertEquals(deletedUser, null);
-
},
-
sanitizeResources: false,
-
sanitizeOps: false,
-
});
-
-
Deno.test({
-
name: "Schema Validation - should validate user data",
-
async fn() {
-
await setup();
-
-
const invalidUser = {
-
name: "Invalid User",
-
email: "not-an-email", // Invalid email
-
age: -5, // Negative age
-
};
-
-
// This should throw an error due to schema validation
-
await assertRejects(
-
async () => {
-
await UserModel.insertOne(invalidUser as UserInsert);
-
},
-
Error,
-
);
-
},
-
sanitizeResources: false,
-
sanitizeOps: false,
-
});
-
-
Deno.test({
-
name: "Find Multiple - should find multiple users",
-
async fn() {
-
await setup();
-
-
// Insert multiple users
-
const users: UserInsert[] = [
-
{ name: "User 1", email: "user1@example.com", age: 20 },
-
{ name: "User 2", email: "user2@example.com", age: 25 },
-
{ name: "User 3", email: "user3@example.com", age: 30 },
-
];
-
-
for (const user of users) {
-
await UserModel.insertOne(user);
-
}
-
-
// Find all users with age >= 25
-
const foundUsers = await UserModel.find({ age: { $gte: 25 } });
-
-
assertEquals(foundUsers.length >= 2, true);
-
},
-
sanitizeResources: false,
-
sanitizeOps: false,
-
});
-
-
Deno.test({
-
name: "Default Values - should handle default createdAt",
-
async fn() {
-
await setup();
-
-
const newUser: UserInsert = {
-
name: "Default Test User",
-
email: "default@example.com",
-
// No createdAt provided - should use default
-
};
-
-
const insertResult = await UserModel.insertOne(newUser);
-
assertExists(insertResult.insertedId);
-
-
const foundUser = await UserModel.findOne({
-
_id: new ObjectId(insertResult.insertedId),
-
});
-
-
assertExists(foundUser);
-
assertExists(foundUser.createdAt);
-
assertEquals(foundUser.createdAt instanceof Date, true);
-
},
-
sanitizeResources: false,
-
sanitizeOps: false,
-
});
-
-
Deno.test({
-
name: "Teardown - Clean up and disconnect",
-
async fn() {
-
await teardown();
-
},
-
sanitizeResources: false,
-
sanitizeOps: false,
-
});
-452
tests/mock_test.ts
···
-
import { afterEach, beforeEach, describe, it } from "jsr:@std/testing/bdd";
-
import { assertEquals, assertExists, assertRejects } from "jsr:@std/assert";
-
import { z } from "jsr:@zod/zod";
-
-
// Mock implementation for demonstration
-
class MockModel<T> {
-
private data: Array<T & { _id: number }> = [];
-
private idCounter = 1;
-
-
constructor(private collection: string, private schema: z.ZodSchema<T>) {}
-
-
insertOne(doc: z.input<z.ZodSchema<T>>) {
-
// Validate with schema
-
const validated = this.schema.parse(doc);
-
const withId = { ...validated, _id: this.idCounter++ };
-
this.data.push(withId);
-
return { insertedId: withId._id };
-
}
-
-
findOne(filter: Partial<T & { _id: number }>) {
-
if (filter._id) {
-
return this.data.find((item) => item._id === filter._id) || null;
-
}
-
return this.data.find((item) =>
-
Object.entries(filter).every(([key, value]) =>
-
(item as Record<string, unknown>)[key] === value
-
)
-
) || null;
-
}
-
-
find(filter: Record<string, unknown> = {}) {
-
return this.data.filter((item) => {
-
return Object.entries(filter).every(([key, value]) => {
-
if (typeof value === "object" && value !== null && "$gte" in value) {
-
const itemValue = (item as Record<string, unknown>)[key];
-
const gteValue = (value as { $gte: unknown }).$gte;
-
return typeof itemValue === "number" &&
-
typeof gteValue === "number" && itemValue >= gteValue;
-
}
-
return (item as Record<string, unknown>)[key] === value;
-
});
-
});
-
}
-
-
update(filter: Partial<T & { _id: number }>, update: Partial<T>) {
-
let modifiedCount = 0;
-
this.data = this.data.map((item) => {
-
if (filter._id && (item as T & { _id: number })._id === filter._id) {
-
modifiedCount++;
-
return { ...item, ...update };
-
}
-
return item;
-
});
-
return { modifiedCount };
-
}
-
-
delete(filter: Partial<T & { _id: number }>) {
-
const initialLength = this.data.length;
-
if (Object.keys(filter).length === 0) {
-
// Delete all
-
this.data = [];
-
return { deletedCount: initialLength };
-
}
-
-
this.data = this.data.filter((item) => {
-
if (filter._id) {
-
return (item as T & { _id: number })._id !== filter._id;
-
}
-
return !Object.entries(filter).every(([key, value]) =>
-
(item as Record<string, unknown>)[key] === value
-
);
-
});
-
-
return { deletedCount: initialLength - this.data.length };
-
}
-
-
// Helper method to clear data
-
clear() {
-
this.data = [];
-
this.idCounter = 1;
-
}
-
}
-
-
// Schema definition
-
const userSchema = z.object({
-
name: z.string(),
-
email: z.string().email(),
-
age: z.number().int().positive().optional(),
-
createdAt: z.date().default(() => new Date()),
-
});
-
-
type User = z.infer<typeof userSchema>;
-
type UserInsert = z.input<typeof userSchema>;
-
-
let UserModel: MockModel<User>;
-
-
describe("Mock User Model Tests", () => {
-
beforeEach(() => {
-
UserModel = new MockModel("users", userSchema);
-
});
-
-
afterEach(() => {
-
UserModel?.clear();
-
});
-
-
describe("Insert Operations", () => {
-
it("should insert a new user successfully", async () => {
-
const newUser: UserInsert = {
-
name: "Test User",
-
email: "test@example.com",
-
age: 25,
-
};
-
-
const insertResult = await UserModel.insertOne(newUser);
-
-
assertExists(insertResult.insertedId);
-
assertEquals(typeof insertResult.insertedId, "number");
-
});
-
-
it("should handle user without optional age", async () => {
-
const newUser: UserInsert = {
-
name: "User Without Age",
-
email: "noage@example.com",
-
};
-
-
const insertResult = await UserModel.insertOne(newUser);
-
assertExists(insertResult.insertedId);
-
-
const foundUser = await UserModel.findOne({
-
_id: insertResult.insertedId,
-
});
-
assertEquals(foundUser?.name, "User Without Age");
-
assertEquals(foundUser?.age, undefined);
-
});
-
-
it("should apply default createdAt value", async () => {
-
const newUser: UserInsert = {
-
name: "Default Test User",
-
email: "default@example.com",
-
};
-
-
const insertResult = await UserModel.insertOne(newUser);
-
const foundUser = await UserModel.findOne({
-
_id: insertResult.insertedId,
-
});
-
-
assertExists(foundUser);
-
assertExists(foundUser.createdAt);
-
assertEquals(foundUser.createdAt instanceof Date, true);
-
});
-
});
-
-
describe("Find Operations", () => {
-
it("should find user by ID", async () => {
-
const newUser: UserInsert = {
-
name: "Find Test User",
-
email: "findtest@example.com",
-
age: 30,
-
};
-
const insertResult = await UserModel.insertOne(newUser);
-
-
const foundUser = await UserModel.findOne({
-
_id: insertResult.insertedId,
-
});
-
-
assertExists(foundUser);
-
assertEquals(foundUser.email, "findtest@example.com");
-
assertEquals(foundUser.name, "Find Test User");
-
assertEquals(foundUser.age, 30);
-
});
-
-
it("should find user by email", async () => {
-
await UserModel.insertOne({
-
name: "Email Test User",
-
email: "email@test.com",
-
age: 28,
-
});
-
-
const foundUser = await UserModel.findOne({
-
email: "email@test.com",
-
});
-
-
assertExists(foundUser);
-
assertEquals(foundUser.name, "Email Test User");
-
});
-
-
it("should return null for non-existent user", async () => {
-
const foundUser = await UserModel.findOne({
-
_id: 999,
-
});
-
-
assertEquals(foundUser, null);
-
});
-
-
it("should find multiple users with filters", async () => {
-
const users: UserInsert[] = [
-
{ name: "User 1", email: "user1@example.com", age: 20 },
-
{ name: "User 2", email: "user2@example.com", age: 25 },
-
{ name: "User 3", email: "user3@example.com", age: 30 },
-
];
-
-
for (const user of users) {
-
await UserModel.insertOne(user);
-
}
-
-
const foundUsers = await UserModel.find({ age: { $gte: 25 } });
-
-
assertEquals(foundUsers.length, 2);
-
assertEquals(
-
foundUsers.every((user) => user.age !== undefined && user.age >= 25),
-
true,
-
);
-
});
-
-
it("should find all users with empty filter", async () => {
-
await UserModel.insertOne({
-
name: "User A",
-
email: "a@test.com",
-
age: 20,
-
});
-
await UserModel.insertOne({
-
name: "User B",
-
email: "b@test.com",
-
age: 25,
-
});
-
-
const allUsers = await UserModel.find();
-
-
assertEquals(allUsers.length, 2);
-
});
-
});
-
-
describe("Update Operations", () => {
-
it("should update user data", async () => {
-
const newUser: UserInsert = {
-
name: "Update Test User",
-
email: "updatetest@example.com",
-
age: 25,
-
};
-
const insertResult = await UserModel.insertOne(newUser);
-
-
const updateResult = await UserModel.update(
-
{ _id: insertResult.insertedId },
-
{ age: 26 },
-
);
-
-
assertEquals(updateResult.modifiedCount, 1);
-
-
const updatedUser = await UserModel.findOne({
-
_id: insertResult.insertedId,
-
});
-
assertEquals(updatedUser?.age, 26);
-
});
-
-
it("should update multiple fields", async () => {
-
const newUser: UserInsert = {
-
name: "Multi Update User",
-
email: "multi@example.com",
-
age: 30,
-
};
-
const insertResult = await UserModel.insertOne(newUser);
-
-
await UserModel.update(
-
{ _id: insertResult.insertedId },
-
{ name: "Updated Name", age: 35 },
-
);
-
-
const updatedUser = await UserModel.findOne({
-
_id: insertResult.insertedId,
-
});
-
assertEquals(updatedUser?.name, "Updated Name");
-
assertEquals(updatedUser?.age, 35);
-
});
-
-
it("should return 0 modified count for non-existent user", async () => {
-
const updateResult = await UserModel.update(
-
{ _id: 999 },
-
{ age: 100 },
-
);
-
-
assertEquals(updateResult.modifiedCount, 0);
-
});
-
});
-
-
describe("Delete Operations", () => {
-
it("should delete user successfully", async () => {
-
const newUser: UserInsert = {
-
name: "Delete Test User",
-
email: "deletetest@example.com",
-
age: 35,
-
};
-
const insertResult = await UserModel.insertOne(newUser);
-
-
const deleteResult = await UserModel.delete({
-
_id: insertResult.insertedId,
-
});
-
-
assertEquals(deleteResult.deletedCount, 1);
-
-
const deletedUser = await UserModel.findOne({
-
_id: insertResult.insertedId,
-
});
-
assertEquals(deletedUser, null);
-
});
-
-
it("should delete all users with empty filter", async () => {
-
await UserModel.insertOne({
-
name: "User 1",
-
email: "user1@test.com",
-
});
-
await UserModel.insertOne({
-
name: "User 2",
-
email: "user2@test.com",
-
});
-
-
const deleteResult = await UserModel.delete({});
-
-
assertEquals(deleteResult.deletedCount, 2);
-
-
const remainingUsers = await UserModel.find();
-
assertEquals(remainingUsers.length, 0);
-
});
-
-
it("should return 0 deleted count for non-existent user", async () => {
-
const deleteResult = await UserModel.delete({
-
_id: 999,
-
});
-
-
assertEquals(deleteResult.deletedCount, 0);
-
});
-
});
-
-
describe("Schema Validation", () => {
-
it("should validate user data and reject invalid email", async () => {
-
const invalidUser = {
-
name: "Invalid User",
-
email: "not-an-email",
-
age: 25,
-
};
-
-
await assertRejects(
-
async () => {
-
await UserModel.insertOne(invalidUser as UserInsert);
-
},
-
z.ZodError,
-
);
-
});
-
-
it("should reject negative age", async () => {
-
const invalidUser = {
-
name: "Invalid Age User",
-
email: "valid@example.com",
-
age: -5,
-
};
-
-
await assertRejects(
-
async () => {
-
await UserModel.insertOne(invalidUser as UserInsert);
-
},
-
z.ZodError,
-
);
-
});
-
-
it("should reject missing required fields", async () => {
-
const invalidUser = {
-
age: 25,
-
// Missing name and email
-
};
-
-
await assertRejects(
-
async () => {
-
await UserModel.insertOne(invalidUser as UserInsert);
-
},
-
z.ZodError,
-
);
-
});
-
});
-
-
describe("Complex Scenarios", () => {
-
it("should handle multiple operations in sequence", async () => {
-
// Insert multiple users
-
const user1 = await UserModel.insertOne({
-
name: "Alice",
-
email: "alice@example.com",
-
age: 28,
-
});
-
-
const user2 = await UserModel.insertOne({
-
name: "Bob",
-
email: "bob@example.com",
-
age: 32,
-
});
-
-
// Find all users
-
const allUsers = await UserModel.find({});
-
assertEquals(allUsers.length, 2);
-
-
// Update one user
-
await UserModel.update({ _id: user1.insertedId }, { age: 29 });
-
-
// Delete one user
-
await UserModel.delete({ _id: user2.insertedId });
-
-
// Verify final state
-
const finalUsers = await UserModel.find({});
-
assertEquals(finalUsers.length, 1);
-
assertEquals(finalUsers[0].name, "Alice");
-
assertEquals(finalUsers[0].age, 29);
-
});
-
-
it("should maintain data isolation between operations", async () => {
-
// This test ensures that operations don't interfere with each other
-
const user1 = await UserModel.insertOne({
-
name: "Isolation Test 1",
-
email: "iso1@test.com",
-
age: 20,
-
});
-
-
const user2 = await UserModel.insertOne({
-
name: "Isolation Test 2",
-
email: "iso2@test.com",
-
age: 30,
-
});
-
-
// Update user1 shouldn't affect user2
-
await UserModel.update({ _id: user1.insertedId }, {
-
name: "Updated User 1",
-
});
-
-
const foundUser2 = await UserModel.findOne({ _id: user2.insertedId });
-
assertEquals(foundUser2?.name, "Isolation Test 2"); // Should remain unchanged
-
});
-
-
it("should handle concurrent-like operations", async () => {
-
const insertPromises = [
-
UserModel.insertOne({ name: "Concurrent 1", email: "con1@test.com" }),
-
UserModel.insertOne({ name: "Concurrent 2", email: "con2@test.com" }),
-
UserModel.insertOne({ name: "Concurrent 3", email: "con3@test.com" }),
-
];
-
-
const results = await Promise.all(insertPromises);
-
-
assertEquals(results.length, 3);
-
results.forEach((result) => {
-
assertExists(result.insertedId);
-
});
-
-
const allUsers = await UserModel.find({});
-
assertEquals(allUsers.length, 3);
-
});
-
});
-
});
+47
tests/utils.ts
···
+
import { z } from "@zod/zod";
+
import { connect, disconnect, type InsertType, Model } from "../mod.ts";
+
import { MongoMemoryServer } from "mongodb-memory-server-core";
+
+
export const userSchema = z.object({
+
name: z.string(),
+
email: z.email(),
+
age: z.number().int().positive().optional(),
+
createdAt: z.date().default(() => new Date()),
+
});
+
+
export type UserInsert = InsertType<typeof userSchema>;
+
+
let mongoServer: MongoMemoryServer | null = null;
+
let isSetup = false;
+
+
export async function setupTestDb() {
+
if (!isSetup) {
+
// Start MongoDB Memory Server
+
mongoServer = await MongoMemoryServer.create();
+
const uri = mongoServer.getUri();
+
+
// Connect to the in-memory database
+
await connect(uri, "test_db");
+
isSetup = true;
+
}
+
}
+
+
export async function teardownTestDb() {
+
if (isSetup) {
+
await disconnect();
+
if (mongoServer) {
+
await mongoServer.stop();
+
mongoServer = null;
+
}
+
isSetup = false;
+
}
+
}
+
+
export function createUserModel(): Model<typeof userSchema> {
+
return new Model("users", userSchema);
+
}
+
+
export async function cleanupCollection(model: Model<typeof userSchema>) {
+
await model.delete({});
+
}
+
+172
tests/validation_test.ts
···
+
import { assertEquals, assertExists, assertRejects } from "@std/assert";
+
import { ObjectId } from "mongodb";
+
import {
+
cleanupCollection,
+
createUserModel,
+
setupTestDb,
+
teardownTestDb,
+
type UserInsert,
+
type userSchema,
+
} from "./utils.ts";
+
import type { Model } from "../mod.ts";
+
+
let UserModel: Model<typeof userSchema>;
+
+
Deno.test.beforeAll(async () => {
+
await setupTestDb();
+
UserModel = createUserModel();
+
});
+
+
Deno.test.beforeEach(async () => {
+
await cleanupCollection(UserModel);
+
});
+
+
Deno.test.afterAll(async () => {
+
await teardownTestDb();
+
});
+
+
Deno.test({
+
name: "Validation: Schema - should validate user data on insert",
+
async fn() {
+
+
const invalidUser = {
+
name: "Invalid User",
+
email: "not-an-email", // Invalid email
+
age: -5, // Negative age
+
};
+
+
// This should throw an error due to schema validation
+
await assertRejects(
+
async () => {
+
await UserModel.insertOne(invalidUser as UserInsert);
+
},
+
Error,
+
);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Validation: Update - should reject invalid email in update",
+
async fn() {
+
+
// Insert a user for this test
+
const newUser: UserInsert = {
+
name: "Validation Test User",
+
email: "valid@example.com",
+
age: 25,
+
};
+
const insertResult = await UserModel.insertOne(newUser);
+
assertExists(insertResult.insertedId);
+
+
// Try to update with invalid email - should throw error
+
await assertRejects(
+
async () => {
+
await UserModel.update(
+
{ _id: new ObjectId(insertResult.insertedId) },
+
{ email: "not-an-email" },
+
);
+
},
+
Error,
+
"Update validation failed",
+
);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Validation: Update - should reject negative age in update",
+
async fn() {
+
+
// Insert a user for this test
+
const newUser: UserInsert = {
+
name: "Age Validation Test User",
+
email: "age@example.com",
+
age: 25,
+
};
+
const insertResult = await UserModel.insertOne(newUser);
+
assertExists(insertResult.insertedId);
+
+
// Try to update with negative age - should throw error
+
await assertRejects(
+
async () => {
+
await UserModel.updateOne(
+
{ _id: new ObjectId(insertResult.insertedId) },
+
{ age: -5 },
+
);
+
},
+
Error,
+
"Update validation failed",
+
);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Validation: Update - should reject invalid name type in update",
+
async fn() {
+
+
// Insert a user for this test
+
const newUser: UserInsert = {
+
name: "Type Validation Test User",
+
email: "type@example.com",
+
age: 25,
+
};
+
const insertResult = await UserModel.insertOne(newUser);
+
assertExists(insertResult.insertedId);
+
+
// Try to update with invalid name type (number instead of string) - should throw error
+
await assertRejects(
+
async () => {
+
await UserModel.updateOne(
+
{ _id: new ObjectId(insertResult.insertedId) },
+
{ name: 123 as unknown as string },
+
);
+
},
+
Error,
+
"Update validation failed",
+
);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Validation: Update - should accept valid partial updates",
+
async fn() {
+
+
// Insert a user for this test
+
const newUser: UserInsert = {
+
name: "Valid Update Test User",
+
email: "validupdate@example.com",
+
age: 25,
+
};
+
const insertResult = await UserModel.insertOne(newUser);
+
assertExists(insertResult.insertedId);
+
+
// Update with valid data - should succeed
+
const updateResult = await UserModel.updateOne(
+
{ _id: new ObjectId(insertResult.insertedId) },
+
{ age: 30, email: "newemail@example.com" },
+
);
+
+
assertEquals(updateResult.modifiedCount, 1);
+
+
// Verify the update
+
const updatedUser = await UserModel.findOne({
+
_id: new ObjectId(insertResult.insertedId),
+
});
+
+
assertExists(updatedUser);
+
assertEquals(updatedUser.age, 30);
+
assertEquals(updatedUser.email, "newemail@example.com");
+
assertEquals(updatedUser.name, "Valid Update Test User"); // Should remain unchanged
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+