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 33// Helper function to validate partial update data 34// Uses schema.partial() if available (e.g., Zod) 35function parsePartial<T extends Schema>( 36 schema: T, 37 data: Partial<Infer<T>>, 38): Partial<Infer<T>> { 39 // Get partial schema if available 40 const partialSchema = ( 41 typeof schema === "object" && 42 schema !== null && 43 "partial" in schema && 44 typeof (schema as { partial?: () => unknown }).partial === "function" 45 ) 46 ? (schema as { partial: () => T }).partial() 47 : schema; 48 49 const result = partialSchema["~standard"].validate(data); 50 if (result instanceof Promise) { 51 throw new Error("Async validation not supported"); 52 } 53 if (result.issues) { 54 throw new Error(`Update validation failed: ${JSON.stringify(result.issues)}`); 55 } 56 return result.value as Partial<Infer<T>>; 57} 58 59export class Model<T extends Schema> { 60 private collection: Collection<Infer<T>>; 61 private schema: T; 62 63 constructor(collectionName: string, schema: T) { 64 this.collection = getDb().collection<Infer<T> & Document>(collectionName); 65 this.schema = schema; 66 } 67 68 async insertOne(data: Input<T>): Promise<InsertOneResult<Infer<T>>> { 69 const validatedData = parse(this.schema, data); 70 return await this.collection.insertOne( 71 validatedData as OptionalUnlessRequiredId<Infer<T>>, 72 ); 73 } 74 75 async insertMany(data: Input<T>[]): Promise<InsertManyResult<Infer<T>>> { 76 const validatedData = data.map((item) => parse(this.schema, item)); 77 return await this.collection.insertMany( 78 validatedData as OptionalUnlessRequiredId<Infer<T>>[], 79 ); 80 } 81 82 async find(query: Filter<Infer<T>>): Promise<(WithId<Infer<T>>)[]> { 83 return await this.collection.find(query).toArray(); 84 } 85 86 async findOne(query: Filter<Infer<T>>): Promise<WithId<Infer<T>> | null> { 87 return await this.collection.findOne(query); 88 } 89 90 async findById(id: string | ObjectId): Promise<WithId<Infer<T>> | null> { 91 const objectId = typeof id === "string" ? new ObjectId(id) : id; 92 return await this.findOne({ _id: objectId } as Filter<Infer<T>>); 93 } 94 95 async update( 96 query: Filter<Infer<T>>, 97 data: Partial<Infer<T>>, 98 ): Promise<UpdateResult> { 99 const validatedData = parsePartial(this.schema, data); 100 return await this.collection.updateMany(query, { $set: validatedData }); 101 } 102 103 async updateOne( 104 query: Filter<Infer<T>>, 105 data: Partial<Infer<T>>, 106 ): Promise<UpdateResult> { 107 const validatedData = parsePartial(this.schema, data); 108 return await this.collection.updateOne(query, { $set: validatedData }); 109 } 110 111 async replaceOne( 112 query: Filter<Infer<T>>, 113 data: Input<T>, 114 ): Promise<UpdateResult> { 115 const validatedData = parse(this.schema, data); 116 return await this.collection.replaceOne( 117 query, 118 validatedData as OptionalUnlessRequiredId<Infer<T>>, 119 ); 120 } 121 122 async delete(query: Filter<Infer<T>>): Promise<DeleteResult> { 123 return await this.collection.deleteMany(query); 124 } 125 126 async deleteOne(query: Filter<Infer<T>>): Promise<DeleteResult> { 127 return await this.collection.deleteOne(query); 128 } 129 130 async count(query: Filter<Infer<T>>): Promise<number> { 131 return await this.collection.countDocuments(query); 132 } 133 134 async aggregate(pipeline: Document[]): Promise<Document[]> { 135 return await this.collection.aggregate(pipeline).toArray(); 136 } 137 138 // Pagination support for find 139 async findPaginated( 140 query: Filter<Infer<T>>, 141 options: { skip?: number; limit?: number; sort?: Document } = {}, 142 ): Promise<(WithId<Infer<T>>)[]> { 143 return await this.collection 144 .find(query) 145 .skip(options.skip ?? 0) 146 .limit(options.limit ?? 10) 147 .sort(options.sort ?? {}) 148 .toArray(); 149 } 150}