Thin MongoDB ODM built for Standard Schema
mongodb zod deno
1import type { StandardSchemaV1 } from "@standard-schema/spec"; 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 17type Schema = StandardSchemaV1<unknown, Document>; 18type Infer<T extends Schema> = StandardSchemaV1.InferOutput<T>; 19type Input<T extends Schema> = StandardSchemaV1.InferInput<T>; 20 21// Helper function to make StandardSchemaV1 validation as simple as Zod's parse() 22function 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 33export class Model<T extends Schema> { 34 private collection: Collection<Infer<T>>; 35 private schema: T; 36 37 constructor(collectionName: string, schema: T) { 38 this.collection = getDb().collection<Infer<T> & Document>(collectionName); 39 this.schema = schema; 40 } 41 42 async insertOne(data: Input<T>): Promise<InsertOneResult<Infer<T>>> { 43 const validatedData = parse(this.schema, data); 44 return await this.collection.insertOne( 45 validatedData as OptionalUnlessRequiredId<Infer<T>>, 46 ); 47 } 48 49 async insertMany(data: Input<T>[]): Promise<InsertManyResult<Infer<T>>> { 50 const validatedData = data.map((item) => parse(this.schema, item)); 51 return await this.collection.insertMany( 52 validatedData as OptionalUnlessRequiredId<Infer<T>>[], 53 ); 54 } 55 56 async find(query: Filter<Infer<T>>): Promise<(WithId<Infer<T>>)[]> { 57 return await this.collection.find(query).toArray(); 58 } 59 60 async findOne(query: Filter<Infer<T>>): Promise<WithId<Infer<T>> | null> { 61 return await this.collection.findOne(query); 62 } 63 64 async findById(id: string | ObjectId): Promise<WithId<Infer<T>> | null> { 65 const objectId = typeof id === "string" ? new ObjectId(id) : id; 66 return await this.findOne({ _id: objectId } as Filter<Infer<T>>); 67 } 68 69 async update( 70 query: Filter<Infer<T>>, 71 data: Partial<Infer<T>>, 72 ): Promise<UpdateResult> { 73 return await this.collection.updateMany(query, { $set: data }); 74 } 75 76 async updateOne( 77 query: Filter<Infer<T>>, 78 data: Partial<Infer<T>>, 79 ): Promise<UpdateResult> { 80 return await this.collection.updateOne(query, { $set: data }); 81 } 82 83 async replaceOne( 84 query: Filter<Infer<T>>, 85 data: Input<T>, 86 ): Promise<UpdateResult> { 87 const validatedData = parse(this.schema, data); 88 return await this.collection.replaceOne( 89 query, 90 validatedData as OptionalUnlessRequiredId<Infer<T>>, 91 ); 92 } 93 94 async delete(query: Filter<Infer<T>>): Promise<DeleteResult> { 95 return await this.collection.deleteMany(query); 96 } 97 98 async deleteOne(query: Filter<Infer<T>>): Promise<DeleteResult> { 99 return await this.collection.deleteOne(query); 100 } 101 102 async count(query: Filter<Infer<T>>): Promise<number> { 103 return await this.collection.countDocuments(query); 104 } 105 106 async aggregate(pipeline: Document[]): Promise<Document[]> { 107 return await this.collection.aggregate(pipeline).toArray(); 108 } 109 110 // Pagination support for find 111 async findPaginated( 112 query: Filter<Infer<T>>, 113 options: { skip?: number; limit?: number; sort?: Document } = {}, 114 ): Promise<(WithId<Infer<T>>)[]> { 115 return await this.collection 116 .find(query) 117 .skip(options.skip ?? 0) 118 .limit(options.limit ?? 10) 119 .sort(options.sort ?? {}) 120 .toArray(); 121 } 122}