Thin MongoDB ODM built for Standard Schema
mongodb zod deno
at main 11 kB view raw
1import type { z } from "@zod/zod"; 2import type { 3 AggregateOptions, 4 BulkWriteOptions, 5 Collection, 6 CountDocumentsOptions, 7 DeleteOptions, 8 DeleteResult, 9 Document, 10 Filter, 11 FindOneAndReplaceOptions, 12 FindOneAndUpdateOptions, 13 FindOptions, 14 InsertManyResult, 15 InsertOneOptions, 16 InsertOneResult, 17 ModifyResult, 18 OptionalUnlessRequiredId, 19 ReplaceOptions, 20 UpdateFilter, 21 UpdateOptions, 22 UpdateResult, 23 WithId, 24} from "mongodb"; 25import { ObjectId } from "mongodb"; 26import type { Infer, Input, Schema } from "../types.ts"; 27import { 28 applyDefaultsForUpsert, 29 parse, 30 parsePartial, 31 parseReplace, 32} from "./validation.ts"; 33 34/** 35 * Core CRUD operations for the Model class 36 * 37 * This module contains all basic create, read, update, and delete operations 38 * with automatic Zod validation and transaction support. 39 */ 40 41/** 42 * Insert a single document into the collection 43 * 44 * @param collection - MongoDB collection 45 * @param schema - Zod schema for validation 46 * @param data - Document data to insert 47 * @param options - Insert options (including session for transactions) 48 * @returns Insert result with insertedId 49 */ 50export async function insertOne<T extends Schema>( 51 collection: Collection<Infer<T>>, 52 schema: T, 53 data: Input<T>, 54 options?: InsertOneOptions, 55): Promise<InsertOneResult<Infer<T>>> { 56 const validatedData = parse(schema, data); 57 return await collection.insertOne( 58 validatedData as OptionalUnlessRequiredId<Infer<T>>, 59 options, 60 ); 61} 62 63/** 64 * Insert multiple documents into the collection 65 * 66 * @param collection - MongoDB collection 67 * @param schema - Zod schema for validation 68 * @param data - Array of document data to insert 69 * @param options - Insert options (including session for transactions) 70 * @returns Insert result with insertedIds 71 */ 72export async function insertMany<T extends Schema>( 73 collection: Collection<Infer<T>>, 74 schema: T, 75 data: Input<T>[], 76 options?: BulkWriteOptions, 77): Promise<InsertManyResult<Infer<T>>> { 78 const validatedData = data.map((item) => parse(schema, item)); 79 return await collection.insertMany( 80 validatedData as OptionalUnlessRequiredId<Infer<T>>[], 81 options, 82 ); 83} 84 85/** 86 * Find multiple documents matching the query 87 * 88 * @param collection - MongoDB collection 89 * @param query - MongoDB query filter 90 * @param options - Find options (including session for transactions) 91 * @returns Array of matching documents 92 */ 93export async function find<T extends Schema>( 94 collection: Collection<Infer<T>>, 95 query: Filter<Infer<T>>, 96 options?: FindOptions, 97): Promise<(WithId<Infer<T>>)[]> { 98 return await collection.find(query, options).toArray(); 99} 100 101/** 102 * Find a single document matching the query 103 * 104 * @param collection - MongoDB collection 105 * @param query - MongoDB query filter 106 * @param options - Find options (including session for transactions) 107 * @returns Matching document or null if not found 108 */ 109export async function findOne<T extends Schema>( 110 collection: Collection<Infer<T>>, 111 query: Filter<Infer<T>>, 112 options?: FindOptions, 113): Promise<WithId<Infer<T>> | null> { 114 return await collection.findOne(query, options); 115} 116 117/** 118 * Find a document by its MongoDB ObjectId 119 * 120 * @param collection - MongoDB collection 121 * @param id - Document ID (string or ObjectId) 122 * @param options - Find options (including session for transactions) 123 * @returns Matching document or null if not found 124 */ 125export async function findById<T extends Schema>( 126 collection: Collection<Infer<T>>, 127 id: string | ObjectId, 128 options?: FindOptions, 129): Promise<WithId<Infer<T>> | null> { 130 const objectId = typeof id === "string" ? new ObjectId(id) : id; 131 return await findOne( 132 collection, 133 { _id: objectId } as Filter<Infer<T>>, 134 options, 135 ); 136} 137 138/** 139 * Update multiple documents matching the query 140 * 141 * Case handling: 142 * - If upsert: false (or undefined) → Normal update, no defaults applied 143 * - If upsert: true → Defaults added to $setOnInsert for new document creation 144 * 145 * @param collection - MongoDB collection 146 * @param schema - Zod schema for validation 147 * @param query - MongoDB query filter 148 * @param data - Partial data to update 149 * @param options - Update options (including session for transactions and upsert flag) 150 * @returns Update result 151 */ 152export async function update<T extends Schema>( 153 collection: Collection<Infer<T>>, 154 schema: T, 155 query: Filter<Infer<T>>, 156 data: Partial<z.infer<T>>, 157 options?: UpdateOptions, 158): Promise<UpdateResult<Infer<T>>> { 159 const validatedData = parsePartial(schema, data); 160 let updateDoc: UpdateFilter<Infer<T>> = { 161 $set: validatedData as Partial<Infer<T>>, 162 }; 163 164 // If this is an upsert, apply defaults using $setOnInsert 165 if (options?.upsert) { 166 updateDoc = applyDefaultsForUpsert(schema, query, updateDoc); 167 } 168 169 return await collection.updateMany(query, updateDoc, options); 170} 171 172/** 173 * Update a single document matching the query 174 * 175 * Case handling: 176 * - If upsert: false (or undefined) → Normal update, no defaults applied 177 * - If upsert: true → Defaults added to $setOnInsert for new document creation 178 * 179 * @param collection - MongoDB collection 180 * @param schema - Zod schema for validation 181 * @param query - MongoDB query filter 182 * @param data - Partial data to update 183 * @param options - Update options (including session for transactions and upsert flag) 184 * @returns Update result 185 */ 186export async function updateOne<T extends Schema>( 187 collection: Collection<Infer<T>>, 188 schema: T, 189 query: Filter<Infer<T>>, 190 data: Partial<z.infer<T>>, 191 options?: UpdateOptions, 192): Promise<UpdateResult<Infer<T>>> { 193 const validatedData = parsePartial(schema, data); 194 let updateDoc: UpdateFilter<Infer<T>> = { 195 $set: validatedData as Partial<Infer<T>>, 196 }; 197 198 // If this is an upsert, apply defaults using $setOnInsert 199 if (options?.upsert) { 200 updateDoc = applyDefaultsForUpsert(schema, query, updateDoc); 201 } 202 203 return await collection.updateOne(query, updateDoc, options); 204} 205 206/** 207 * Replace a single document matching the query 208 * 209 * Case handling: 210 * - If upsert: false (or undefined) → Normal replace on existing doc, no additional defaults 211 * - If upsert: true → Defaults applied via parse() since we're passing a full document 212 * 213 * Note: For replace operations, defaults are automatically applied by the schema's 214 * parse() function which treats missing fields as candidates for defaults. This works 215 * for both regular replaces and upsert-creates since we're providing a full document. 216 * 217 * @param collection - MongoDB collection 218 * @param schema - Zod schema for validation 219 * @param query - MongoDB query filter 220 * @param data - Complete document data for replacement 221 * @param options - Replace options (including session for transactions and upsert flag) 222 * @returns Update result 223 */ 224export async function replaceOne<T extends Schema>( 225 collection: Collection<Infer<T>>, 226 schema: T, 227 query: Filter<Infer<T>>, 228 data: Input<T>, 229 options?: ReplaceOptions, 230): Promise<UpdateResult<Infer<T>>> { 231 // parseReplace will apply all schema defaults to missing fields 232 // This works correctly for both regular replaces and upsert-created documents 233 const validatedData = parseReplace(schema, data); 234 235 // Remove _id from validatedData for replaceOne (it will use the query's _id) 236 const { _id, ...withoutId } = validatedData as Infer<T> & { _id?: unknown }; 237 return await collection.replaceOne( 238 query, 239 withoutId as Infer<T>, 240 options, 241 ); 242} 243 244/** 245 * Find a single document and update it 246 * 247 * Case handling: 248 * - If upsert: false (or undefined) → Normal update 249 * - If upsert: true → Defaults added to $setOnInsert for new document creation 250 */ 251export async function findOneAndUpdate<T extends Schema>( 252 collection: Collection<Infer<T>>, 253 schema: T, 254 query: Filter<Infer<T>>, 255 data: Partial<z.infer<T>>, 256 options?: FindOneAndUpdateOptions, 257): Promise<ModifyResult<Infer<T>>> { 258 const validatedData = parsePartial(schema, data); 259 let updateDoc: UpdateFilter<Infer<T>> = { 260 $set: validatedData as Partial<Infer<T>>, 261 }; 262 263 if (options?.upsert) { 264 updateDoc = applyDefaultsForUpsert(schema, query, updateDoc); 265 } 266 267 const resolvedOptions: FindOneAndUpdateOptions & { 268 includeResultMetadata: true; 269 } = { 270 ...(options ?? {}), 271 includeResultMetadata: true as const, 272 }; 273 274 return await collection.findOneAndUpdate(query, updateDoc, resolvedOptions); 275} 276 277/** 278 * Find a single document and replace it 279 * 280 * Defaults are applied via parseReplace(), which fills in missing fields 281 * for both normal replacements and upsert-created documents. 282 */ 283export async function findOneAndReplace<T extends Schema>( 284 collection: Collection<Infer<T>>, 285 schema: T, 286 query: Filter<Infer<T>>, 287 data: Input<T>, 288 options?: FindOneAndReplaceOptions, 289): Promise<ModifyResult<Infer<T>>> { 290 const validatedData = parseReplace(schema, data); 291 const { _id, ...withoutId } = validatedData as Infer<T> & { _id?: unknown }; 292 293 const resolvedOptions: FindOneAndReplaceOptions & { 294 includeResultMetadata: true; 295 } = { 296 ...(options ?? {}), 297 includeResultMetadata: true as const, 298 }; 299 300 return await collection.findOneAndReplace( 301 query, 302 withoutId as Infer<T>, 303 resolvedOptions, 304 ); 305} 306 307/** 308 * Delete multiple documents matching the query 309 * 310 * @param collection - MongoDB collection 311 * @param query - MongoDB query filter 312 * @param options - Delete options (including session for transactions) 313 * @returns Delete result 314 */ 315export async function deleteMany<T extends Schema>( 316 collection: Collection<Infer<T>>, 317 query: Filter<Infer<T>>, 318 options?: DeleteOptions, 319): Promise<DeleteResult> { 320 return await collection.deleteMany(query, options); 321} 322 323/** 324 * Delete a single document matching the query 325 * 326 * @param collection - MongoDB collection 327 * @param query - MongoDB query filter 328 * @param options - Delete options (including session for transactions) 329 * @returns Delete result 330 */ 331export async function deleteOne<T extends Schema>( 332 collection: Collection<Infer<T>>, 333 query: Filter<Infer<T>>, 334 options?: DeleteOptions, 335): Promise<DeleteResult> { 336 return await collection.deleteOne(query, options); 337} 338 339/** 340 * Count documents matching the query 341 * 342 * @param collection - MongoDB collection 343 * @param query - MongoDB query filter 344 * @param options - Count options (including session for transactions) 345 * @returns Number of matching documents 346 */ 347export async function count<T extends Schema>( 348 collection: Collection<Infer<T>>, 349 query: Filter<Infer<T>>, 350 options?: CountDocumentsOptions, 351): Promise<number> { 352 return await collection.countDocuments(query, options); 353} 354 355/** 356 * Execute an aggregation pipeline 357 * 358 * @param collection - MongoDB collection 359 * @param pipeline - MongoDB aggregation pipeline 360 * @param options - Aggregate options (including session for transactions) 361 * @returns Array of aggregation results 362 */ 363export async function aggregate<T extends Schema>( 364 collection: Collection<Infer<T>>, 365 pipeline: Document[], 366 options?: AggregateOptions, 367): Promise<Document[]> { 368 return await collection.aggregate(pipeline, options).toArray(); 369}