Thin MongoDB ODM built for Standard Schema
mongodb zod deno
at main 12 kB view raw
1import type { z } from "@zod/zod"; 2import type { 3 AggregateOptions, 4 BulkWriteOptions, 5 Collection, 6 CountDocumentsOptions, 7 CreateIndexesOptions, 8 DeleteOptions, 9 DeleteResult, 10 Document, 11 DropIndexesOptions, 12 Filter, 13 FindOneAndReplaceOptions, 14 FindOneAndUpdateOptions, 15 FindOptions, 16 IndexDescription, 17 IndexSpecification, 18 InsertManyResult, 19 InsertOneOptions, 20 InsertOneResult, 21 ListIndexesOptions, 22 ModifyResult, 23 ReplaceOptions, 24 UpdateOptions, 25 UpdateResult, 26 WithId, 27} from "mongodb"; 28import type { ObjectId } from "mongodb"; 29import { getDb } from "../client/connection.ts"; 30import type { Indexes, Infer, Input, ModelDef, Schema } from "../types.ts"; 31import * as core from "./core.ts"; 32import * as indexes from "./indexes.ts"; 33import * as pagination from "./pagination.ts"; 34 35/** 36 * Model class for type-safe MongoDB operations 37 * 38 * Provides a clean API for CRUD operations, pagination, and index management 39 * with automatic Zod validation and TypeScript type safety. 40 * 41 * @example 42 * ```ts 43 * const userSchema = z.object({ 44 * name: z.string(), 45 * email: z.string().email(), 46 * }); 47 * 48 * const UserModel = new Model("users", userSchema); 49 * await UserModel.insertOne({ name: "Alice", email: "alice@example.com" }); 50 * ``` 51 */ 52export class Model<T extends Schema> { 53 private collection: Collection<Infer<T>>; 54 private schema: T; 55 private indexes?: Indexes; 56 57 constructor(collectionName: string, definition: ModelDef<T> | T) { 58 if ("schema" in definition) { 59 this.schema = definition.schema; 60 this.indexes = definition.indexes; 61 } else { 62 this.schema = definition as T; 63 } 64 this.collection = getDb().collection<Infer<T>>(collectionName); 65 66 // Automatically create indexes if they were provided 67 if (this.indexes && this.indexes.length > 0) { 68 // Fire and forget - indexes will be created asynchronously 69 indexes.syncIndexes(this.collection, this.indexes); 70 } 71 } 72 73 // ============================================================================ 74 // CRUD Operations (delegated to core.ts) 75 // ============================================================================ 76 77 /** 78 * Insert a single document into the collection 79 * 80 * @param data - Document data to insert 81 * @param options - Insert options (including session for transactions) 82 * @returns Insert result with insertedId 83 */ 84 async insertOne( 85 data: Input<T>, 86 options?: InsertOneOptions, 87 ): Promise<InsertOneResult<Infer<T>>> { 88 return await core.insertOne(this.collection, this.schema, data, options); 89 } 90 91 /** 92 * Insert multiple documents into the collection 93 * 94 * @param data - Array of document data to insert 95 * @param options - Insert options (including session for transactions) 96 * @returns Insert result with insertedIds 97 */ 98 async insertMany( 99 data: Input<T>[], 100 options?: BulkWriteOptions, 101 ): Promise<InsertManyResult<Infer<T>>> { 102 return await core.insertMany(this.collection, this.schema, data, options); 103 } 104 105 /** 106 * Find multiple documents matching the query 107 * 108 * @param query - MongoDB query filter 109 * @param options - Find options (including session for transactions) 110 * @returns Array of matching documents 111 */ 112 async find( 113 query: Filter<Infer<T>>, 114 options?: FindOptions, 115 ): Promise<(WithId<Infer<T>>)[]> { 116 return await core.find(this.collection, query, options); 117 } 118 119 /** 120 * Find a single document matching the query 121 * 122 * @param query - MongoDB query filter 123 * @param options - Find options (including session for transactions) 124 * @returns Matching document or null if not found 125 */ 126 async findOne( 127 query: Filter<Infer<T>>, 128 options?: FindOptions, 129 ): Promise<WithId<Infer<T>> | null> { 130 return await core.findOne(this.collection, query, options); 131 } 132 133 /** 134 * Find a document by its MongoDB ObjectId 135 * 136 * @param id - Document ID (string or ObjectId) 137 * @param options - Find options (including session for transactions) 138 * @returns Matching document or null if not found 139 */ 140 async findById( 141 id: string | ObjectId, 142 options?: FindOptions, 143 ): Promise<WithId<Infer<T>> | null> { 144 return await core.findById(this.collection, id, options); 145 } 146 147 /** 148 * Update multiple documents matching the query 149 * 150 * @param query - MongoDB query filter 151 * @param data - Partial data to update 152 * @param options - Update options (including session for transactions) 153 * @returns Update result 154 */ 155 async update( 156 query: Filter<Infer<T>>, 157 data: Partial<z.infer<T>>, 158 options?: UpdateOptions, 159 ): Promise<UpdateResult<Infer<T>>> { 160 return await core.update( 161 this.collection, 162 this.schema, 163 query, 164 data, 165 options, 166 ); 167 } 168 169 /** 170 * Update a single document matching the query 171 * 172 * @param query - MongoDB query filter 173 * @param data - Partial data to update 174 * @param options - Update options (including session for transactions) 175 * @returns Update result 176 */ 177 async updateOne( 178 query: Filter<Infer<T>>, 179 data: Partial<z.infer<T>>, 180 options?: UpdateOptions, 181 ): Promise<UpdateResult<Infer<T>>> { 182 return await core.updateOne( 183 this.collection, 184 this.schema, 185 query, 186 data, 187 options, 188 ); 189 } 190 191 /** 192 * Find a single document and update it 193 * 194 * @param query - MongoDB query filter 195 * @param data - Partial data to update 196 * @param options - FindOneAndUpdate options (including upsert and returnDocument) 197 * @returns Modify result containing the matched document 198 */ 199 async findOneAndUpdate( 200 query: Filter<Infer<T>>, 201 data: Partial<z.infer<T>>, 202 options?: FindOneAndUpdateOptions, 203 ): Promise<ModifyResult<Infer<T>>> { 204 return await core.findOneAndUpdate( 205 this.collection, 206 this.schema, 207 query, 208 data, 209 options, 210 ); 211 } 212 213 /** 214 * Replace a single document matching the query 215 * 216 * @param query - MongoDB query filter 217 * @param data - Complete document data for replacement 218 * @param options - Replace options (including session for transactions) 219 * @returns Update result 220 */ 221 async replaceOne( 222 query: Filter<Infer<T>>, 223 data: Input<T>, 224 options?: ReplaceOptions, 225 ): Promise<UpdateResult<Infer<T>>> { 226 return await core.replaceOne( 227 this.collection, 228 this.schema, 229 query, 230 data, 231 options, 232 ); 233 } 234 235 /** 236 * Find a single document and replace it 237 * 238 * @param query - MongoDB query filter 239 * @param data - Complete document data for replacement 240 * @param options - FindOneAndReplace options (including upsert and returnDocument) 241 * @returns Modify result containing the matched document 242 */ 243 async findOneAndReplace( 244 query: Filter<Infer<T>>, 245 data: Input<T>, 246 options?: FindOneAndReplaceOptions, 247 ): Promise<ModifyResult<Infer<T>>> { 248 return await core.findOneAndReplace( 249 this.collection, 250 this.schema, 251 query, 252 data, 253 options, 254 ); 255 } 256 257 /** 258 * Delete multiple documents matching the query 259 * 260 * @param query - MongoDB query filter 261 * @param options - Delete options (including session for transactions) 262 * @returns Delete result 263 */ 264 async delete( 265 query: Filter<Infer<T>>, 266 options?: DeleteOptions, 267 ): Promise<DeleteResult> { 268 return await core.deleteMany(this.collection, query, options); 269 } 270 271 /** 272 * Delete a single document matching the query 273 * 274 * @param query - MongoDB query filter 275 * @param options - Delete options (including session for transactions) 276 * @returns Delete result 277 */ 278 async deleteOne( 279 query: Filter<Infer<T>>, 280 options?: DeleteOptions, 281 ): Promise<DeleteResult> { 282 return await core.deleteOne(this.collection, query, options); 283 } 284 285 /** 286 * Count documents matching the query 287 * 288 * @param query - MongoDB query filter 289 * @param options - Count options (including session for transactions) 290 * @returns Number of matching documents 291 */ 292 async count( 293 query: Filter<Infer<T>>, 294 options?: CountDocumentsOptions, 295 ): Promise<number> { 296 return await core.count(this.collection, query, options); 297 } 298 299 /** 300 * Execute an aggregation pipeline 301 * 302 * @param pipeline - MongoDB aggregation pipeline 303 * @param options - Aggregate options (including session for transactions) 304 * @returns Array of aggregation results 305 */ 306 async aggregate( 307 pipeline: Document[], 308 options?: AggregateOptions, 309 ): Promise<Document[]> { 310 return await core.aggregate(this.collection, pipeline, options); 311 } 312 313 // ============================================================================ 314 // Pagination (delegated to pagination.ts) 315 // ============================================================================ 316 317 /** 318 * Find documents with pagination support 319 * 320 * @param query - MongoDB query filter 321 * @param options - Pagination options (skip, limit, sort) 322 * @returns Array of matching documents 323 */ 324 async findPaginated( 325 query: Filter<Infer<T>>, 326 options: { skip?: number; limit?: number; sort?: Document } = {}, 327 ): Promise<(WithId<Infer<T>>)[]> { 328 return await pagination.findPaginated(this.collection, query, options); 329 } 330 331 // ============================================================================ 332 // Index Management (delegated to indexes.ts) 333 // ============================================================================ 334 335 /** 336 * Create a single index on the collection 337 * 338 * @param keys - Index specification (e.g., { email: 1 } or { name: "text" }) 339 * @param options - Index creation options (unique, sparse, expireAfterSeconds, etc.) 340 * @returns The name of the created index 341 */ 342 async createIndex( 343 keys: IndexSpecification, 344 options?: CreateIndexesOptions, 345 ): Promise<string> { 346 return await indexes.createIndex(this.collection, keys, options); 347 } 348 349 /** 350 * Create multiple indexes on the collection 351 * 352 * @param indexes - Array of index descriptions 353 * @param options - Index creation options 354 * @returns Array of index names created 355 */ 356 async createIndexes( 357 indexList: IndexDescription[], 358 options?: CreateIndexesOptions, 359 ): Promise<string[]> { 360 return await indexes.createIndexes(this.collection, indexList, options); 361 } 362 363 /** 364 * Drop a single index from the collection 365 * 366 * @param index - Index name or specification 367 * @param options - Drop index options 368 */ 369 async dropIndex( 370 index: string | IndexSpecification, 371 options?: DropIndexesOptions, 372 ): Promise<void> { 373 return await indexes.dropIndex(this.collection, index, options); 374 } 375 376 /** 377 * Drop all indexes from the collection (except _id index) 378 * 379 * @param options - Drop index options 380 */ 381 async dropIndexes(options?: DropIndexesOptions): Promise<void> { 382 return await indexes.dropIndexes(this.collection, options); 383 } 384 385 /** 386 * List all indexes on the collection 387 * 388 * @param options - List indexes options 389 * @returns Array of index information 390 */ 391 async listIndexes( 392 options?: ListIndexesOptions, 393 ): Promise<IndexDescription[]> { 394 return await indexes.listIndexes(this.collection, options); 395 } 396 397 /** 398 * Get index information by name 399 * 400 * @param indexName - Name of the index 401 * @returns Index description or null if not found 402 */ 403 async getIndex(indexName: string): Promise<IndexDescription | null> { 404 return await indexes.getIndex(this.collection, indexName); 405 } 406 407 /** 408 * Check if an index exists 409 * 410 * @param indexName - Name of the index 411 * @returns True if index exists, false otherwise 412 */ 413 async indexExists(indexName: string): Promise<boolean> { 414 return await indexes.indexExists(this.collection, indexName); 415 } 416 417 /** 418 * Synchronize indexes - create indexes if they don't exist, update if they differ 419 * 420 * This is useful for ensuring indexes match your schema definition 421 * 422 * @param indexes - Array of index descriptions to synchronize 423 * @param options - Options for index creation 424 * @returns Array of index names that were created 425 */ 426 async syncIndexes( 427 indexList: IndexDescription[], 428 options?: CreateIndexesOptions, 429 ): Promise<string[]> { 430 return await indexes.syncIndexes(this.collection, indexList, options); 431 } 432}