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 16export class Model<T extends StandardSchemaV1<unknown, Document>> { 17 private collection: Collection<StandardSchemaV1.InferOutput<T>>; 18 private schema: T; 19 20 constructor(collectionName: string, schema: T) { 21 this.collection = getDb().collection< 22 StandardSchemaV1.InferOutput<T> & Document 23 >( 24 collectionName, 25 ); 26 this.schema = schema; 27 } 28 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 } 39 return await this.collection.insertOne( 40 result.value as OptionalUnlessRequiredId<StandardSchemaV1.InferOutput<T>>, 41 ); 42 } 43 44 async insertMany( 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 }); 57 return await this.collection.insertMany( 58 validatedData as OptionalUnlessRequiredId< 59 StandardSchemaV1.InferOutput<T> 60 >[], 61 ); 62 } 63 64 async find( 65 query: Filter<StandardSchemaV1.InferOutput<T>>, 66 ): Promise<(WithId<StandardSchemaV1.InferOutput<T>>)[]> { 67 return await this.collection.find(query).toArray(); 68 } 69 70 async findOne( 71 query: Filter<StandardSchemaV1.InferOutput<T>>, 72 ): Promise<WithId<StandardSchemaV1.InferOutput<T>> | null> { 73 return await this.collection.findOne(query); 74 } 75 76 async findById( 77 id: string | ObjectId, 78 ): Promise<WithId<StandardSchemaV1.InferOutput<T>> | null> { 79 const objectId = typeof id === "string" ? new ObjectId(id) : id; 80 return await this.findOne( 81 { _id: objectId } as Filter<StandardSchemaV1.InferOutput<T>>, 82 ); 83 } 84 85 async update( 86 query: Filter<StandardSchemaV1.InferOutput<T>>, 87 data: Partial<StandardSchemaV1.InferOutput<T>>, 88 ): Promise<UpdateResult> { 89 return await this.collection.updateMany(query, { $set: data }); 90 } 91 92 async updateOne( 93 query: Filter<StandardSchemaV1.InferOutput<T>>, 94 data: Partial<StandardSchemaV1.InferOutput<T>>, 95 ): Promise<UpdateResult> { 96 return await this.collection.updateOne(query, { $set: data }); 97 } 98 99 async replaceOne( 100 query: Filter<StandardSchemaV1.InferOutput<T>>, 101 data: StandardSchemaV1.InferInput<T>, 102 ): Promise<UpdateResult> { 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 } 110 return await this.collection.replaceOne( 111 query, 112 result.value as OptionalUnlessRequiredId<StandardSchemaV1.InferOutput<T>>, 113 ); 114 } 115 116 async delete( 117 query: Filter<StandardSchemaV1.InferOutput<T>>, 118 ): Promise<DeleteResult> { 119 return await this.collection.deleteMany(query); 120 } 121 122 async deleteOne( 123 query: Filter<StandardSchemaV1.InferOutput<T>>, 124 ): Promise<DeleteResult> { 125 return await this.collection.deleteOne(query); 126 } 127 128 async count(query: Filter<StandardSchemaV1.InferOutput<T>>): Promise<number> { 129 return await this.collection.countDocuments(query); 130 } 131 132 async aggregate(pipeline: Document[]): Promise<Document[]> { 133 return await this.collection.aggregate(pipeline).toArray(); 134 } 135 136 // Pagination support for find 137 async findPaginated( 138 query: Filter<StandardSchemaV1.InferOutput<T>>, 139 options: { skip?: number; limit?: number; sort?: Document } = {}, 140 ): Promise<(WithId<StandardSchemaV1.InferOutput<T>>)[]> { 141 return await this.collection 142 .find(query) 143 .skip(options.skip ?? 0) 144 .limit(options.limit ?? 10) 145 .sort(options.sort ?? {}) 146 .toArray(); 147 } 148}