Thin MongoDB ODM built for Standard Schema
mongodb zod deno

back to zod i go

knotbin.com 722faea3 85074949

verified
+35 -60
+2 -6
README.md
··· 8 9 ## ✨ Features 10 11 - - **Schema-first:** Define and validate collections using any schema validator 12 - that supports [Standard Schema](https://standardschema.dev). 13 - **Type-safe operations:** Auto-complete and strict typings for `insert`, 14 `find`, `update`, and `delete`. 15 - **Minimal & modular:** No decorators or magic. Just clean, composable APIs. ··· 30 31 ## 🚀 Quick Start 32 33 - Examples below use Zod but any schema validator that supports 34 - [Standard Schema](https://standardschema.dev) will work. 35 - 36 ### 1. Define a schema 37 38 ```ts 39 // src/schemas/user.ts 40 - import { z } from "zod"; 41 42 export const userSchema = z.object({ 43 name: z.string(),
··· 8 9 ## ✨ Features 10 11 + - **Schema-first:** Define and validate collections using [Zod](https://zod.dev). 12 - **Type-safe operations:** Auto-complete and strict typings for `insert`, 13 `find`, `update`, and `delete`. 14 - **Minimal & modular:** No decorators or magic. Just clean, composable APIs. ··· 29 30 ## 🚀 Quick Start 31 32 ### 1. Define a schema 33 34 ```ts 35 // src/schemas/user.ts 36 + import { z } from "@zod/zod"; 37 38 export const userSchema = z.object({ 39 name: z.string(),
-1
deno.json
··· 4 "exports": "./mod.ts", 5 "license": "MIT", 6 "imports": { 7 - "@standard-schema/spec": "jsr:@standard-schema/spec@^1.0.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",
··· 4 "exports": "./mod.ts", 5 "license": "MIT", 6 "imports": { 7 "@std/assert": "jsr:@std/assert@^1.0.16", 8 "@zod/zod": "jsr:@zod/zod@^4.1.13", 9 "mongodb": "npm:mongodb@^6.18.0",
-5
deno.lock
··· 1 { 2 "version": "5", 3 "specifiers": { 4 - "jsr:@standard-schema/spec@1": "1.0.0", 5 "jsr:@std/assert@*": "1.0.13", 6 "jsr:@std/assert@^1.0.13": "1.0.13", 7 "jsr:@std/assert@^1.0.16": "1.0.16", ··· 16 "npm:mongodb@^6.18.0": "6.18.0" 17 }, 18 "jsr": { 19 - "@standard-schema/spec@1.0.0": { 20 - "integrity": "4f20bbcf34e92b92f8c01589b958abc7c87385fa9a96170cecdc643d4d5737c0" 21 - }, 22 "@std/assert@1.0.13": { 23 "integrity": "ae0d31e41919b12c656c742b22522c32fb26ed0cba32975cb0de2a273cb68b29", 24 "dependencies": [ ··· 291 }, 292 "workspace": { 293 "dependencies": [ 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",
··· 1 { 2 "version": "5", 3 "specifiers": { 4 "jsr:@std/assert@*": "1.0.13", 5 "jsr:@std/assert@^1.0.13": "1.0.13", 6 "jsr:@std/assert@^1.0.16": "1.0.16", ··· 15 "npm:mongodb@^6.18.0": "6.18.0" 16 }, 17 "jsr": { 18 "@std/assert@1.0.13": { 19 "integrity": "ae0d31e41919b12c656c742b22522c32fb26ed0cba32975cb0de2a273cb68b29", 20 "dependencies": [ ··· 287 }, 288 "workspace": { 289 "dependencies": [ 290 "jsr:@std/assert@^1.0.16", 291 "jsr:@zod/zod@^4.1.13", 292 "npm:mongodb-memory-server-core@^10.3.0",
+1 -1
examples/user.ts
··· 1 - import { z } from "jsr:@zod/zod"; 2 import { ObjectId } from "mongodb"; 3 import { 4 connect,
··· 1 + import { z } from "@zod/zod"; 2 import { ObjectId } from "mongodb"; 3 import { 4 connect,
+29 -44
model.ts
··· 1 - import type { StandardSchemaV1 } from "@standard-schema/spec"; 2 import type { 3 Collection, 4 DeleteResult, ··· 13 import { ObjectId } from "mongodb"; 14 import { getDb } from "./client.ts"; 15 16 - // Type alias for cleaner code 17 - type Schema = StandardSchemaV1<unknown, Document>; 18 - type Infer<T extends Schema> = StandardSchemaV1.InferOutput<T>; 19 - type Input<T extends Schema> = StandardSchemaV1.InferInput<T>; 20 21 - // Helper function to make StandardSchemaV1 validation as simple as Zod's parse() 22 - function parse<T extends Schema>(schema: T, data: unknown): Infer<T> { 23 - const result = schema["~standard"].validate(data); 24 - if (result instanceof Promise) { 25 - throw new Error("Async validation not supported"); 26 } 27 - if (result.issues) { 28 - throw new Error(`Validation failed: ${JSON.stringify(result.issues)}`); 29 - } 30 - return result.value; 31 } 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 59 export class Model<T extends Schema> { ··· 61 private schema: T; 62 63 constructor(collectionName: string, schema: T) { 64 - this.collection = getDb().collection<Infer<T> & Document>(collectionName); 65 this.schema = schema; 66 } 67 ··· 94 95 async update( 96 query: Filter<Infer<T>>, 97 - data: Partial<Infer<T>>, 98 - ): Promise<UpdateResult> { 99 const validatedData = parsePartial(this.schema, data); 100 - return await this.collection.updateMany(query, { $set: validatedData }); 101 } 102 103 async updateOne( 104 query: Filter<Infer<T>>, 105 - data: Partial<Infer<T>>, 106 - ): Promise<UpdateResult> { 107 const validatedData = parsePartial(this.schema, data); 108 - return await this.collection.updateOne(query, { $set: validatedData }); 109 } 110 111 async replaceOne( 112 query: Filter<Infer<T>>, 113 data: Input<T>, 114 - ): Promise<UpdateResult> { 115 const validatedData = parse(this.schema, data); 116 return await this.collection.replaceOne( 117 query, 118 - validatedData as OptionalUnlessRequiredId<Infer<T>>, 119 ); 120 } 121
··· 1 + import type { z } from "@zod/zod"; 2 import type { 3 Collection, 4 DeleteResult, ··· 13 import { ObjectId } from "mongodb"; 14 import { getDb } from "./client.ts"; 15 16 + // Type alias for cleaner code - Zod schema 17 + type Schema = z.ZodObject; 18 + type Infer<T extends Schema> = z.infer<T> & Document; 19 + type Input<T extends Schema> = z.input<T>; 20 21 + // Helper function to validate data using Zod 22 + function parse<T extends Schema>(schema: T, data: Input<T>): Infer<T> { 23 + const result = schema.safeParse(data); 24 + if (!result.success) { 25 + throw new Error(`Validation failed: ${JSON.stringify(result.error.issues)}`); 26 } 27 + return result.data as Infer<T>; 28 } 29 30 + // Helper function to validate partial update data using Zod's partial() 31 function parsePartial<T extends Schema>( 32 schema: T, 33 + data: Partial<z.infer<T>>, 34 + ): Partial<z.infer<T>> { 35 + const result = schema.partial().safeParse(data); 36 + if (!result.success) { 37 + throw new Error(`Update validation failed: ${JSON.stringify(result.error.issues)}`); 38 } 39 + return result.data as Partial<z.infer<T>>; 40 } 41 42 export class Model<T extends Schema> { ··· 44 private schema: T; 45 46 constructor(collectionName: string, schema: T) { 47 + this.collection = getDb().collection<Infer<T>>(collectionName); 48 this.schema = schema; 49 } 50 ··· 77 78 async update( 79 query: Filter<Infer<T>>, 80 + data: Partial<z.infer<T>>, 81 + ): Promise<UpdateResult<Infer<T>>> { 82 const validatedData = parsePartial(this.schema, data); 83 + return await this.collection.updateMany(query, { $set: validatedData as Partial<Infer<T>> }); 84 } 85 86 async updateOne( 87 query: Filter<Infer<T>>, 88 + data: Partial<z.infer<T>>, 89 + ): Promise<UpdateResult<Infer<T>>> { 90 const validatedData = parsePartial(this.schema, data); 91 + return await this.collection.updateOne(query, { $set: validatedData as Partial<Infer<T>> }); 92 } 93 94 async replaceOne( 95 query: Filter<Infer<T>>, 96 data: Input<T>, 97 + ): Promise<UpdateResult<Infer<T>>> { 98 const validatedData = parse(this.schema, data); 99 + // Remove _id from validatedData for replaceOne (it will use the query's _id) 100 + const { _id, ...withoutId } = validatedData as Infer<T> & { _id?: unknown }; 101 return await this.collection.replaceOne( 102 query, 103 + withoutId as Infer<T>, 104 ); 105 } 106
+3 -3
schema.ts
··· 1 - import type { StandardSchemaV1 } from "@standard-schema/spec"; 2 import type { ObjectId } from "mongodb"; 3 4 - type Schema = StandardSchemaV1<unknown, Record<string, unknown>>; 5 - type Infer<T extends Schema> = StandardSchemaV1.InferOutput<T>; 6 7 export type InferModel<T extends Schema> = Infer<T> & { 8 _id?: ObjectId;
··· 1 + import type { z } from "@zod/zod"; 2 import type { ObjectId } from "mongodb"; 3 4 + type Schema = z.ZodObject; 5 + type Infer<T extends Schema> = z.infer<T>; 6 7 export type InferModel<T extends Schema> = Infer<T> & { 8 _id?: ObjectId;