···11-import {
22- type Db,
33- type MongoClientOptions,
44- type ClientSession,
55- type TransactionOptions,
66- MongoClient
77-} from "mongodb";
88-import { ConnectionError } from "./errors.ts";
99-1010-interface Connection {
1111- client: MongoClient;
1212- db: Db;
1313-}
1414-1515-let connection: Connection | null = null;
1616-1717-export interface ConnectOptions extends MongoClientOptions {};
1818-1919-/**
2020- * Health check details of the MongoDB connection
2121- *
2222- * @property healthy - Overall health status of the connection
2323- * @property connected - Whether a connection is established
2424- * @property responseTimeMs - Response time in milliseconds (if connection is healthy)
2525- * @property error - Error message if health check failed
2626- * @property timestamp - Timestamp when health check was performed
2727- */
2828-export interface HealthCheckResult {
2929- healthy: boolean;
3030- connected: boolean;
3131- responseTimeMs?: number;
3232- error?: string;
3333- timestamp: Date;
3434-}
3535-3636-/**
3737- * Connect to MongoDB with connection pooling, retry logic, and resilience options
3838- *
3939- * The MongoDB driver handles connection pooling and automatic retries.
4040- * Retry logic is enabled by default for both reads and writes in MongoDB 4.2+.
4141- *
4242- * @param uri - MongoDB connection string
4343- * @param dbName - Name of the database to connect to
4444- * @param options - Connection options (pooling, retries, timeouts, etc.)
4545- *
4646- * @example
4747- * Basic connection with pooling:
4848- * ```ts
4949- * await connect("mongodb://localhost:27017", "mydb", {
5050- * maxPoolSize: 10,
5151- * minPoolSize: 2,
5252- * maxIdleTimeMS: 30000,
5353- * connectTimeoutMS: 10000,
5454- * socketTimeoutMS: 45000,
5555- * });
5656- * ```
5757- *
5858- * @example
5959- * Production-ready connection with retry logic and resilience:
6060- * ```ts
6161- * await connect("mongodb://localhost:27017", "mydb", {
6262- * // Connection pooling
6363- * maxPoolSize: 10,
6464- * minPoolSize: 2,
6565- *
6666- * // Automatic retry logic (enabled by default)
6767- * retryReads: true, // Retry failed read operations
6868- * retryWrites: true, // Retry failed write operations
6969- *
7070- * // Timeouts
7171- * connectTimeoutMS: 10000, // Initial connection timeout
7272- * socketTimeoutMS: 45000, // Socket operation timeout
7373- * serverSelectionTimeoutMS: 10000, // Server selection timeout
7474- *
7575- * // Connection resilience
7676- * maxIdleTimeMS: 30000, // Close idle connections
7777- * heartbeatFrequencyMS: 10000, // Server health check interval
7878- *
7979- * // Optional: Compression for reduced bandwidth
8080- * compressors: ['snappy', 'zlib'],
8181- * });
8282- * ```
8383- */
8484-export async function connect(
8585- uri: string,
8686- dbName: string,
8787- options?: ConnectOptions,
8888-): Promise<Connection> {
8989- if (connection) {
9090- return connection;
9191- }
9292-9393- try {
9494- const client = new MongoClient(uri, options);
9595- await client.connect();
9696- const db = client.db(dbName);
9797-9898- connection = { client, db };
9999- return connection;
100100- } catch (error) {
101101- throw new ConnectionError(
102102- `Failed to connect to MongoDB: ${error instanceof Error ? error.message : String(error)}`,
103103- uri
104104- );
105105- }
106106-}
107107-108108-export async function disconnect(): Promise<void> {
109109- if (connection) {
110110- await connection.client.close();
111111- connection = null;
112112- }
113113-}
114114-115115-/**
116116- * Start a new client session for transactions
117117- *
118118- * Sessions must be ended when done using `endSession()`
119119- *
120120- * @example
121121- * ```ts
122122- * const session = await startSession();
123123- * try {
124124- * // use session
125125- * } finally {
126126- * await endSession(session);
127127- * }
128128- * ```
129129- */
130130-export function startSession(): ClientSession {
131131- if (!connection) {
132132- throw new ConnectionError("MongoDB not connected. Call connect() first.");
133133- }
134134- return connection.client.startSession();
135135-}
136136-137137-/**
138138- * End a client session
139139- *
140140- * @param session - The session to end
141141- */
142142-export async function endSession(session: ClientSession): Promise<void> {
143143- await session.endSession();
144144-}
145145-146146-/**
147147- * Execute a function within a transaction
148148- *
149149- * Automatically handles session creation, transaction start/commit/abort, and cleanup.
150150- * If the callback throws an error, the transaction is automatically aborted.
151151- *
152152- * @param callback - Async function to execute within the transaction. Receives the session as parameter.
153153- * @param options - Optional transaction options (read/write concern, etc.)
154154- * @returns The result from the callback function
155155- *
156156- * @example
157157- * ```ts
158158- * const result = await withTransaction(async (session) => {
159159- * await UserModel.insertOne({ name: "Alice" }, { session });
160160- * await OrderModel.insertOne({ userId: "123", total: 100 }, { session });
161161- * return { success: true };
162162- * });
163163- * ```
164164- */
165165-export async function withTransaction<T>(
166166- callback: (session: ClientSession) => Promise<T>,
167167- options?: TransactionOptions
168168-): Promise<T> {
169169- const session = await startSession();
170170-171171- try {
172172- let result: T;
173173-174174- await session.withTransaction(async () => {
175175- result = await callback(session);
176176- }, options);
177177-178178- return result!;
179179- } finally {
180180- await endSession(session);
181181- }
182182-}
183183-184184-export function getDb(): Db {
185185- if (!connection) {
186186- throw new ConnectionError("MongoDB not connected. Call connect() first.");
187187- }
188188- return connection.db;
189189-}
190190-191191-/**
192192- * Check the health of the MongoDB connection
193193- *
194194- * Performs a ping operation to verify the database is responsive
195195- * and returns detailed health information including response time.
196196- *
197197- * @example
198198- * ```ts
199199- * const health = await healthCheck();
200200- * if (health.healthy) {
201201- * console.log(`Database healthy (${health.responseTimeMs}ms)`);
202202- * } else {
203203- * console.error(`Database unhealthy: ${health.error}`);
204204- * }
205205- * ```
206206- */
207207-export async function healthCheck(): Promise<HealthCheckResult> {
208208- const timestamp = new Date();
209209-210210- // Check if connection exists
211211- if (!connection) {
212212- return {
213213- healthy: false,
214214- connected: false,
215215- error: "No active connection. Call connect() first.",
216216- timestamp,
217217- };
218218- }
219219-220220- try {
221221- // Measure ping response time
222222- const startTime = performance.now();
223223- await connection.db.admin().ping();
224224- const endTime = performance.now();
225225- const responseTimeMs = Math.round(endTime - startTime);
226226-227227- return {
228228- healthy: true,
229229- connected: true,
230230- responseTimeMs,
231231- timestamp,
232232- };
233233- } catch (error) {
234234- return {
235235- healthy: false,
236236- connected: true,
237237- error: error instanceof Error ? error.message : String(error),
238238- timestamp,
239239- };
240240- }
241241-}
+126
client/connection.ts
···11+import { type Db, type MongoClientOptions, MongoClient } from "mongodb";
22+import { ConnectionError } from "../errors.ts";
33+44+/**
55+ * Connection management module
66+ *
77+ * Handles MongoDB connection lifecycle including connect, disconnect,
88+ * and connection state management.
99+ */
1010+1111+export interface Connection {
1212+ client: MongoClient;
1313+ db: Db;
1414+}
1515+1616+export interface ConnectOptions extends MongoClientOptions {}
1717+1818+// Singleton connection state
1919+let connection: Connection | null = null;
2020+2121+/**
2222+ * Connect to MongoDB with connection pooling, retry logic, and resilience options
2323+ *
2424+ * The MongoDB driver handles connection pooling and automatic retries.
2525+ * Retry logic is enabled by default for both reads and writes in MongoDB 4.2+.
2626+ *
2727+ * @param uri - MongoDB connection string
2828+ * @param dbName - Name of the database to connect to
2929+ * @param options - Connection options (pooling, retries, timeouts, etc.)
3030+ * @returns Connection object with client and db
3131+ *
3232+ * @example
3333+ * Basic connection with pooling:
3434+ * ```ts
3535+ * await connect("mongodb://localhost:27017", "mydb", {
3636+ * maxPoolSize: 10,
3737+ * minPoolSize: 2,
3838+ * maxIdleTimeMS: 30000,
3939+ * connectTimeoutMS: 10000,
4040+ * socketTimeoutMS: 45000,
4141+ * });
4242+ * ```
4343+ *
4444+ * @example
4545+ * Production-ready connection with retry logic and resilience:
4646+ * ```ts
4747+ * await connect("mongodb://localhost:27017", "mydb", {
4848+ * // Connection pooling
4949+ * maxPoolSize: 10,
5050+ * minPoolSize: 2,
5151+ *
5252+ * // Automatic retry logic (enabled by default)
5353+ * retryReads: true, // Retry failed read operations
5454+ * retryWrites: true, // Retry failed write operations
5555+ *
5656+ * // Timeouts
5757+ * connectTimeoutMS: 10000, // Initial connection timeout
5858+ * socketTimeoutMS: 45000, // Socket operation timeout
5959+ * serverSelectionTimeoutMS: 10000, // Server selection timeout
6060+ *
6161+ * // Connection resilience
6262+ * maxIdleTimeMS: 30000, // Close idle connections
6363+ * heartbeatFrequencyMS: 10000, // Server health check interval
6464+ *
6565+ * // Optional: Compression for reduced bandwidth
6666+ * compressors: ['snappy', 'zlib'],
6767+ * });
6868+ * ```
6969+ */
7070+export async function connect(
7171+ uri: string,
7272+ dbName: string,
7373+ options?: ConnectOptions,
7474+): Promise<Connection> {
7575+ if (connection) {
7676+ return connection;
7777+ }
7878+7979+ try {
8080+ const client = new MongoClient(uri, options);
8181+ await client.connect();
8282+ const db = client.db(dbName);
8383+8484+ connection = { client, db };
8585+ return connection;
8686+ } catch (error) {
8787+ throw new ConnectionError(
8888+ `Failed to connect to MongoDB: ${error instanceof Error ? error.message : String(error)}`,
8989+ uri
9090+ );
9191+ }
9292+}
9393+9494+/**
9595+ * Disconnect from MongoDB and clean up resources
9696+ */
9797+export async function disconnect(): Promise<void> {
9898+ if (connection) {
9999+ await connection.client.close();
100100+ connection = null;
101101+ }
102102+}
103103+104104+/**
105105+ * Get the current database connection
106106+ *
107107+ * @returns MongoDB Db instance
108108+ * @throws {ConnectionError} If not connected
109109+ * @internal
110110+ */
111111+export function getDb(): Db {
112112+ if (!connection) {
113113+ throw new ConnectionError("MongoDB not connected. Call connect() first.");
114114+ }
115115+ return connection.db;
116116+}
117117+118118+/**
119119+ * Get the current connection state
120120+ *
121121+ * @returns Connection object or null if not connected
122122+ * @internal
123123+ */
124124+export function getConnection(): Connection | null {
125125+ return connection;
126126+}
+80
client/health.ts
···11+import { getConnection } from "./connection.ts";
22+33+/**
44+ * Health check module
55+ *
66+ * Provides functionality for monitoring MongoDB connection health
77+ * including ping operations and response time measurement.
88+ */
99+1010+/**
1111+ * Health check details of the MongoDB connection
1212+ *
1313+ * @property healthy - Overall health status of the connection
1414+ * @property connected - Whether a connection is established
1515+ * @property responseTimeMs - Response time in milliseconds (if connection is healthy)
1616+ * @property error - Error message if health check failed
1717+ * @property timestamp - Timestamp when health check was performed
1818+ */
1919+export interface HealthCheckResult {
2020+ healthy: boolean;
2121+ connected: boolean;
2222+ responseTimeMs?: number;
2323+ error?: string;
2424+ timestamp: Date;
2525+}
2626+2727+/**
2828+ * Check the health of the MongoDB connection
2929+ *
3030+ * Performs a ping operation to verify the database is responsive
3131+ * and returns detailed health information including response time.
3232+ *
3333+ * @returns Health check result with status and metrics
3434+ *
3535+ * @example
3636+ * ```ts
3737+ * const health = await healthCheck();
3838+ * if (health.healthy) {
3939+ * console.log(`Database healthy (${health.responseTimeMs}ms)`);
4040+ * } else {
4141+ * console.error(`Database unhealthy: ${health.error}`);
4242+ * }
4343+ * ```
4444+ */
4545+export async function healthCheck(): Promise<HealthCheckResult> {
4646+ const timestamp = new Date();
4747+ const connection = getConnection();
4848+4949+ // Check if connection exists
5050+ if (!connection) {
5151+ return {
5252+ healthy: false,
5353+ connected: false,
5454+ error: "No active connection. Call connect() first.",
5555+ timestamp,
5656+ };
5757+ }
5858+5959+ try {
6060+ // Measure ping response time
6161+ const startTime = performance.now();
6262+ await connection.db.admin().ping();
6363+ const endTime = performance.now();
6464+ const responseTimeMs = Math.round(endTime - startTime);
6565+6666+ return {
6767+ healthy: true,
6868+ connected: true,
6969+ responseTimeMs,
7070+ timestamp,
7171+ };
7272+ } catch (error) {
7373+ return {
7474+ healthy: false,
7575+ connected: true,
7676+ error: error instanceof Error ? error.message : String(error),
7777+ timestamp,
7878+ };
7979+ }
8080+}
+30
client/index.ts
···11+/**
22+ * Client module - MongoDB connection and session management
33+ *
44+ * This module provides all client-level functionality including:
55+ * - Connection management (connect, disconnect)
66+ * - Health monitoring (healthCheck)
77+ * - Transaction support (startSession, endSession, withTransaction)
88+ */
99+1010+// Re-export connection management
1111+export {
1212+ connect,
1313+ disconnect,
1414+ getDb,
1515+ type ConnectOptions,
1616+ type Connection,
1717+} from "./connection.ts";
1818+1919+// Re-export health monitoring
2020+export {
2121+ healthCheck,
2222+ type HealthCheckResult,
2323+} from "./health.ts";
2424+2525+// Re-export transaction management
2626+export {
2727+ startSession,
2828+ endSession,
2929+ withTransaction,
3030+} from "./transactions.ts";
+83
client/transactions.ts
···11+import type { ClientSession, TransactionOptions } from "mongodb";
22+import { getConnection } from "./connection.ts";
33+import { ConnectionError } from "../errors.ts";
44+55+/**
66+ * Transaction management module
77+ *
88+ * Provides session and transaction management functionality including
99+ * automatic transaction handling and manual session control.
1010+ */
1111+1212+/**
1313+ * Start a new client session for transactions
1414+ *
1515+ * Sessions must be ended when done using `endSession()`
1616+ *
1717+ * @returns New MongoDB ClientSession
1818+ * @throws {ConnectionError} If not connected
1919+ *
2020+ * @example
2121+ * ```ts
2222+ * const session = startSession();
2323+ * try {
2424+ * // use session
2525+ * } finally {
2626+ * await endSession(session);
2727+ * }
2828+ * ```
2929+ */
3030+export function startSession(): ClientSession {
3131+ const connection = getConnection();
3232+ if (!connection) {
3333+ throw new ConnectionError("MongoDB not connected. Call connect() first.");
3434+ }
3535+ return connection.client.startSession();
3636+}
3737+3838+/**
3939+ * End a client session
4040+ *
4141+ * @param session - The session to end
4242+ */
4343+export async function endSession(session: ClientSession): Promise<void> {
4444+ await session.endSession();
4545+}
4646+4747+/**
4848+ * Execute a function within a transaction
4949+ *
5050+ * Automatically handles session creation, transaction start/commit/abort, and cleanup.
5151+ * If the callback throws an error, the transaction is automatically aborted.
5252+ *
5353+ * @param callback - Async function to execute within the transaction. Receives the session as parameter.
5454+ * @param options - Optional transaction options (read/write concern, etc.)
5555+ * @returns The result from the callback function
5656+ *
5757+ * @example
5858+ * ```ts
5959+ * const result = await withTransaction(async (session) => {
6060+ * await UserModel.insertOne({ name: "Alice" }, { session });
6161+ * await OrderModel.insertOne({ userId: "123", total: 100 }, { session });
6262+ * return { success: true };
6363+ * });
6464+ * ```
6565+ */
6666+export async function withTransaction<T>(
6767+ callback: (session: ClientSession) => Promise<T>,
6868+ options?: TransactionOptions
6969+): Promise<T> {
7070+ const session = startSession();
7171+7272+ try {
7373+ let result: T;
7474+7575+ await session.withTransaction(async () => {
7676+ result = await callback(session);
7777+ }, options);
7878+7979+ return result!;
8080+ } finally {
8181+ await endSession(session);
8282+ }
8383+}
+3-3
mod.ts
···11-export { type InferModel, type Input } from "./schema.ts";
11+export type { Schema, Infer, Input } from "./types.ts";
22export {
33 connect,
44 disconnect,
···88 withTransaction,
99 type ConnectOptions,
1010 type HealthCheckResult
1111-} from "./client.ts";
1212-export { Model } from "./model.ts";
1111+} from "./client/index.ts";
1212+export { Model } from "./model/index.ts";
1313export {
1414 NozzleError,
1515 ValidationError,
-350
model.ts
···11-import type { z } from "@zod/zod";
22-import type {
33- Collection,
44- CreateIndexesOptions,
55- DeleteResult,
66- Document,
77- DropIndexesOptions,
88- Filter,
99- IndexDescription,
1010- IndexSpecification,
1111- InsertManyResult,
1212- InsertOneResult,
1313- InsertOneOptions,
1414- FindOptions,
1515- UpdateOptions,
1616- ReplaceOptions,
1717- DeleteOptions,
1818- CountDocumentsOptions,
1919- AggregateOptions,
2020- ListIndexesOptions,
2121- OptionalUnlessRequiredId,
2222- UpdateResult,
2323- WithId,
2424- BulkWriteOptions,
2525-} from "mongodb";
2626-import { ObjectId } from "mongodb";
2727-import { getDb } from "./client.ts";
2828-import { ValidationError, AsyncValidationError } from "./errors.ts";
2929-3030-// Type alias for cleaner code - Zod schema
3131-type Schema = z.ZodObject;
3232-type Infer<T extends Schema> = z.infer<T> & Document;
3333-type Input<T extends Schema> = z.input<T>;
3434-3535-// Helper function to validate data using Zod
3636-function parse<T extends Schema>(schema: T, data: Input<T>): Infer<T> {
3737- const result = schema.safeParse(data);
3838-3939- // Check for async validation
4040- if (result instanceof Promise) {
4141- throw new AsyncValidationError();
4242- }
4343-4444- if (!result.success) {
4545- throw new ValidationError(result.error.issues, "insert");
4646- }
4747- return result.data as Infer<T>;
4848-}
4949-5050-// Helper function to validate partial update data using Zod's partial()
5151-function parsePartial<T extends Schema>(
5252- schema: T,
5353- data: Partial<z.infer<T>>,
5454-): Partial<z.infer<T>> {
5555- const result = schema.partial().safeParse(data);
5656-5757- // Check for async validation
5858- if (result instanceof Promise) {
5959- throw new AsyncValidationError();
6060- }
6161-6262- if (!result.success) {
6363- throw new ValidationError(result.error.issues, "update");
6464- }
6565- return result.data as Partial<z.infer<T>>;
6666-}
6767-6868-// Helper function to validate replace data using Zod
6969-function parseReplace<T extends Schema>(schema: T, data: Input<T>): Infer<T> {
7070- const result = schema.safeParse(data);
7171-7272- // Check for async validation
7373- if (result instanceof Promise) {
7474- throw new AsyncValidationError();
7575- }
7676-7777- if (!result.success) {
7878- throw new ValidationError(result.error.issues, "replace");
7979- }
8080- return result.data as Infer<T>;
8181-}
8282-8383-export class Model<T extends Schema> {
8484- private collection: Collection<Infer<T>>;
8585- private schema: T;
8686-8787- constructor(collectionName: string, schema: T) {
8888- this.collection = getDb().collection<Infer<T>>(collectionName);
8989- this.schema = schema;
9090- }
9191-9292- async insertOne(
9393- data: Input<T>,
9494- options?: InsertOneOptions
9595- ): Promise<InsertOneResult<Infer<T>>> {
9696- const validatedData = parse(this.schema, data);
9797- return await this.collection.insertOne(
9898- validatedData as OptionalUnlessRequiredId<Infer<T>>,
9999- options
100100- );
101101- }
102102-103103- async insertMany(
104104- data: Input<T>[],
105105- options?: BulkWriteOptions
106106- ): Promise<InsertManyResult<Infer<T>>> {
107107- const validatedData = data.map((item) => parse(this.schema, item));
108108- return await this.collection.insertMany(
109109- validatedData as OptionalUnlessRequiredId<Infer<T>>[],
110110- options
111111- );
112112- }
113113-114114- async find(
115115- query: Filter<Infer<T>>,
116116- options?: FindOptions
117117- ): Promise<(WithId<Infer<T>>)[]> {
118118- return await this.collection.find(query, options).toArray();
119119- }
120120-121121- async findOne(
122122- query: Filter<Infer<T>>,
123123- options?: FindOptions
124124- ): Promise<WithId<Infer<T>> | null> {
125125- return await this.collection.findOne(query, options);
126126- }
127127-128128- async findById(
129129- id: string | ObjectId,
130130- options?: FindOptions
131131- ): Promise<WithId<Infer<T>> | null> {
132132- const objectId = typeof id === "string" ? new ObjectId(id) : id;
133133- return await this.findOne({ _id: objectId } as Filter<Infer<T>>, options);
134134- }
135135-136136- async update(
137137- query: Filter<Infer<T>>,
138138- data: Partial<z.infer<T>>,
139139- options?: UpdateOptions
140140- ): Promise<UpdateResult<Infer<T>>> {
141141- const validatedData = parsePartial(this.schema, data);
142142- return await this.collection.updateMany(
143143- query,
144144- { $set: validatedData as Partial<Infer<T>> },
145145- options
146146- );
147147- }
148148-149149- async updateOne(
150150- query: Filter<Infer<T>>,
151151- data: Partial<z.infer<T>>,
152152- options?: UpdateOptions
153153- ): Promise<UpdateResult<Infer<T>>> {
154154- const validatedData = parsePartial(this.schema, data);
155155- return await this.collection.updateOne(
156156- query,
157157- { $set: validatedData as Partial<Infer<T>> },
158158- options
159159- );
160160- }
161161-162162- async replaceOne(
163163- query: Filter<Infer<T>>,
164164- data: Input<T>,
165165- options?: ReplaceOptions
166166- ): Promise<UpdateResult<Infer<T>>> {
167167- const validatedData = parseReplace(this.schema, data);
168168- // Remove _id from validatedData for replaceOne (it will use the query's _id)
169169- const { _id, ...withoutId } = validatedData as Infer<T> & { _id?: unknown };
170170- return await this.collection.replaceOne(
171171- query,
172172- withoutId as Infer<T>,
173173- options
174174- );
175175- }
176176-177177- async delete(
178178- query: Filter<Infer<T>>,
179179- options?: DeleteOptions
180180- ): Promise<DeleteResult> {
181181- return await this.collection.deleteMany(query, options);
182182- }
183183-184184- async deleteOne(
185185- query: Filter<Infer<T>>,
186186- options?: DeleteOptions
187187- ): Promise<DeleteResult> {
188188- return await this.collection.deleteOne(query, options);
189189- }
190190-191191- async count(
192192- query: Filter<Infer<T>>,
193193- options?: CountDocumentsOptions
194194- ): Promise<number> {
195195- return await this.collection.countDocuments(query, options);
196196- }
197197-198198- async aggregate(
199199- pipeline: Document[],
200200- options?: AggregateOptions
201201- ): Promise<Document[]> {
202202- return await this.collection.aggregate(pipeline, options).toArray();
203203- }
204204-205205- // Pagination support for find
206206- async findPaginated(
207207- query: Filter<Infer<T>>,
208208- options: { skip?: number; limit?: number; sort?: Document } = {},
209209- ): Promise<(WithId<Infer<T>>)[]> {
210210- return await this.collection
211211- .find(query)
212212- .skip(options.skip ?? 0)
213213- .limit(options.limit ?? 10)
214214- .sort(options.sort ?? {})
215215- .toArray();
216216- }
217217-218218- // Index Management Methods
219219-220220- /**
221221- * Create a single index on the collection
222222- * @param keys - Index specification (e.g., { email: 1 } or { name: "text" })
223223- * @param options - Index creation options (unique, sparse, expireAfterSeconds, etc.)
224224- * @returns The name of the created index
225225- */
226226- async createIndex(
227227- keys: IndexSpecification,
228228- options?: CreateIndexesOptions,
229229- ): Promise<string> {
230230- return await this.collection.createIndex(keys, options);
231231- }
232232-233233- /**
234234- * Create multiple indexes on the collection
235235- * @param indexes - Array of index descriptions
236236- * @param options - Index creation options
237237- * @returns Array of index names created
238238- */
239239- async createIndexes(
240240- indexes: IndexDescription[],
241241- options?: CreateIndexesOptions,
242242- ): Promise<string[]> {
243243- return await this.collection.createIndexes(indexes, options);
244244- }
245245-246246- /**
247247- * Drop a single index from the collection
248248- * @param index - Index name or specification
249249- * @param options - Drop index options
250250- */
251251- async dropIndex(
252252- index: string | IndexSpecification,
253253- options?: DropIndexesOptions,
254254- ): Promise<void> {
255255- // MongoDB driver accepts string or IndexSpecification
256256- await this.collection.dropIndex(index as string, options);
257257- }
258258-259259- /**
260260- * Drop all indexes from the collection (except _id index)
261261- * @param options - Drop index options
262262- */
263263- async dropIndexes(options?: DropIndexesOptions): Promise<void> {
264264- await this.collection.dropIndexes(options);
265265- }
266266-267267- /**
268268- * List all indexes on the collection
269269- * @param options - List indexes options
270270- * @returns Array of index information
271271- */
272272- async listIndexes(
273273- options?: ListIndexesOptions,
274274- ): Promise<IndexDescription[]> {
275275- const indexes = await this.collection.listIndexes(options).toArray();
276276- return indexes as IndexDescription[];
277277- }
278278-279279- /**
280280- * Get index information by name
281281- * @param indexName - Name of the index
282282- * @returns Index description or null if not found
283283- */
284284- async getIndex(indexName: string): Promise<IndexDescription | null> {
285285- const indexes = await this.listIndexes();
286286- return indexes.find((idx) => idx.name === indexName) || null;
287287- }
288288-289289- /**
290290- * Check if an index exists
291291- * @param indexName - Name of the index
292292- * @returns True if index exists, false otherwise
293293- */
294294- async indexExists(indexName: string): Promise<boolean> {
295295- const index = await this.getIndex(indexName);
296296- return index !== null;
297297- }
298298-299299- /**
300300- * Synchronize indexes - create indexes if they don't exist, update if they differ
301301- * This is useful for ensuring indexes match your schema definition
302302- * @param indexes - Array of index descriptions to synchronize
303303- * @param options - Options for index creation
304304- */
305305- async syncIndexes(
306306- indexes: IndexDescription[],
307307- options?: CreateIndexesOptions,
308308- ): Promise<string[]> {
309309- const existingIndexes = await this.listIndexes();
310310-311311- const indexesToCreate: IndexDescription[] = [];
312312-313313- for (const index of indexes) {
314314- const indexName = index.name || this._generateIndexName(index.key);
315315- const existingIndex = existingIndexes.find(
316316- (idx) => idx.name === indexName,
317317- );
318318-319319- if (!existingIndex) {
320320- indexesToCreate.push(index);
321321- } else if (
322322- JSON.stringify(existingIndex.key) !== JSON.stringify(index.key)
323323- ) {
324324- // Index exists but keys differ - drop and recreate
325325- await this.dropIndex(indexName);
326326- indexesToCreate.push(index);
327327- }
328328- // If index exists and matches, skip it
329329- }
330330-331331- const created: string[] = [];
332332- if (indexesToCreate.length > 0) {
333333- const names = await this.createIndexes(indexesToCreate, options);
334334- created.push(...names);
335335- }
336336-337337- return created;
338338- }
339339-340340- /**
341341- * Helper method to generate index name from key specification
342342- */
343343- private _generateIndexName(keys: IndexSpecification): string {
344344- if (typeof keys === "string") {
345345- return keys;
346346- }
347347- const entries = Object.entries(keys as Record<string, number | string>);
348348- return entries.map(([field, direction]) => `${field}_${direction}`).join("_");
349349- }
350350-}
+264
model/core.ts
···11+import type { z } from "@zod/zod";
22+import type {
33+ Collection,
44+ DeleteResult,
55+ Document,
66+ Filter,
77+ InsertManyResult,
88+ InsertOneResult,
99+ InsertOneOptions,
1010+ FindOptions,
1111+ UpdateOptions,
1212+ ReplaceOptions,
1313+ DeleteOptions,
1414+ CountDocumentsOptions,
1515+ AggregateOptions,
1616+ OptionalUnlessRequiredId,
1717+ UpdateResult,
1818+ WithId,
1919+ BulkWriteOptions,
2020+} from "mongodb";
2121+import { ObjectId } from "mongodb";
2222+import type { Schema, Infer, Input } from "../types.ts";
2323+import { parse, parsePartial, parseReplace } from "./validation.ts";
2424+2525+/**
2626+ * Core CRUD operations for the Model class
2727+ *
2828+ * This module contains all basic create, read, update, and delete operations
2929+ * with automatic Zod validation and transaction support.
3030+ */
3131+3232+/**
3333+ * Insert a single document into the collection
3434+ *
3535+ * @param collection - MongoDB collection
3636+ * @param schema - Zod schema for validation
3737+ * @param data - Document data to insert
3838+ * @param options - Insert options (including session for transactions)
3939+ * @returns Insert result with insertedId
4040+ */
4141+export async function insertOne<T extends Schema>(
4242+ collection: Collection<Infer<T>>,
4343+ schema: T,
4444+ data: Input<T>,
4545+ options?: InsertOneOptions
4646+): Promise<InsertOneResult<Infer<T>>> {
4747+ const validatedData = parse(schema, data);
4848+ return await collection.insertOne(
4949+ validatedData as OptionalUnlessRequiredId<Infer<T>>,
5050+ options
5151+ );
5252+}
5353+5454+/**
5555+ * Insert multiple documents into the collection
5656+ *
5757+ * @param collection - MongoDB collection
5858+ * @param schema - Zod schema for validation
5959+ * @param data - Array of document data to insert
6060+ * @param options - Insert options (including session for transactions)
6161+ * @returns Insert result with insertedIds
6262+ */
6363+export async function insertMany<T extends Schema>(
6464+ collection: Collection<Infer<T>>,
6565+ schema: T,
6666+ data: Input<T>[],
6767+ options?: BulkWriteOptions
6868+): Promise<InsertManyResult<Infer<T>>> {
6969+ const validatedData = data.map((item) => parse(schema, item));
7070+ return await collection.insertMany(
7171+ validatedData as OptionalUnlessRequiredId<Infer<T>>[],
7272+ options
7373+ );
7474+}
7575+7676+/**
7777+ * Find multiple documents matching the query
7878+ *
7979+ * @param collection - MongoDB collection
8080+ * @param query - MongoDB query filter
8181+ * @param options - Find options (including session for transactions)
8282+ * @returns Array of matching documents
8383+ */
8484+export async function find<T extends Schema>(
8585+ collection: Collection<Infer<T>>,
8686+ query: Filter<Infer<T>>,
8787+ options?: FindOptions
8888+): Promise<(WithId<Infer<T>>)[]> {
8989+ return await collection.find(query, options).toArray();
9090+}
9191+9292+/**
9393+ * Find a single document matching the query
9494+ *
9595+ * @param collection - MongoDB collection
9696+ * @param query - MongoDB query filter
9797+ * @param options - Find options (including session for transactions)
9898+ * @returns Matching document or null if not found
9999+ */
100100+export async function findOne<T extends Schema>(
101101+ collection: Collection<Infer<T>>,
102102+ query: Filter<Infer<T>>,
103103+ options?: FindOptions
104104+): Promise<WithId<Infer<T>> | null> {
105105+ return await collection.findOne(query, options);
106106+}
107107+108108+/**
109109+ * Find a document by its MongoDB ObjectId
110110+ *
111111+ * @param collection - MongoDB collection
112112+ * @param id - Document ID (string or ObjectId)
113113+ * @param options - Find options (including session for transactions)
114114+ * @returns Matching document or null if not found
115115+ */
116116+export async function findById<T extends Schema>(
117117+ collection: Collection<Infer<T>>,
118118+ id: string | ObjectId,
119119+ options?: FindOptions
120120+): Promise<WithId<Infer<T>> | null> {
121121+ const objectId = typeof id === "string" ? new ObjectId(id) : id;
122122+ return await findOne(collection, { _id: objectId } as Filter<Infer<T>>, options);
123123+}
124124+125125+/**
126126+ * Update multiple documents matching the query
127127+ *
128128+ * @param collection - MongoDB collection
129129+ * @param schema - Zod schema for validation
130130+ * @param query - MongoDB query filter
131131+ * @param data - Partial data to update
132132+ * @param options - Update options (including session for transactions)
133133+ * @returns Update result
134134+ */
135135+export async function update<T extends Schema>(
136136+ collection: Collection<Infer<T>>,
137137+ schema: T,
138138+ query: Filter<Infer<T>>,
139139+ data: Partial<z.infer<T>>,
140140+ options?: UpdateOptions
141141+): Promise<UpdateResult<Infer<T>>> {
142142+ const validatedData = parsePartial(schema, data);
143143+ return await collection.updateMany(
144144+ query,
145145+ { $set: validatedData as Partial<Infer<T>> },
146146+ options
147147+ );
148148+}
149149+150150+/**
151151+ * Update a single document matching the query
152152+ *
153153+ * @param collection - MongoDB collection
154154+ * @param schema - Zod schema for validation
155155+ * @param query - MongoDB query filter
156156+ * @param data - Partial data to update
157157+ * @param options - Update options (including session for transactions)
158158+ * @returns Update result
159159+ */
160160+export async function updateOne<T extends Schema>(
161161+ collection: Collection<Infer<T>>,
162162+ schema: T,
163163+ query: Filter<Infer<T>>,
164164+ data: Partial<z.infer<T>>,
165165+ options?: UpdateOptions
166166+): Promise<UpdateResult<Infer<T>>> {
167167+ const validatedData = parsePartial(schema, data);
168168+ return await collection.updateOne(
169169+ query,
170170+ { $set: validatedData as Partial<Infer<T>> },
171171+ options
172172+ );
173173+}
174174+175175+/**
176176+ * Replace a single document matching the query
177177+ *
178178+ * @param collection - MongoDB collection
179179+ * @param schema - Zod schema for validation
180180+ * @param query - MongoDB query filter
181181+ * @param data - Complete document data for replacement
182182+ * @param options - Replace options (including session for transactions)
183183+ * @returns Update result
184184+ */
185185+export async function replaceOne<T extends Schema>(
186186+ collection: Collection<Infer<T>>,
187187+ schema: T,
188188+ query: Filter<Infer<T>>,
189189+ data: Input<T>,
190190+ options?: ReplaceOptions
191191+): Promise<UpdateResult<Infer<T>>> {
192192+ const validatedData = parseReplace(schema, data);
193193+ // Remove _id from validatedData for replaceOne (it will use the query's _id)
194194+ const { _id, ...withoutId } = validatedData as Infer<T> & { _id?: unknown };
195195+ return await collection.replaceOne(
196196+ query,
197197+ withoutId as Infer<T>,
198198+ options
199199+ );
200200+}
201201+202202+/**
203203+ * Delete multiple documents matching the query
204204+ *
205205+ * @param collection - MongoDB collection
206206+ * @param query - MongoDB query filter
207207+ * @param options - Delete options (including session for transactions)
208208+ * @returns Delete result
209209+ */
210210+export async function deleteMany<T extends Schema>(
211211+ collection: Collection<Infer<T>>,
212212+ query: Filter<Infer<T>>,
213213+ options?: DeleteOptions
214214+): Promise<DeleteResult> {
215215+ return await collection.deleteMany(query, options);
216216+}
217217+218218+/**
219219+ * Delete a single document matching the query
220220+ *
221221+ * @param collection - MongoDB collection
222222+ * @param query - MongoDB query filter
223223+ * @param options - Delete options (including session for transactions)
224224+ * @returns Delete result
225225+ */
226226+export async function deleteOne<T extends Schema>(
227227+ collection: Collection<Infer<T>>,
228228+ query: Filter<Infer<T>>,
229229+ options?: DeleteOptions
230230+): Promise<DeleteResult> {
231231+ return await collection.deleteOne(query, options);
232232+}
233233+234234+/**
235235+ * Count documents matching the query
236236+ *
237237+ * @param collection - MongoDB collection
238238+ * @param query - MongoDB query filter
239239+ * @param options - Count options (including session for transactions)
240240+ * @returns Number of matching documents
241241+ */
242242+export async function count<T extends Schema>(
243243+ collection: Collection<Infer<T>>,
244244+ query: Filter<Infer<T>>,
245245+ options?: CountDocumentsOptions
246246+): Promise<number> {
247247+ return await collection.countDocuments(query, options);
248248+}
249249+250250+/**
251251+ * Execute an aggregation pipeline
252252+ *
253253+ * @param collection - MongoDB collection
254254+ * @param pipeline - MongoDB aggregation pipeline
255255+ * @param options - Aggregate options (including session for transactions)
256256+ * @returns Array of aggregation results
257257+ */
258258+export async function aggregate<T extends Schema>(
259259+ collection: Collection<Infer<T>>,
260260+ pipeline: Document[],
261261+ options?: AggregateOptions
262262+): Promise<Document[]> {
263263+ return await collection.aggregate(pipeline, options).toArray();
264264+}
+355
model/index.ts
···11+import type { z } from "@zod/zod";
22+import type {
33+ Collection,
44+ CreateIndexesOptions,
55+ DeleteResult,
66+ Document,
77+ DropIndexesOptions,
88+ Filter,
99+ IndexDescription,
1010+ IndexSpecification,
1111+ InsertManyResult,
1212+ InsertOneResult,
1313+ InsertOneOptions,
1414+ FindOptions,
1515+ UpdateOptions,
1616+ ReplaceOptions,
1717+ DeleteOptions,
1818+ CountDocumentsOptions,
1919+ AggregateOptions,
2020+ ListIndexesOptions,
2121+ UpdateResult,
2222+ WithId,
2323+ BulkWriteOptions,
2424+} from "mongodb";
2525+import type { ObjectId } from "mongodb";
2626+import { getDb } from "../client/connection.ts";
2727+import type { Schema, Infer, Input } from "../types.ts";
2828+import * as core from "./core.ts";
2929+import * as indexes from "./indexes.ts";
3030+import * as pagination from "./pagination.ts";
3131+3232+/**
3333+ * Model class for type-safe MongoDB operations
3434+ *
3535+ * Provides a clean API for CRUD operations, pagination, and index management
3636+ * with automatic Zod validation and TypeScript type safety.
3737+ *
3838+ * @example
3939+ * ```ts
4040+ * const userSchema = z.object({
4141+ * name: z.string(),
4242+ * email: z.string().email(),
4343+ * });
4444+ *
4545+ * const UserModel = new Model("users", userSchema);
4646+ * await UserModel.insertOne({ name: "Alice", email: "alice@example.com" });
4747+ * ```
4848+ */
4949+export class Model<T extends Schema> {
5050+ private collection: Collection<Infer<T>>;
5151+ private schema: T;
5252+5353+ constructor(collectionName: string, schema: T) {
5454+ this.collection = getDb().collection<Infer<T>>(collectionName);
5555+ this.schema = schema;
5656+ }
5757+5858+ // ============================================================================
5959+ // CRUD Operations (delegated to core.ts)
6060+ // ============================================================================
6161+6262+ /**
6363+ * Insert a single document into the collection
6464+ *
6565+ * @param data - Document data to insert
6666+ * @param options - Insert options (including session for transactions)
6767+ * @returns Insert result with insertedId
6868+ */
6969+ async insertOne(
7070+ data: Input<T>,
7171+ options?: InsertOneOptions
7272+ ): Promise<InsertOneResult<Infer<T>>> {
7373+ return await core.insertOne(this.collection, this.schema, data, options);
7474+ }
7575+7676+ /**
7777+ * Insert multiple documents into the collection
7878+ *
7979+ * @param data - Array of document data to insert
8080+ * @param options - Insert options (including session for transactions)
8181+ * @returns Insert result with insertedIds
8282+ */
8383+ async insertMany(
8484+ data: Input<T>[],
8585+ options?: BulkWriteOptions
8686+ ): Promise<InsertManyResult<Infer<T>>> {
8787+ return await core.insertMany(this.collection, this.schema, data, options);
8888+ }
8989+9090+ /**
9191+ * Find multiple documents matching the query
9292+ *
9393+ * @param query - MongoDB query filter
9494+ * @param options - Find options (including session for transactions)
9595+ * @returns Array of matching documents
9696+ */
9797+ async find(
9898+ query: Filter<Infer<T>>,
9999+ options?: FindOptions
100100+ ): Promise<(WithId<Infer<T>>)[]> {
101101+ return await core.find(this.collection, query, options);
102102+ }
103103+104104+ /**
105105+ * Find a single document matching the query
106106+ *
107107+ * @param query - MongoDB query filter
108108+ * @param options - Find options (including session for transactions)
109109+ * @returns Matching document or null if not found
110110+ */
111111+ async findOne(
112112+ query: Filter<Infer<T>>,
113113+ options?: FindOptions
114114+ ): Promise<WithId<Infer<T>> | null> {
115115+ return await core.findOne(this.collection, query, options);
116116+ }
117117+118118+ /**
119119+ * Find a document by its MongoDB ObjectId
120120+ *
121121+ * @param id - Document ID (string or ObjectId)
122122+ * @param options - Find options (including session for transactions)
123123+ * @returns Matching document or null if not found
124124+ */
125125+ async findById(
126126+ id: string | ObjectId,
127127+ options?: FindOptions
128128+ ): Promise<WithId<Infer<T>> | null> {
129129+ return await core.findById(this.collection, id, options);
130130+ }
131131+132132+ /**
133133+ * Update multiple documents matching the query
134134+ *
135135+ * @param query - MongoDB query filter
136136+ * @param data - Partial data to update
137137+ * @param options - Update options (including session for transactions)
138138+ * @returns Update result
139139+ */
140140+ async update(
141141+ query: Filter<Infer<T>>,
142142+ data: Partial<z.infer<T>>,
143143+ options?: UpdateOptions
144144+ ): Promise<UpdateResult<Infer<T>>> {
145145+ return await core.update(this.collection, this.schema, query, data, options);
146146+ }
147147+148148+ /**
149149+ * Update a single document matching the query
150150+ *
151151+ * @param query - MongoDB query filter
152152+ * @param data - Partial data to update
153153+ * @param options - Update options (including session for transactions)
154154+ * @returns Update result
155155+ */
156156+ async updateOne(
157157+ query: Filter<Infer<T>>,
158158+ data: Partial<z.infer<T>>,
159159+ options?: UpdateOptions
160160+ ): Promise<UpdateResult<Infer<T>>> {
161161+ return await core.updateOne(this.collection, this.schema, query, data, options);
162162+ }
163163+164164+ /**
165165+ * Replace a single document matching the query
166166+ *
167167+ * @param query - MongoDB query filter
168168+ * @param data - Complete document data for replacement
169169+ * @param options - Replace options (including session for transactions)
170170+ * @returns Update result
171171+ */
172172+ async replaceOne(
173173+ query: Filter<Infer<T>>,
174174+ data: Input<T>,
175175+ options?: ReplaceOptions
176176+ ): Promise<UpdateResult<Infer<T>>> {
177177+ return await core.replaceOne(this.collection, this.schema, query, data, options);
178178+ }
179179+180180+ /**
181181+ * Delete multiple documents matching the query
182182+ *
183183+ * @param query - MongoDB query filter
184184+ * @param options - Delete options (including session for transactions)
185185+ * @returns Delete result
186186+ */
187187+ async delete(
188188+ query: Filter<Infer<T>>,
189189+ options?: DeleteOptions
190190+ ): Promise<DeleteResult> {
191191+ return await core.deleteMany(this.collection, query, options);
192192+ }
193193+194194+ /**
195195+ * Delete a single document matching the query
196196+ *
197197+ * @param query - MongoDB query filter
198198+ * @param options - Delete options (including session for transactions)
199199+ * @returns Delete result
200200+ */
201201+ async deleteOne(
202202+ query: Filter<Infer<T>>,
203203+ options?: DeleteOptions
204204+ ): Promise<DeleteResult> {
205205+ return await core.deleteOne(this.collection, query, options);
206206+ }
207207+208208+ /**
209209+ * Count documents matching the query
210210+ *
211211+ * @param query - MongoDB query filter
212212+ * @param options - Count options (including session for transactions)
213213+ * @returns Number of matching documents
214214+ */
215215+ async count(
216216+ query: Filter<Infer<T>>,
217217+ options?: CountDocumentsOptions
218218+ ): Promise<number> {
219219+ return await core.count(this.collection, query, options);
220220+ }
221221+222222+ /**
223223+ * Execute an aggregation pipeline
224224+ *
225225+ * @param pipeline - MongoDB aggregation pipeline
226226+ * @param options - Aggregate options (including session for transactions)
227227+ * @returns Array of aggregation results
228228+ */
229229+ async aggregate(
230230+ pipeline: Document[],
231231+ options?: AggregateOptions
232232+ ): Promise<Document[]> {
233233+ return await core.aggregate(this.collection, pipeline, options);
234234+ }
235235+236236+ // ============================================================================
237237+ // Pagination (delegated to pagination.ts)
238238+ // ============================================================================
239239+240240+ /**
241241+ * Find documents with pagination support
242242+ *
243243+ * @param query - MongoDB query filter
244244+ * @param options - Pagination options (skip, limit, sort)
245245+ * @returns Array of matching documents
246246+ */
247247+ async findPaginated(
248248+ query: Filter<Infer<T>>,
249249+ options: { skip?: number; limit?: number; sort?: Document } = {},
250250+ ): Promise<(WithId<Infer<T>>)[]> {
251251+ return await pagination.findPaginated(this.collection, query, options);
252252+ }
253253+254254+ // ============================================================================
255255+ // Index Management (delegated to indexes.ts)
256256+ // ============================================================================
257257+258258+ /**
259259+ * Create a single index on the collection
260260+ *
261261+ * @param keys - Index specification (e.g., { email: 1 } or { name: "text" })
262262+ * @param options - Index creation options (unique, sparse, expireAfterSeconds, etc.)
263263+ * @returns The name of the created index
264264+ */
265265+ async createIndex(
266266+ keys: IndexSpecification,
267267+ options?: CreateIndexesOptions,
268268+ ): Promise<string> {
269269+ return await indexes.createIndex(this.collection, keys, options);
270270+ }
271271+272272+ /**
273273+ * Create multiple indexes on the collection
274274+ *
275275+ * @param indexes - Array of index descriptions
276276+ * @param options - Index creation options
277277+ * @returns Array of index names created
278278+ */
279279+ async createIndexes(
280280+ indexList: IndexDescription[],
281281+ options?: CreateIndexesOptions,
282282+ ): Promise<string[]> {
283283+ return await indexes.createIndexes(this.collection, indexList, options);
284284+ }
285285+286286+ /**
287287+ * Drop a single index from the collection
288288+ *
289289+ * @param index - Index name or specification
290290+ * @param options - Drop index options
291291+ */
292292+ async dropIndex(
293293+ index: string | IndexSpecification,
294294+ options?: DropIndexesOptions,
295295+ ): Promise<void> {
296296+ return await indexes.dropIndex(this.collection, index, options);
297297+ }
298298+299299+ /**
300300+ * Drop all indexes from the collection (except _id index)
301301+ *
302302+ * @param options - Drop index options
303303+ */
304304+ async dropIndexes(options?: DropIndexesOptions): Promise<void> {
305305+ return await indexes.dropIndexes(this.collection, options);
306306+ }
307307+308308+ /**
309309+ * List all indexes on the collection
310310+ *
311311+ * @param options - List indexes options
312312+ * @returns Array of index information
313313+ */
314314+ async listIndexes(
315315+ options?: ListIndexesOptions,
316316+ ): Promise<IndexDescription[]> {
317317+ return await indexes.listIndexes(this.collection, options);
318318+ }
319319+320320+ /**
321321+ * Get index information by name
322322+ *
323323+ * @param indexName - Name of the index
324324+ * @returns Index description or null if not found
325325+ */
326326+ async getIndex(indexName: string): Promise<IndexDescription | null> {
327327+ return await indexes.getIndex(this.collection, indexName);
328328+ }
329329+330330+ /**
331331+ * Check if an index exists
332332+ *
333333+ * @param indexName - Name of the index
334334+ * @returns True if index exists, false otherwise
335335+ */
336336+ async indexExists(indexName: string): Promise<boolean> {
337337+ return await indexes.indexExists(this.collection, indexName);
338338+ }
339339+340340+ /**
341341+ * Synchronize indexes - create indexes if they don't exist, update if they differ
342342+ *
343343+ * This is useful for ensuring indexes match your schema definition
344344+ *
345345+ * @param indexes - Array of index descriptions to synchronize
346346+ * @param options - Options for index creation
347347+ * @returns Array of index names that were created
348348+ */
349349+ async syncIndexes(
350350+ indexList: IndexDescription[],
351351+ options?: CreateIndexesOptions,
352352+ ): Promise<string[]> {
353353+ return await indexes.syncIndexes(this.collection, indexList, options);
354354+ }
355355+}
+180
model/indexes.ts
···11+import type {
22+ Collection,
33+ CreateIndexesOptions,
44+ DropIndexesOptions,
55+ IndexDescription,
66+ IndexSpecification,
77+ ListIndexesOptions,
88+} from "mongodb";
99+import type { Schema, Infer } from "../types.ts";
1010+1111+/**
1212+ * Index management operations for the Model class
1313+ *
1414+ * This module contains all index-related operations including creation,
1515+ * deletion, listing, and synchronization of indexes.
1616+ */
1717+1818+/**
1919+ * Create a single index on the collection
2020+ *
2121+ * @param collection - MongoDB collection
2222+ * @param keys - Index specification (e.g., { email: 1 } or { name: "text" })
2323+ * @param options - Index creation options (unique, sparse, expireAfterSeconds, etc.)
2424+ * @returns The name of the created index
2525+ */
2626+export async function createIndex<T extends Schema>(
2727+ collection: Collection<Infer<T>>,
2828+ keys: IndexSpecification,
2929+ options?: CreateIndexesOptions,
3030+): Promise<string> {
3131+ return await collection.createIndex(keys, options);
3232+}
3333+3434+/**
3535+ * Create multiple indexes on the collection
3636+ *
3737+ * @param collection - MongoDB collection
3838+ * @param indexes - Array of index descriptions
3939+ * @param options - Index creation options
4040+ * @returns Array of index names created
4141+ */
4242+export async function createIndexes<T extends Schema>(
4343+ collection: Collection<Infer<T>>,
4444+ indexes: IndexDescription[],
4545+ options?: CreateIndexesOptions,
4646+): Promise<string[]> {
4747+ return await collection.createIndexes(indexes, options);
4848+}
4949+5050+/**
5151+ * Drop a single index from the collection
5252+ *
5353+ * @param collection - MongoDB collection
5454+ * @param index - Index name or specification
5555+ * @param options - Drop index options
5656+ */
5757+export async function dropIndex<T extends Schema>(
5858+ collection: Collection<Infer<T>>,
5959+ index: string | IndexSpecification,
6060+ options?: DropIndexesOptions,
6161+): Promise<void> {
6262+ await collection.dropIndex(index as string, options);
6363+}
6464+6565+/**
6666+ * Drop all indexes from the collection (except _id index)
6767+ *
6868+ * @param collection - MongoDB collection
6969+ * @param options - Drop index options
7070+ */
7171+export async function dropIndexes<T extends Schema>(
7272+ collection: Collection<Infer<T>>,
7373+ options?: DropIndexesOptions
7474+): Promise<void> {
7575+ await collection.dropIndexes(options);
7676+}
7777+7878+/**
7979+ * List all indexes on the collection
8080+ *
8181+ * @param collection - MongoDB collection
8282+ * @param options - List indexes options
8383+ * @returns Array of index information
8484+ */
8585+export async function listIndexes<T extends Schema>(
8686+ collection: Collection<Infer<T>>,
8787+ options?: ListIndexesOptions,
8888+): Promise<IndexDescription[]> {
8989+ const indexes = await collection.listIndexes(options).toArray();
9090+ return indexes as IndexDescription[];
9191+}
9292+9393+/**
9494+ * Get index information by name
9595+ *
9696+ * @param collection - MongoDB collection
9797+ * @param indexName - Name of the index
9898+ * @returns Index description or null if not found
9999+ */
100100+export async function getIndex<T extends Schema>(
101101+ collection: Collection<Infer<T>>,
102102+ indexName: string
103103+): Promise<IndexDescription | null> {
104104+ const indexes = await listIndexes(collection);
105105+ return indexes.find((idx) => idx.name === indexName) || null;
106106+}
107107+108108+/**
109109+ * Check if an index exists
110110+ *
111111+ * @param collection - MongoDB collection
112112+ * @param indexName - Name of the index
113113+ * @returns True if index exists, false otherwise
114114+ */
115115+export async function indexExists<T extends Schema>(
116116+ collection: Collection<Infer<T>>,
117117+ indexName: string
118118+): Promise<boolean> {
119119+ const index = await getIndex(collection, indexName);
120120+ return index !== null;
121121+}
122122+123123+/**
124124+ * Synchronize indexes - create indexes if they don't exist, update if they differ
125125+ *
126126+ * This is useful for ensuring indexes match your schema definition
127127+ *
128128+ * @param collection - MongoDB collection
129129+ * @param indexes - Array of index descriptions to synchronize
130130+ * @param options - Options for index creation
131131+ * @returns Array of index names that were created
132132+ */
133133+export async function syncIndexes<T extends Schema>(
134134+ collection: Collection<Infer<T>>,
135135+ indexes: IndexDescription[],
136136+ options?: CreateIndexesOptions,
137137+): Promise<string[]> {
138138+ const existingIndexes = await listIndexes(collection);
139139+ const indexesToCreate: IndexDescription[] = [];
140140+141141+ for (const index of indexes) {
142142+ const indexName = index.name || generateIndexName(index.key);
143143+ const existingIndex = existingIndexes.find(
144144+ (idx) => idx.name === indexName,
145145+ );
146146+147147+ if (!existingIndex) {
148148+ indexesToCreate.push(index);
149149+ } else if (
150150+ JSON.stringify(existingIndex.key) !== JSON.stringify(index.key)
151151+ ) {
152152+ // Index exists but keys differ - drop and recreate
153153+ await dropIndex(collection, indexName);
154154+ indexesToCreate.push(index);
155155+ }
156156+ // If index exists and matches, skip it
157157+ }
158158+159159+ const created: string[] = [];
160160+ if (indexesToCreate.length > 0) {
161161+ const names = await createIndexes(collection, indexesToCreate, options);
162162+ created.push(...names);
163163+ }
164164+165165+ return created;
166166+}
167167+168168+/**
169169+ * Generate index name from key specification
170170+ *
171171+ * @param keys - Index specification
172172+ * @returns Generated index name
173173+ */
174174+export function generateIndexName(keys: IndexSpecification): string {
175175+ if (typeof keys === "string") {
176176+ return keys;
177177+ }
178178+ const entries = Object.entries(keys as Record<string, number | string>);
179179+ return entries.map(([field, direction]) => `${field}_${direction}`).join("_");
180180+}
···11+import type { z } from "@zod/zod";
22+import type { Document, ObjectId } from "mongodb";
33+44+/**
55+ * Type alias for Zod schema objects
66+ */
77+export type Schema = z.ZodObject<z.ZodRawShape>;
88+99+/**
1010+ * Infer the TypeScript type from a Zod schema, including MongoDB Document
1111+ */
1212+export type Infer<T extends Schema> = z.infer<T> & Document;
1313+1414+1515+/**
1616+ * Infer the model type from a Zod schema, including MongoDB Document and ObjectId
1717+ */
1818+export type InferModel<T extends Schema> = Infer<T> & {
1919+ _id?: ObjectId;
2020+ };
2121+2222+/**
2323+ * Infer the input type for a Zod schema (handles defaults)
2424+ */
2525+export type Input<T extends Schema> = z.input<T>;