Thin MongoDB ODM built for Standard Schema
mongodb zod deno
1import type { z } from "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"; 15import type { InsertType } from "./schema.ts"; 16 17export class Model<T extends z.ZodObject> { 18 private collection: Collection<z.infer<T>>; 19 private schema: T; 20 21 constructor(collectionName: string, schema: T) { 22 this.collection = getDb().collection<z.infer<T>>(collectionName); 23 this.schema = schema; 24 } 25 26 async insertOne(data: InsertType<T>): Promise<InsertOneResult<z.infer<T>>> { 27 const validatedData = this.schema.parse(data); 28 return await this.collection.insertOne( 29 validatedData as OptionalUnlessRequiredId<z.infer<T>>, 30 ); 31 } 32 33 async insertMany( 34 data: InsertType<T>[], 35 ): Promise<InsertManyResult<z.infer<T>>> { 36 const validatedData = data.map((item) => this.schema.parse(item)); 37 return await this.collection.insertMany( 38 validatedData as OptionalUnlessRequiredId<z.infer<T>>[], 39 ); 40 } 41 42 async find(query: Filter<z.infer<T>>): Promise<(WithId<z.infer<T>>)[]> { 43 return await this.collection.find(query).toArray(); 44 } 45 46 async findOne(query: Filter<z.infer<T>>): Promise<WithId<z.infer<T>> | null> { 47 return await this.collection.findOne(query); 48 } 49 50 async findById(id: string | ObjectId): Promise<WithId<z.infer<T>> | null> { 51 const objectId = typeof id === "string" ? new ObjectId(id) : id; 52 return await this.findOne({ _id: objectId } as Filter<z.infer<T>>); 53 } 54 55 async update( 56 query: Filter<z.infer<T>>, 57 data: Partial<z.infer<T>>, 58 ): Promise<UpdateResult> { 59 return await this.collection.updateMany(query, { $set: data }); 60 } 61 62 async updateOne( 63 query: Filter<z.infer<T>>, 64 data: Partial<z.infer<T>>, 65 ): Promise<UpdateResult> { 66 return await this.collection.updateOne(query, { $set: data }); 67 } 68 69 async replaceOne( 70 query: Filter<z.infer<T>>, 71 data: InsertType<T>, 72 ): Promise<UpdateResult> { 73 const validatedData = this.schema.parse(data); 74 return await this.collection.replaceOne( 75 query, 76 validatedData as OptionalUnlessRequiredId<z.infer<T>>, 77 ); 78 } 79 80 async delete(query: Filter<z.infer<T>>): Promise<DeleteResult> { 81 return await this.collection.deleteMany(query); 82 } 83 84 async deleteOne(query: Filter<z.infer<T>>): Promise<DeleteResult> { 85 return await this.collection.deleteOne(query); 86 } 87 88 async count(query: Filter<z.infer<T>>): Promise<number> { 89 return await this.collection.countDocuments(query); 90 } 91 92 async aggregate(pipeline: Document[]): Promise<Document[]> { 93 return await this.collection.aggregate(pipeline).toArray(); 94 } 95 96 // Pagination support for find 97 async findPaginated( 98 query: Filter<z.infer<T>>, 99 options: { skip?: number; limit?: number; sort?: Document } = {}, 100 ): Promise<(WithId<z.infer<T>>)[]> { 101 return await this.collection 102 .find(query) 103 .skip(options.skip ?? 0) 104 .limit(options.limit ?? 10) 105 .sort(options.sort ?? {}) 106 .toArray(); 107 } 108}