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