Thin MongoDB ODM built for Standard Schema
mongodb zod deno

zod to standard schema

+87 -35
+1
deno.json
··· 9 9 "test:watch": "deno run -A scripts/test.ts --mock --watch" 10 10 }, 11 11 "imports": { 12 + "@standard-schema/spec": "jsr:@standard-schema/spec@^1.0.0", 12 13 "zod": "jsr:@zod/zod@^4.0.17", 13 14 "mongodb": "npm:mongodb@^6.18.0" 14 15 }
+5
deno.lock
··· 1 1 { 2 2 "version": "5", 3 3 "specifiers": { 4 + "jsr:@standard-schema/spec@1": "1.0.0", 4 5 "jsr:@std/assert@*": "1.0.13", 5 6 "jsr:@std/assert@^1.0.13": "1.0.13", 6 7 "jsr:@std/internal@^1.0.10": "1.0.10", ··· 11 12 "npm:mongodb@^6.18.0": "6.18.0" 12 13 }, 13 14 "jsr": { 15 + "@standard-schema/spec@1.0.0": { 16 + "integrity": "4f20bbcf34e92b92f8c01589b958abc7c87385fa9a96170cecdc643d4d5737c0" 17 + }, 14 18 "@std/assert@1.0.13": { 15 19 "integrity": "ae0d31e41919b12c656c742b22522c32fb26ed0cba32975cb0de2a273cb68b29", 16 20 "dependencies": [ ··· 105 109 }, 106 110 "workspace": { 107 111 "dependencies": [ 112 + "jsr:@standard-schema/spec@1", 108 113 "jsr:@zod/zod@^4.0.17", 109 114 "npm:mongodb@^6.18.0" 110 115 ]
+69 -29
model.ts
··· 1 - import type { z } from "zod"; 1 + import type { StandardSchemaV1 } from "@standard-schema/spec"; 2 2 import type { 3 3 Collection, 4 4 DeleteResult, ··· 12 12 } from "mongodb"; 13 13 import { ObjectId } from "mongodb"; 14 14 import { getDb } from "./client.ts"; 15 - import type { InsertType } from "./schema.ts"; 16 15 17 - export class Model<T extends z.ZodObject> { 18 - private collection: Collection<z.infer<T>>; 16 + export class Model<T extends StandardSchemaV1<unknown, Document>> { 17 + private collection: Collection<StandardSchemaV1.InferOutput<T>>; 19 18 private schema: T; 20 19 21 20 constructor(collectionName: string, schema: T) { 22 - this.collection = getDb().collection<z.infer<T>>(collectionName); 21 + this.collection = getDb().collection< 22 + StandardSchemaV1.InferOutput<T> & Document 23 + >( 24 + collectionName, 25 + ); 23 26 this.schema = schema; 24 27 } 25 28 26 - async insertOne(data: InsertType<T>): Promise<InsertOneResult<z.infer<T>>> { 27 - const validatedData = this.schema.parse(data); 29 + async insertOne( 30 + data: StandardSchemaV1.InferInput<T>, 31 + ): Promise<InsertOneResult<StandardSchemaV1.InferOutput<T>>> { 32 + const result = this.schema["~standard"].validate(data); 33 + if (result instanceof Promise) { 34 + throw new Error("Async validation not supported"); 35 + } 36 + if (result.issues) { 37 + throw new Error(`Validation failed: ${JSON.stringify(result.issues)}`); 38 + } 28 39 return await this.collection.insertOne( 29 - validatedData as OptionalUnlessRequiredId<z.infer<T>>, 40 + result.value as OptionalUnlessRequiredId<StandardSchemaV1.InferOutput<T>>, 30 41 ); 31 42 } 32 43 33 44 async insertMany( 34 - data: InsertType<T>[], 35 - ): Promise<InsertManyResult<z.infer<T>>> { 36 - const validatedData = data.map((item) => this.schema.parse(item)); 45 + data: StandardSchemaV1.InferInput<T>[], 46 + ): Promise<InsertManyResult<StandardSchemaV1.InferOutput<T>>> { 47 + const validatedData = data.map((item) => { 48 + const result = this.schema["~standard"].validate(item); 49 + if (result instanceof Promise) { 50 + throw new Error("Async validation not supported"); 51 + } 52 + if (result.issues) { 53 + throw new Error(`Validation failed: ${JSON.stringify(result.issues)}`); 54 + } 55 + return result.value; 56 + }); 37 57 return await this.collection.insertMany( 38 - validatedData as OptionalUnlessRequiredId<z.infer<T>>[], 58 + validatedData as OptionalUnlessRequiredId< 59 + StandardSchemaV1.InferOutput<T> 60 + >[], 39 61 ); 40 62 } 41 63 42 - async find(query: Filter<z.infer<T>>): Promise<(WithId<z.infer<T>>)[]> { 64 + async find( 65 + query: Filter<StandardSchemaV1.InferOutput<T>>, 66 + ): Promise<(WithId<StandardSchemaV1.InferOutput<T>>)[]> { 43 67 return await this.collection.find(query).toArray(); 44 68 } 45 69 46 - async findOne(query: Filter<z.infer<T>>): Promise<WithId<z.infer<T>> | null> { 70 + async findOne( 71 + query: Filter<StandardSchemaV1.InferOutput<T>>, 72 + ): Promise<WithId<StandardSchemaV1.InferOutput<T>> | null> { 47 73 return await this.collection.findOne(query); 48 74 } 49 75 50 - async findById(id: string | ObjectId): Promise<WithId<z.infer<T>> | null> { 76 + async findById( 77 + id: string | ObjectId, 78 + ): Promise<WithId<StandardSchemaV1.InferOutput<T>> | null> { 51 79 const objectId = typeof id === "string" ? new ObjectId(id) : id; 52 - return await this.findOne({ _id: objectId } as Filter<z.infer<T>>); 80 + return await this.findOne( 81 + { _id: objectId } as Filter<StandardSchemaV1.InferOutput<T>>, 82 + ); 53 83 } 54 84 55 85 async update( 56 - query: Filter<z.infer<T>>, 57 - data: Partial<z.infer<T>>, 86 + query: Filter<StandardSchemaV1.InferOutput<T>>, 87 + data: Partial<StandardSchemaV1.InferOutput<T>>, 58 88 ): Promise<UpdateResult> { 59 89 return await this.collection.updateMany(query, { $set: data }); 60 90 } 61 91 62 92 async updateOne( 63 - query: Filter<z.infer<T>>, 64 - data: Partial<z.infer<T>>, 93 + query: Filter<StandardSchemaV1.InferOutput<T>>, 94 + data: Partial<StandardSchemaV1.InferOutput<T>>, 65 95 ): Promise<UpdateResult> { 66 96 return await this.collection.updateOne(query, { $set: data }); 67 97 } 68 98 69 99 async replaceOne( 70 - query: Filter<z.infer<T>>, 71 - data: InsertType<T>, 100 + query: Filter<StandardSchemaV1.InferOutput<T>>, 101 + data: StandardSchemaV1.InferInput<T>, 72 102 ): Promise<UpdateResult> { 73 - const validatedData = this.schema.parse(data); 103 + const result = this.schema["~standard"].validate(data); 104 + if (result instanceof Promise) { 105 + throw new Error("Async validation not supported"); 106 + } 107 + if (result.issues) { 108 + throw new Error(`Validation failed: ${JSON.stringify(result.issues)}`); 109 + } 74 110 return await this.collection.replaceOne( 75 111 query, 76 - validatedData as OptionalUnlessRequiredId<z.infer<T>>, 112 + result.value as OptionalUnlessRequiredId<StandardSchemaV1.InferOutput<T>>, 77 113 ); 78 114 } 79 115 80 - async delete(query: Filter<z.infer<T>>): Promise<DeleteResult> { 116 + async delete( 117 + query: Filter<StandardSchemaV1.InferOutput<T>>, 118 + ): Promise<DeleteResult> { 81 119 return await this.collection.deleteMany(query); 82 120 } 83 121 84 - async deleteOne(query: Filter<z.infer<T>>): Promise<DeleteResult> { 122 + async deleteOne( 123 + query: Filter<StandardSchemaV1.InferOutput<T>>, 124 + ): Promise<DeleteResult> { 85 125 return await this.collection.deleteOne(query); 86 126 } 87 127 88 - async count(query: Filter<z.infer<T>>): Promise<number> { 128 + async count(query: Filter<StandardSchemaV1.InferOutput<T>>): Promise<number> { 89 129 return await this.collection.countDocuments(query); 90 130 } 91 131 ··· 95 135 96 136 // Pagination support for find 97 137 async findPaginated( 98 - query: Filter<z.infer<T>>, 138 + query: Filter<StandardSchemaV1.InferOutput<T>>, 99 139 options: { skip?: number; limit?: number; sort?: Document } = {}, 100 - ): Promise<(WithId<z.infer<T>>)[]> { 140 + ): Promise<(WithId<StandardSchemaV1.InferOutput<T>>)[]> { 101 141 return await this.collection 102 142 .find(query) 103 143 .skip(options.skip ?? 0)
+12 -6
schema.ts
··· 1 - import type { z } from "zod"; 1 + import type { StandardSchemaV1 } from "@standard-schema/spec"; 2 2 import type { ObjectId } from "mongodb"; 3 3 4 - export type InferModel<T extends z.ZodObject> = z.infer<T> & { 5 - _id?: ObjectId; 6 - }; 4 + export type InferModel< 5 + T extends StandardSchemaV1<unknown, Record<string, unknown>>, 6 + > = 7 + & StandardSchemaV1.InferOutput<T> 8 + & { 9 + _id?: ObjectId; 10 + }; 7 11 8 - export type InsertType<T extends z.ZodObject> = 9 - & Omit<z.infer<T>, "createdAt"> 12 + export type InsertType< 13 + T extends StandardSchemaV1<unknown, Record<string, unknown>>, 14 + > = 15 + & Omit<StandardSchemaV1.InferOutput<T>, "createdAt"> 10 16 & { createdAt?: Date };