Thin MongoDB ODM built for Standard Schema
mongodb
zod
deno
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}