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