Thin MongoDB ODM built for Standard Schema
mongodb zod deno
1import type { z } from "@zod/zod"; 2import type { 3 Collection, 4 DeleteResult, 5 Document, 6 Filter, 7 InsertManyResult, 8 InsertOneResult, 9 OptionalUnlessRequiredId, 10 UpdateResult, 11 WithId, 12} from "mongodb"; 13import { ObjectId } from "mongodb"; 14import { getDb } from "./client.ts"; 15 16// Type alias for cleaner code - Zod schema 17type Schema = z.ZodObject; 18type Infer<T extends Schema> = z.infer<T> & Document; 19type Input<T extends Schema> = z.input<T>; 20 21// Helper function to validate data using Zod 22function 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() 31function 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 42export class Model<T extends Schema> { 43 private collection: Collection<Infer<T>>; 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 51 async insertOne(data: Input<T>): Promise<InsertOneResult<Infer<T>>> { 52 const validatedData = parse(this.schema, data); 53 return await this.collection.insertOne( 54 validatedData as OptionalUnlessRequiredId<Infer<T>>, 55 ); 56 } 57 58 async insertMany(data: Input<T>[]): Promise<InsertManyResult<Infer<T>>> { 59 const validatedData = data.map((item) => parse(this.schema, item)); 60 return await this.collection.insertMany( 61 validatedData as OptionalUnlessRequiredId<Infer<T>>[], 62 ); 63 } 64 65 async find(query: Filter<Infer<T>>): Promise<(WithId<Infer<T>>)[]> { 66 return await this.collection.find(query).toArray(); 67 } 68 69 async findOne(query: Filter<Infer<T>>): Promise<WithId<Infer<T>> | null> { 70 return await this.collection.findOne(query); 71 } 72 73 async findById(id: string | ObjectId): Promise<WithId<Infer<T>> | null> { 74 const objectId = typeof id === "string" ? new ObjectId(id) : id; 75 return await this.findOne({ _id: objectId } as Filter<Infer<T>>); 76 } 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 107 async delete(query: Filter<Infer<T>>): Promise<DeleteResult> { 108 return await this.collection.deleteMany(query); 109 } 110 111 async deleteOne(query: Filter<Infer<T>>): Promise<DeleteResult> { 112 return await this.collection.deleteOne(query); 113 } 114 115 async count(query: Filter<Infer<T>>): Promise<number> { 116 return await this.collection.countDocuments(query); 117 } 118 119 async aggregate(pipeline: Document[]): Promise<Document[]> { 120 return await this.collection.aggregate(pipeline).toArray(); 121 } 122 123 // Pagination support for find 124 async findPaginated( 125 query: Filter<Infer<T>>, 126 options: { skip?: number; limit?: number; sort?: Document } = {}, 127 ): Promise<(WithId<Infer<T>>)[]> { 128 return await this.collection 129 .find(query) 130 .skip(options.skip ?? 0) 131 .limit(options.limit ?? 10) 132 .sort(options.sort ?? {}) 133 .toArray(); 134 } 135}