Thin MongoDB ODM built for Standard Schema
mongodb zod deno

re-org

knotbin.com 80092f8a 413c7a59

verified
+1271 -612
-241
client.ts
··· 1 - import { 2 - type Db, 3 - type MongoClientOptions, 4 - type ClientSession, 5 - type TransactionOptions, 6 - MongoClient 7 - } from "mongodb"; 8 - import { ConnectionError } from "./errors.ts"; 9 - 10 - interface Connection { 11 - client: MongoClient; 12 - db: Db; 13 - } 14 - 15 - let connection: Connection | null = null; 16 - 17 - export interface ConnectOptions extends MongoClientOptions {}; 18 - 19 - /** 20 - * Health check details of the MongoDB connection 21 - * 22 - * @property healthy - Overall health status of the connection 23 - * @property connected - Whether a connection is established 24 - * @property responseTimeMs - Response time in milliseconds (if connection is healthy) 25 - * @property error - Error message if health check failed 26 - * @property timestamp - Timestamp when health check was performed 27 - */ 28 - export interface HealthCheckResult { 29 - healthy: boolean; 30 - connected: boolean; 31 - responseTimeMs?: number; 32 - error?: string; 33 - timestamp: Date; 34 - } 35 - 36 - /** 37 - * Connect to MongoDB with connection pooling, retry logic, and resilience options 38 - * 39 - * The MongoDB driver handles connection pooling and automatic retries. 40 - * Retry logic is enabled by default for both reads and writes in MongoDB 4.2+. 41 - * 42 - * @param uri - MongoDB connection string 43 - * @param dbName - Name of the database to connect to 44 - * @param options - Connection options (pooling, retries, timeouts, etc.) 45 - * 46 - * @example 47 - * Basic connection with pooling: 48 - * ```ts 49 - * await connect("mongodb://localhost:27017", "mydb", { 50 - * maxPoolSize: 10, 51 - * minPoolSize: 2, 52 - * maxIdleTimeMS: 30000, 53 - * connectTimeoutMS: 10000, 54 - * socketTimeoutMS: 45000, 55 - * }); 56 - * ``` 57 - * 58 - * @example 59 - * Production-ready connection with retry logic and resilience: 60 - * ```ts 61 - * await connect("mongodb://localhost:27017", "mydb", { 62 - * // Connection pooling 63 - * maxPoolSize: 10, 64 - * minPoolSize: 2, 65 - * 66 - * // Automatic retry logic (enabled by default) 67 - * retryReads: true, // Retry failed read operations 68 - * retryWrites: true, // Retry failed write operations 69 - * 70 - * // Timeouts 71 - * connectTimeoutMS: 10000, // Initial connection timeout 72 - * socketTimeoutMS: 45000, // Socket operation timeout 73 - * serverSelectionTimeoutMS: 10000, // Server selection timeout 74 - * 75 - * // Connection resilience 76 - * maxIdleTimeMS: 30000, // Close idle connections 77 - * heartbeatFrequencyMS: 10000, // Server health check interval 78 - * 79 - * // Optional: Compression for reduced bandwidth 80 - * compressors: ['snappy', 'zlib'], 81 - * }); 82 - * ``` 83 - */ 84 - export async function connect( 85 - uri: string, 86 - dbName: string, 87 - options?: ConnectOptions, 88 - ): Promise<Connection> { 89 - if (connection) { 90 - return connection; 91 - } 92 - 93 - try { 94 - const client = new MongoClient(uri, options); 95 - await client.connect(); 96 - const db = client.db(dbName); 97 - 98 - connection = { client, db }; 99 - return connection; 100 - } catch (error) { 101 - throw new ConnectionError( 102 - `Failed to connect to MongoDB: ${error instanceof Error ? error.message : String(error)}`, 103 - uri 104 - ); 105 - } 106 - } 107 - 108 - export async function disconnect(): Promise<void> { 109 - if (connection) { 110 - await connection.client.close(); 111 - connection = null; 112 - } 113 - } 114 - 115 - /** 116 - * Start a new client session for transactions 117 - * 118 - * Sessions must be ended when done using `endSession()` 119 - * 120 - * @example 121 - * ```ts 122 - * const session = await startSession(); 123 - * try { 124 - * // use session 125 - * } finally { 126 - * await endSession(session); 127 - * } 128 - * ``` 129 - */ 130 - export function startSession(): ClientSession { 131 - if (!connection) { 132 - throw new ConnectionError("MongoDB not connected. Call connect() first."); 133 - } 134 - return connection.client.startSession(); 135 - } 136 - 137 - /** 138 - * End a client session 139 - * 140 - * @param session - The session to end 141 - */ 142 - export async function endSession(session: ClientSession): Promise<void> { 143 - await session.endSession(); 144 - } 145 - 146 - /** 147 - * Execute a function within a transaction 148 - * 149 - * Automatically handles session creation, transaction start/commit/abort, and cleanup. 150 - * If the callback throws an error, the transaction is automatically aborted. 151 - * 152 - * @param callback - Async function to execute within the transaction. Receives the session as parameter. 153 - * @param options - Optional transaction options (read/write concern, etc.) 154 - * @returns The result from the callback function 155 - * 156 - * @example 157 - * ```ts 158 - * const result = await withTransaction(async (session) => { 159 - * await UserModel.insertOne({ name: "Alice" }, { session }); 160 - * await OrderModel.insertOne({ userId: "123", total: 100 }, { session }); 161 - * return { success: true }; 162 - * }); 163 - * ``` 164 - */ 165 - export async function withTransaction<T>( 166 - callback: (session: ClientSession) => Promise<T>, 167 - options?: TransactionOptions 168 - ): Promise<T> { 169 - const session = await startSession(); 170 - 171 - try { 172 - let result: T; 173 - 174 - await session.withTransaction(async () => { 175 - result = await callback(session); 176 - }, options); 177 - 178 - return result!; 179 - } finally { 180 - await endSession(session); 181 - } 182 - } 183 - 184 - export function getDb(): Db { 185 - if (!connection) { 186 - throw new ConnectionError("MongoDB not connected. Call connect() first."); 187 - } 188 - return connection.db; 189 - } 190 - 191 - /** 192 - * Check the health of the MongoDB connection 193 - * 194 - * Performs a ping operation to verify the database is responsive 195 - * and returns detailed health information including response time. 196 - * 197 - * @example 198 - * ```ts 199 - * const health = await healthCheck(); 200 - * if (health.healthy) { 201 - * console.log(`Database healthy (${health.responseTimeMs}ms)`); 202 - * } else { 203 - * console.error(`Database unhealthy: ${health.error}`); 204 - * } 205 - * ``` 206 - */ 207 - export async function healthCheck(): Promise<HealthCheckResult> { 208 - const timestamp = new Date(); 209 - 210 - // Check if connection exists 211 - if (!connection) { 212 - return { 213 - healthy: false, 214 - connected: false, 215 - error: "No active connection. Call connect() first.", 216 - timestamp, 217 - }; 218 - } 219 - 220 - try { 221 - // Measure ping response time 222 - const startTime = performance.now(); 223 - await connection.db.admin().ping(); 224 - const endTime = performance.now(); 225 - const responseTimeMs = Math.round(endTime - startTime); 226 - 227 - return { 228 - healthy: true, 229 - connected: true, 230 - responseTimeMs, 231 - timestamp, 232 - }; 233 - } catch (error) { 234 - return { 235 - healthy: false, 236 - connected: true, 237 - error: error instanceof Error ? error.message : String(error), 238 - timestamp, 239 - }; 240 - } 241 - }
+126
client/connection.ts
··· 1 + import { type Db, type MongoClientOptions, MongoClient } from "mongodb"; 2 + import { ConnectionError } from "../errors.ts"; 3 + 4 + /** 5 + * Connection management module 6 + * 7 + * Handles MongoDB connection lifecycle including connect, disconnect, 8 + * and connection state management. 9 + */ 10 + 11 + export interface Connection { 12 + client: MongoClient; 13 + db: Db; 14 + } 15 + 16 + export interface ConnectOptions extends MongoClientOptions {} 17 + 18 + // Singleton connection state 19 + let connection: Connection | null = null; 20 + 21 + /** 22 + * Connect to MongoDB with connection pooling, retry logic, and resilience options 23 + * 24 + * The MongoDB driver handles connection pooling and automatic retries. 25 + * Retry logic is enabled by default for both reads and writes in MongoDB 4.2+. 26 + * 27 + * @param uri - MongoDB connection string 28 + * @param dbName - Name of the database to connect to 29 + * @param options - Connection options (pooling, retries, timeouts, etc.) 30 + * @returns Connection object with client and db 31 + * 32 + * @example 33 + * Basic connection with pooling: 34 + * ```ts 35 + * await connect("mongodb://localhost:27017", "mydb", { 36 + * maxPoolSize: 10, 37 + * minPoolSize: 2, 38 + * maxIdleTimeMS: 30000, 39 + * connectTimeoutMS: 10000, 40 + * socketTimeoutMS: 45000, 41 + * }); 42 + * ``` 43 + * 44 + * @example 45 + * Production-ready connection with retry logic and resilience: 46 + * ```ts 47 + * await connect("mongodb://localhost:27017", "mydb", { 48 + * // Connection pooling 49 + * maxPoolSize: 10, 50 + * minPoolSize: 2, 51 + * 52 + * // Automatic retry logic (enabled by default) 53 + * retryReads: true, // Retry failed read operations 54 + * retryWrites: true, // Retry failed write operations 55 + * 56 + * // Timeouts 57 + * connectTimeoutMS: 10000, // Initial connection timeout 58 + * socketTimeoutMS: 45000, // Socket operation timeout 59 + * serverSelectionTimeoutMS: 10000, // Server selection timeout 60 + * 61 + * // Connection resilience 62 + * maxIdleTimeMS: 30000, // Close idle connections 63 + * heartbeatFrequencyMS: 10000, // Server health check interval 64 + * 65 + * // Optional: Compression for reduced bandwidth 66 + * compressors: ['snappy', 'zlib'], 67 + * }); 68 + * ``` 69 + */ 70 + export async function connect( 71 + uri: string, 72 + dbName: string, 73 + options?: ConnectOptions, 74 + ): Promise<Connection> { 75 + if (connection) { 76 + return connection; 77 + } 78 + 79 + try { 80 + const client = new MongoClient(uri, options); 81 + await client.connect(); 82 + const db = client.db(dbName); 83 + 84 + connection = { client, db }; 85 + return connection; 86 + } catch (error) { 87 + throw new ConnectionError( 88 + `Failed to connect to MongoDB: ${error instanceof Error ? error.message : String(error)}`, 89 + uri 90 + ); 91 + } 92 + } 93 + 94 + /** 95 + * Disconnect from MongoDB and clean up resources 96 + */ 97 + export async function disconnect(): Promise<void> { 98 + if (connection) { 99 + await connection.client.close(); 100 + connection = null; 101 + } 102 + } 103 + 104 + /** 105 + * Get the current database connection 106 + * 107 + * @returns MongoDB Db instance 108 + * @throws {ConnectionError} If not connected 109 + * @internal 110 + */ 111 + export function getDb(): Db { 112 + if (!connection) { 113 + throw new ConnectionError("MongoDB not connected. Call connect() first."); 114 + } 115 + return connection.db; 116 + } 117 + 118 + /** 119 + * Get the current connection state 120 + * 121 + * @returns Connection object or null if not connected 122 + * @internal 123 + */ 124 + export function getConnection(): Connection | null { 125 + return connection; 126 + }
+80
client/health.ts
··· 1 + import { getConnection } from "./connection.ts"; 2 + 3 + /** 4 + * Health check module 5 + * 6 + * Provides functionality for monitoring MongoDB connection health 7 + * including ping operations and response time measurement. 8 + */ 9 + 10 + /** 11 + * Health check details of the MongoDB connection 12 + * 13 + * @property healthy - Overall health status of the connection 14 + * @property connected - Whether a connection is established 15 + * @property responseTimeMs - Response time in milliseconds (if connection is healthy) 16 + * @property error - Error message if health check failed 17 + * @property timestamp - Timestamp when health check was performed 18 + */ 19 + export interface HealthCheckResult { 20 + healthy: boolean; 21 + connected: boolean; 22 + responseTimeMs?: number; 23 + error?: string; 24 + timestamp: Date; 25 + } 26 + 27 + /** 28 + * Check the health of the MongoDB connection 29 + * 30 + * Performs a ping operation to verify the database is responsive 31 + * and returns detailed health information including response time. 32 + * 33 + * @returns Health check result with status and metrics 34 + * 35 + * @example 36 + * ```ts 37 + * const health = await healthCheck(); 38 + * if (health.healthy) { 39 + * console.log(`Database healthy (${health.responseTimeMs}ms)`); 40 + * } else { 41 + * console.error(`Database unhealthy: ${health.error}`); 42 + * } 43 + * ``` 44 + */ 45 + export async function healthCheck(): Promise<HealthCheckResult> { 46 + const timestamp = new Date(); 47 + const connection = getConnection(); 48 + 49 + // Check if connection exists 50 + if (!connection) { 51 + return { 52 + healthy: false, 53 + connected: false, 54 + error: "No active connection. Call connect() first.", 55 + timestamp, 56 + }; 57 + } 58 + 59 + try { 60 + // Measure ping response time 61 + const startTime = performance.now(); 62 + await connection.db.admin().ping(); 63 + const endTime = performance.now(); 64 + const responseTimeMs = Math.round(endTime - startTime); 65 + 66 + return { 67 + healthy: true, 68 + connected: true, 69 + responseTimeMs, 70 + timestamp, 71 + }; 72 + } catch (error) { 73 + return { 74 + healthy: false, 75 + connected: true, 76 + error: error instanceof Error ? error.message : String(error), 77 + timestamp, 78 + }; 79 + } 80 + }
+30
client/index.ts
··· 1 + /** 2 + * Client module - MongoDB connection and session management 3 + * 4 + * This module provides all client-level functionality including: 5 + * - Connection management (connect, disconnect) 6 + * - Health monitoring (healthCheck) 7 + * - Transaction support (startSession, endSession, withTransaction) 8 + */ 9 + 10 + // Re-export connection management 11 + export { 12 + connect, 13 + disconnect, 14 + getDb, 15 + type ConnectOptions, 16 + type Connection, 17 + } from "./connection.ts"; 18 + 19 + // Re-export health monitoring 20 + export { 21 + healthCheck, 22 + type HealthCheckResult, 23 + } from "./health.ts"; 24 + 25 + // Re-export transaction management 26 + export { 27 + startSession, 28 + endSession, 29 + withTransaction, 30 + } from "./transactions.ts";
+83
client/transactions.ts
··· 1 + import type { ClientSession, TransactionOptions } from "mongodb"; 2 + import { getConnection } from "./connection.ts"; 3 + import { ConnectionError } from "../errors.ts"; 4 + 5 + /** 6 + * Transaction management module 7 + * 8 + * Provides session and transaction management functionality including 9 + * automatic transaction handling and manual session control. 10 + */ 11 + 12 + /** 13 + * Start a new client session for transactions 14 + * 15 + * Sessions must be ended when done using `endSession()` 16 + * 17 + * @returns New MongoDB ClientSession 18 + * @throws {ConnectionError} If not connected 19 + * 20 + * @example 21 + * ```ts 22 + * const session = startSession(); 23 + * try { 24 + * // use session 25 + * } finally { 26 + * await endSession(session); 27 + * } 28 + * ``` 29 + */ 30 + export function startSession(): ClientSession { 31 + const connection = getConnection(); 32 + if (!connection) { 33 + throw new ConnectionError("MongoDB not connected. Call connect() first."); 34 + } 35 + return connection.client.startSession(); 36 + } 37 + 38 + /** 39 + * End a client session 40 + * 41 + * @param session - The session to end 42 + */ 43 + export async function endSession(session: ClientSession): Promise<void> { 44 + await session.endSession(); 45 + } 46 + 47 + /** 48 + * Execute a function within a transaction 49 + * 50 + * Automatically handles session creation, transaction start/commit/abort, and cleanup. 51 + * If the callback throws an error, the transaction is automatically aborted. 52 + * 53 + * @param callback - Async function to execute within the transaction. Receives the session as parameter. 54 + * @param options - Optional transaction options (read/write concern, etc.) 55 + * @returns The result from the callback function 56 + * 57 + * @example 58 + * ```ts 59 + * const result = await withTransaction(async (session) => { 60 + * await UserModel.insertOne({ name: "Alice" }, { session }); 61 + * await OrderModel.insertOne({ userId: "123", total: 100 }, { session }); 62 + * return { success: true }; 63 + * }); 64 + * ``` 65 + */ 66 + export async function withTransaction<T>( 67 + callback: (session: ClientSession) => Promise<T>, 68 + options?: TransactionOptions 69 + ): Promise<T> { 70 + const session = startSession(); 71 + 72 + try { 73 + let result: T; 74 + 75 + await session.withTransaction(async () => { 76 + result = await callback(session); 77 + }, options); 78 + 79 + return result!; 80 + } finally { 81 + await endSession(session); 82 + } 83 + }
+3 -3
mod.ts
··· 1 - export { type InferModel, type Input } from "./schema.ts"; 1 + export type { Schema, Infer, Input } from "./types.ts"; 2 2 export { 3 3 connect, 4 4 disconnect, ··· 8 8 withTransaction, 9 9 type ConnectOptions, 10 10 type HealthCheckResult 11 - } from "./client.ts"; 12 - export { Model } from "./model.ts"; 11 + } from "./client/index.ts"; 12 + export { Model } from "./model/index.ts"; 13 13 export { 14 14 NozzleError, 15 15 ValidationError,
-350
model.ts
··· 1 - import type { z } from "@zod/zod"; 2 - import 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 - OptionalUnlessRequiredId, 22 - UpdateResult, 23 - WithId, 24 - BulkWriteOptions, 25 - } from "mongodb"; 26 - import { ObjectId } from "mongodb"; 27 - import { getDb } from "./client.ts"; 28 - import { ValidationError, AsyncValidationError } from "./errors.ts"; 29 - 30 - // Type alias for cleaner code - Zod schema 31 - type Schema = z.ZodObject; 32 - type Infer<T extends Schema> = z.infer<T> & Document; 33 - type Input<T extends Schema> = z.input<T>; 34 - 35 - // Helper function to validate data using Zod 36 - function parse<T extends Schema>(schema: T, data: Input<T>): Infer<T> { 37 - const result = schema.safeParse(data); 38 - 39 - // Check for async validation 40 - if (result instanceof Promise) { 41 - throw new AsyncValidationError(); 42 - } 43 - 44 - if (!result.success) { 45 - throw new ValidationError(result.error.issues, "insert"); 46 - } 47 - return result.data as Infer<T>; 48 - } 49 - 50 - // Helper function to validate partial update data using Zod's partial() 51 - function parsePartial<T extends Schema>( 52 - schema: T, 53 - data: Partial<z.infer<T>>, 54 - ): Partial<z.infer<T>> { 55 - const result = schema.partial().safeParse(data); 56 - 57 - // Check for async validation 58 - if (result instanceof Promise) { 59 - throw new AsyncValidationError(); 60 - } 61 - 62 - if (!result.success) { 63 - throw new ValidationError(result.error.issues, "update"); 64 - } 65 - return result.data as Partial<z.infer<T>>; 66 - } 67 - 68 - // Helper function to validate replace data using Zod 69 - function parseReplace<T extends Schema>(schema: T, data: Input<T>): Infer<T> { 70 - const result = schema.safeParse(data); 71 - 72 - // Check for async validation 73 - if (result instanceof Promise) { 74 - throw new AsyncValidationError(); 75 - } 76 - 77 - if (!result.success) { 78 - throw new ValidationError(result.error.issues, "replace"); 79 - } 80 - return result.data as Infer<T>; 81 - } 82 - 83 - export class Model<T extends Schema> { 84 - private collection: Collection<Infer<T>>; 85 - private schema: T; 86 - 87 - constructor(collectionName: string, schema: T) { 88 - this.collection = getDb().collection<Infer<T>>(collectionName); 89 - this.schema = schema; 90 - } 91 - 92 - async insertOne( 93 - data: Input<T>, 94 - options?: InsertOneOptions 95 - ): Promise<InsertOneResult<Infer<T>>> { 96 - const validatedData = parse(this.schema, data); 97 - return await this.collection.insertOne( 98 - validatedData as OptionalUnlessRequiredId<Infer<T>>, 99 - options 100 - ); 101 - } 102 - 103 - async insertMany( 104 - data: Input<T>[], 105 - options?: BulkWriteOptions 106 - ): Promise<InsertManyResult<Infer<T>>> { 107 - const validatedData = data.map((item) => parse(this.schema, item)); 108 - return await this.collection.insertMany( 109 - validatedData as OptionalUnlessRequiredId<Infer<T>>[], 110 - options 111 - ); 112 - } 113 - 114 - async find( 115 - query: Filter<Infer<T>>, 116 - options?: FindOptions 117 - ): Promise<(WithId<Infer<T>>)[]> { 118 - return await this.collection.find(query, options).toArray(); 119 - } 120 - 121 - async findOne( 122 - query: Filter<Infer<T>>, 123 - options?: FindOptions 124 - ): Promise<WithId<Infer<T>> | null> { 125 - return await this.collection.findOne(query, options); 126 - } 127 - 128 - async findById( 129 - id: string | ObjectId, 130 - options?: FindOptions 131 - ): Promise<WithId<Infer<T>> | null> { 132 - const objectId = typeof id === "string" ? new ObjectId(id) : id; 133 - return await this.findOne({ _id: objectId } as Filter<Infer<T>>, options); 134 - } 135 - 136 - async update( 137 - query: Filter<Infer<T>>, 138 - data: Partial<z.infer<T>>, 139 - options?: UpdateOptions 140 - ): Promise<UpdateResult<Infer<T>>> { 141 - const validatedData = parsePartial(this.schema, data); 142 - return await this.collection.updateMany( 143 - query, 144 - { $set: validatedData as Partial<Infer<T>> }, 145 - options 146 - ); 147 - } 148 - 149 - async updateOne( 150 - query: Filter<Infer<T>>, 151 - data: Partial<z.infer<T>>, 152 - options?: UpdateOptions 153 - ): Promise<UpdateResult<Infer<T>>> { 154 - const validatedData = parsePartial(this.schema, data); 155 - return await this.collection.updateOne( 156 - query, 157 - { $set: validatedData as Partial<Infer<T>> }, 158 - options 159 - ); 160 - } 161 - 162 - async replaceOne( 163 - query: Filter<Infer<T>>, 164 - data: Input<T>, 165 - options?: ReplaceOptions 166 - ): Promise<UpdateResult<Infer<T>>> { 167 - const validatedData = parseReplace(this.schema, data); 168 - // Remove _id from validatedData for replaceOne (it will use the query's _id) 169 - const { _id, ...withoutId } = validatedData as Infer<T> & { _id?: unknown }; 170 - return await this.collection.replaceOne( 171 - query, 172 - withoutId as Infer<T>, 173 - options 174 - ); 175 - } 176 - 177 - async delete( 178 - query: Filter<Infer<T>>, 179 - options?: DeleteOptions 180 - ): Promise<DeleteResult> { 181 - return await this.collection.deleteMany(query, options); 182 - } 183 - 184 - async deleteOne( 185 - query: Filter<Infer<T>>, 186 - options?: DeleteOptions 187 - ): Promise<DeleteResult> { 188 - return await this.collection.deleteOne(query, options); 189 - } 190 - 191 - async count( 192 - query: Filter<Infer<T>>, 193 - options?: CountDocumentsOptions 194 - ): Promise<number> { 195 - return await this.collection.countDocuments(query, options); 196 - } 197 - 198 - async aggregate( 199 - pipeline: Document[], 200 - options?: AggregateOptions 201 - ): Promise<Document[]> { 202 - return await this.collection.aggregate(pipeline, options).toArray(); 203 - } 204 - 205 - // Pagination support for find 206 - async findPaginated( 207 - query: Filter<Infer<T>>, 208 - options: { skip?: number; limit?: number; sort?: Document } = {}, 209 - ): Promise<(WithId<Infer<T>>)[]> { 210 - return await this.collection 211 - .find(query) 212 - .skip(options.skip ?? 0) 213 - .limit(options.limit ?? 10) 214 - .sort(options.sort ?? {}) 215 - .toArray(); 216 - } 217 - 218 - // Index Management Methods 219 - 220 - /** 221 - * Create a single index on the collection 222 - * @param keys - Index specification (e.g., { email: 1 } or { name: "text" }) 223 - * @param options - Index creation options (unique, sparse, expireAfterSeconds, etc.) 224 - * @returns The name of the created index 225 - */ 226 - async createIndex( 227 - keys: IndexSpecification, 228 - options?: CreateIndexesOptions, 229 - ): Promise<string> { 230 - return await this.collection.createIndex(keys, options); 231 - } 232 - 233 - /** 234 - * Create multiple indexes on the collection 235 - * @param indexes - Array of index descriptions 236 - * @param options - Index creation options 237 - * @returns Array of index names created 238 - */ 239 - async createIndexes( 240 - indexes: IndexDescription[], 241 - options?: CreateIndexesOptions, 242 - ): Promise<string[]> { 243 - return await this.collection.createIndexes(indexes, options); 244 - } 245 - 246 - /** 247 - * Drop a single index from the collection 248 - * @param index - Index name or specification 249 - * @param options - Drop index options 250 - */ 251 - async dropIndex( 252 - index: string | IndexSpecification, 253 - options?: DropIndexesOptions, 254 - ): Promise<void> { 255 - // MongoDB driver accepts string or IndexSpecification 256 - await this.collection.dropIndex(index as string, options); 257 - } 258 - 259 - /** 260 - * Drop all indexes from the collection (except _id index) 261 - * @param options - Drop index options 262 - */ 263 - async dropIndexes(options?: DropIndexesOptions): Promise<void> { 264 - await this.collection.dropIndexes(options); 265 - } 266 - 267 - /** 268 - * List all indexes on the collection 269 - * @param options - List indexes options 270 - * @returns Array of index information 271 - */ 272 - async listIndexes( 273 - options?: ListIndexesOptions, 274 - ): Promise<IndexDescription[]> { 275 - const indexes = await this.collection.listIndexes(options).toArray(); 276 - return indexes as IndexDescription[]; 277 - } 278 - 279 - /** 280 - * Get index information by name 281 - * @param indexName - Name of the index 282 - * @returns Index description or null if not found 283 - */ 284 - async getIndex(indexName: string): Promise<IndexDescription | null> { 285 - const indexes = await this.listIndexes(); 286 - return indexes.find((idx) => idx.name === indexName) || null; 287 - } 288 - 289 - /** 290 - * Check if an index exists 291 - * @param indexName - Name of the index 292 - * @returns True if index exists, false otherwise 293 - */ 294 - async indexExists(indexName: string): Promise<boolean> { 295 - const index = await this.getIndex(indexName); 296 - return index !== null; 297 - } 298 - 299 - /** 300 - * Synchronize indexes - create indexes if they don't exist, update if they differ 301 - * This is useful for ensuring indexes match your schema definition 302 - * @param indexes - Array of index descriptions to synchronize 303 - * @param options - Options for index creation 304 - */ 305 - async syncIndexes( 306 - indexes: IndexDescription[], 307 - options?: CreateIndexesOptions, 308 - ): Promise<string[]> { 309 - const existingIndexes = await this.listIndexes(); 310 - 311 - const indexesToCreate: IndexDescription[] = []; 312 - 313 - for (const index of indexes) { 314 - const indexName = index.name || this._generateIndexName(index.key); 315 - const existingIndex = existingIndexes.find( 316 - (idx) => idx.name === indexName, 317 - ); 318 - 319 - if (!existingIndex) { 320 - indexesToCreate.push(index); 321 - } else if ( 322 - JSON.stringify(existingIndex.key) !== JSON.stringify(index.key) 323 - ) { 324 - // Index exists but keys differ - drop and recreate 325 - await this.dropIndex(indexName); 326 - indexesToCreate.push(index); 327 - } 328 - // If index exists and matches, skip it 329 - } 330 - 331 - const created: string[] = []; 332 - if (indexesToCreate.length > 0) { 333 - const names = await this.createIndexes(indexesToCreate, options); 334 - created.push(...names); 335 - } 336 - 337 - return created; 338 - } 339 - 340 - /** 341 - * Helper method to generate index name from key specification 342 - */ 343 - private _generateIndexName(keys: IndexSpecification): string { 344 - if (typeof keys === "string") { 345 - return keys; 346 - } 347 - const entries = Object.entries(keys as Record<string, number | string>); 348 - return entries.map(([field, direction]) => `${field}_${direction}`).join("_"); 349 - } 350 - }
+264
model/core.ts
··· 1 + import type { z } from "@zod/zod"; 2 + import type { 3 + Collection, 4 + DeleteResult, 5 + Document, 6 + Filter, 7 + InsertManyResult, 8 + InsertOneResult, 9 + InsertOneOptions, 10 + FindOptions, 11 + UpdateOptions, 12 + ReplaceOptions, 13 + DeleteOptions, 14 + CountDocumentsOptions, 15 + AggregateOptions, 16 + OptionalUnlessRequiredId, 17 + UpdateResult, 18 + WithId, 19 + BulkWriteOptions, 20 + } from "mongodb"; 21 + import { ObjectId } from "mongodb"; 22 + import type { Schema, Infer, Input } from "../types.ts"; 23 + import { parse, parsePartial, parseReplace } from "./validation.ts"; 24 + 25 + /** 26 + * Core CRUD operations for the Model class 27 + * 28 + * This module contains all basic create, read, update, and delete operations 29 + * with automatic Zod validation and transaction support. 30 + */ 31 + 32 + /** 33 + * Insert a single document into the collection 34 + * 35 + * @param collection - MongoDB collection 36 + * @param schema - Zod schema for validation 37 + * @param data - Document data to insert 38 + * @param options - Insert options (including session for transactions) 39 + * @returns Insert result with insertedId 40 + */ 41 + export async function insertOne<T extends Schema>( 42 + collection: Collection<Infer<T>>, 43 + schema: T, 44 + data: Input<T>, 45 + options?: InsertOneOptions 46 + ): Promise<InsertOneResult<Infer<T>>> { 47 + const validatedData = parse(schema, data); 48 + return await collection.insertOne( 49 + validatedData as OptionalUnlessRequiredId<Infer<T>>, 50 + options 51 + ); 52 + } 53 + 54 + /** 55 + * Insert multiple documents into the collection 56 + * 57 + * @param collection - MongoDB collection 58 + * @param schema - Zod schema for validation 59 + * @param data - Array of document data to insert 60 + * @param options - Insert options (including session for transactions) 61 + * @returns Insert result with insertedIds 62 + */ 63 + export async function insertMany<T extends Schema>( 64 + collection: Collection<Infer<T>>, 65 + schema: T, 66 + data: Input<T>[], 67 + options?: BulkWriteOptions 68 + ): Promise<InsertManyResult<Infer<T>>> { 69 + const validatedData = data.map((item) => parse(schema, item)); 70 + return await collection.insertMany( 71 + validatedData as OptionalUnlessRequiredId<Infer<T>>[], 72 + options 73 + ); 74 + } 75 + 76 + /** 77 + * Find multiple documents matching the query 78 + * 79 + * @param collection - MongoDB collection 80 + * @param query - MongoDB query filter 81 + * @param options - Find options (including session for transactions) 82 + * @returns Array of matching documents 83 + */ 84 + export async function find<T extends Schema>( 85 + collection: Collection<Infer<T>>, 86 + query: Filter<Infer<T>>, 87 + options?: FindOptions 88 + ): Promise<(WithId<Infer<T>>)[]> { 89 + return await collection.find(query, options).toArray(); 90 + } 91 + 92 + /** 93 + * Find a single document matching the query 94 + * 95 + * @param collection - MongoDB collection 96 + * @param query - MongoDB query filter 97 + * @param options - Find options (including session for transactions) 98 + * @returns Matching document or null if not found 99 + */ 100 + export async function findOne<T extends Schema>( 101 + collection: Collection<Infer<T>>, 102 + query: Filter<Infer<T>>, 103 + options?: FindOptions 104 + ): Promise<WithId<Infer<T>> | null> { 105 + return await collection.findOne(query, options); 106 + } 107 + 108 + /** 109 + * Find a document by its MongoDB ObjectId 110 + * 111 + * @param collection - MongoDB collection 112 + * @param id - Document ID (string or ObjectId) 113 + * @param options - Find options (including session for transactions) 114 + * @returns Matching document or null if not found 115 + */ 116 + export async function findById<T extends Schema>( 117 + collection: Collection<Infer<T>>, 118 + id: string | ObjectId, 119 + options?: FindOptions 120 + ): Promise<WithId<Infer<T>> | null> { 121 + const objectId = typeof id === "string" ? new ObjectId(id) : id; 122 + return await findOne(collection, { _id: objectId } as Filter<Infer<T>>, options); 123 + } 124 + 125 + /** 126 + * Update multiple documents matching the query 127 + * 128 + * @param collection - MongoDB collection 129 + * @param schema - Zod schema for validation 130 + * @param query - MongoDB query filter 131 + * @param data - Partial data to update 132 + * @param options - Update options (including session for transactions) 133 + * @returns Update result 134 + */ 135 + export async function update<T extends Schema>( 136 + collection: Collection<Infer<T>>, 137 + schema: T, 138 + query: Filter<Infer<T>>, 139 + data: Partial<z.infer<T>>, 140 + options?: UpdateOptions 141 + ): Promise<UpdateResult<Infer<T>>> { 142 + const validatedData = parsePartial(schema, data); 143 + return await collection.updateMany( 144 + query, 145 + { $set: validatedData as Partial<Infer<T>> }, 146 + options 147 + ); 148 + } 149 + 150 + /** 151 + * Update a single document matching the query 152 + * 153 + * @param collection - MongoDB collection 154 + * @param schema - Zod schema for validation 155 + * @param query - MongoDB query filter 156 + * @param data - Partial data to update 157 + * @param options - Update options (including session for transactions) 158 + * @returns Update result 159 + */ 160 + export async function updateOne<T extends Schema>( 161 + collection: Collection<Infer<T>>, 162 + schema: T, 163 + query: Filter<Infer<T>>, 164 + data: Partial<z.infer<T>>, 165 + options?: UpdateOptions 166 + ): Promise<UpdateResult<Infer<T>>> { 167 + const validatedData = parsePartial(schema, data); 168 + return await collection.updateOne( 169 + query, 170 + { $set: validatedData as Partial<Infer<T>> }, 171 + options 172 + ); 173 + } 174 + 175 + /** 176 + * Replace a single document matching the query 177 + * 178 + * @param collection - MongoDB collection 179 + * @param schema - Zod schema for validation 180 + * @param query - MongoDB query filter 181 + * @param data - Complete document data for replacement 182 + * @param options - Replace options (including session for transactions) 183 + * @returns Update result 184 + */ 185 + export async function replaceOne<T extends Schema>( 186 + collection: Collection<Infer<T>>, 187 + schema: T, 188 + query: Filter<Infer<T>>, 189 + data: Input<T>, 190 + options?: ReplaceOptions 191 + ): Promise<UpdateResult<Infer<T>>> { 192 + const validatedData = parseReplace(schema, data); 193 + // Remove _id from validatedData for replaceOne (it will use the query's _id) 194 + const { _id, ...withoutId } = validatedData as Infer<T> & { _id?: unknown }; 195 + return await collection.replaceOne( 196 + query, 197 + withoutId as Infer<T>, 198 + options 199 + ); 200 + } 201 + 202 + /** 203 + * Delete multiple documents matching the query 204 + * 205 + * @param collection - MongoDB collection 206 + * @param query - MongoDB query filter 207 + * @param options - Delete options (including session for transactions) 208 + * @returns Delete result 209 + */ 210 + export async function deleteMany<T extends Schema>( 211 + collection: Collection<Infer<T>>, 212 + query: Filter<Infer<T>>, 213 + options?: DeleteOptions 214 + ): Promise<DeleteResult> { 215 + return await collection.deleteMany(query, options); 216 + } 217 + 218 + /** 219 + * Delete a single document matching the query 220 + * 221 + * @param collection - MongoDB collection 222 + * @param query - MongoDB query filter 223 + * @param options - Delete options (including session for transactions) 224 + * @returns Delete result 225 + */ 226 + export async function deleteOne<T extends Schema>( 227 + collection: Collection<Infer<T>>, 228 + query: Filter<Infer<T>>, 229 + options?: DeleteOptions 230 + ): Promise<DeleteResult> { 231 + return await collection.deleteOne(query, options); 232 + } 233 + 234 + /** 235 + * Count documents matching the query 236 + * 237 + * @param collection - MongoDB collection 238 + * @param query - MongoDB query filter 239 + * @param options - Count options (including session for transactions) 240 + * @returns Number of matching documents 241 + */ 242 + export async function count<T extends Schema>( 243 + collection: Collection<Infer<T>>, 244 + query: Filter<Infer<T>>, 245 + options?: CountDocumentsOptions 246 + ): Promise<number> { 247 + return await collection.countDocuments(query, options); 248 + } 249 + 250 + /** 251 + * Execute an aggregation pipeline 252 + * 253 + * @param collection - MongoDB collection 254 + * @param pipeline - MongoDB aggregation pipeline 255 + * @param options - Aggregate options (including session for transactions) 256 + * @returns Array of aggregation results 257 + */ 258 + export async function aggregate<T extends Schema>( 259 + collection: Collection<Infer<T>>, 260 + pipeline: Document[], 261 + options?: AggregateOptions 262 + ): Promise<Document[]> { 263 + return await collection.aggregate(pipeline, options).toArray(); 264 + }
+355
model/index.ts
··· 1 + import type { z } from "@zod/zod"; 2 + import 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"; 25 + import type { ObjectId } from "mongodb"; 26 + import { getDb } from "../client/connection.ts"; 27 + import type { Schema, Infer, Input } from "../types.ts"; 28 + import * as core from "./core.ts"; 29 + import * as indexes from "./indexes.ts"; 30 + import * 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 + */ 49 + export class Model<T extends Schema> { 50 + private collection: Collection<Infer<T>>; 51 + private schema: T; 52 + 53 + constructor(collectionName: string, schema: T) { 54 + this.collection = getDb().collection<Infer<T>>(collectionName); 55 + this.schema = schema; 56 + } 57 + 58 + // ============================================================================ 59 + // CRUD Operations (delegated to core.ts) 60 + // ============================================================================ 61 + 62 + /** 63 + * Insert a single document into the collection 64 + * 65 + * @param data - Document data to insert 66 + * @param options - Insert options (including session for transactions) 67 + * @returns Insert result with insertedId 68 + */ 69 + async insertOne( 70 + data: Input<T>, 71 + options?: InsertOneOptions 72 + ): Promise<InsertOneResult<Infer<T>>> { 73 + return await core.insertOne(this.collection, this.schema, data, options); 74 + } 75 + 76 + /** 77 + * Insert multiple documents into the collection 78 + * 79 + * @param data - Array of document data to insert 80 + * @param options - Insert options (including session for transactions) 81 + * @returns Insert result with insertedIds 82 + */ 83 + async insertMany( 84 + data: Input<T>[], 85 + options?: BulkWriteOptions 86 + ): Promise<InsertManyResult<Infer<T>>> { 87 + return await core.insertMany(this.collection, this.schema, data, options); 88 + } 89 + 90 + /** 91 + * Find multiple documents matching the query 92 + * 93 + * @param query - MongoDB query filter 94 + * @param options - Find options (including session for transactions) 95 + * @returns Array of matching documents 96 + */ 97 + async find( 98 + query: Filter<Infer<T>>, 99 + options?: FindOptions 100 + ): Promise<(WithId<Infer<T>>)[]> { 101 + return await core.find(this.collection, query, options); 102 + } 103 + 104 + /** 105 + * Find a single document matching the query 106 + * 107 + * @param query - MongoDB query filter 108 + * @param options - Find options (including session for transactions) 109 + * @returns Matching document or null if not found 110 + */ 111 + async findOne( 112 + query: Filter<Infer<T>>, 113 + options?: FindOptions 114 + ): Promise<WithId<Infer<T>> | null> { 115 + return await core.findOne(this.collection, query, options); 116 + } 117 + 118 + /** 119 + * Find a document by its MongoDB ObjectId 120 + * 121 + * @param id - Document ID (string or ObjectId) 122 + * @param options - Find options (including session for transactions) 123 + * @returns Matching document or null if not found 124 + */ 125 + async findById( 126 + id: string | ObjectId, 127 + options?: FindOptions 128 + ): Promise<WithId<Infer<T>> | null> { 129 + return await core.findById(this.collection, id, options); 130 + } 131 + 132 + /** 133 + * Update multiple documents matching the query 134 + * 135 + * @param query - MongoDB query filter 136 + * @param data - Partial data to update 137 + * @param options - Update options (including session for transactions) 138 + * @returns Update result 139 + */ 140 + async update( 141 + query: Filter<Infer<T>>, 142 + data: Partial<z.infer<T>>, 143 + options?: UpdateOptions 144 + ): Promise<UpdateResult<Infer<T>>> { 145 + return await core.update(this.collection, this.schema, query, data, options); 146 + } 147 + 148 + /** 149 + * Update a single document matching the query 150 + * 151 + * @param query - MongoDB query filter 152 + * @param data - Partial data to update 153 + * @param options - Update options (including session for transactions) 154 + * @returns Update result 155 + */ 156 + async updateOne( 157 + query: Filter<Infer<T>>, 158 + data: Partial<z.infer<T>>, 159 + options?: UpdateOptions 160 + ): Promise<UpdateResult<Infer<T>>> { 161 + return await core.updateOne(this.collection, this.schema, query, data, options); 162 + } 163 + 164 + /** 165 + * Replace a single document matching the query 166 + * 167 + * @param query - MongoDB query filter 168 + * @param data - Complete document data for replacement 169 + * @param options - Replace options (including session for transactions) 170 + * @returns Update result 171 + */ 172 + async replaceOne( 173 + query: Filter<Infer<T>>, 174 + data: Input<T>, 175 + options?: ReplaceOptions 176 + ): Promise<UpdateResult<Infer<T>>> { 177 + return await core.replaceOne(this.collection, this.schema, query, data, options); 178 + } 179 + 180 + /** 181 + * Delete multiple documents matching the query 182 + * 183 + * @param query - MongoDB query filter 184 + * @param options - Delete options (including session for transactions) 185 + * @returns Delete result 186 + */ 187 + async delete( 188 + query: Filter<Infer<T>>, 189 + options?: DeleteOptions 190 + ): Promise<DeleteResult> { 191 + return await core.deleteMany(this.collection, query, options); 192 + } 193 + 194 + /** 195 + * Delete a single document matching the query 196 + * 197 + * @param query - MongoDB query filter 198 + * @param options - Delete options (including session for transactions) 199 + * @returns Delete result 200 + */ 201 + async deleteOne( 202 + query: Filter<Infer<T>>, 203 + options?: DeleteOptions 204 + ): Promise<DeleteResult> { 205 + return await core.deleteOne(this.collection, query, options); 206 + } 207 + 208 + /** 209 + * Count documents matching the query 210 + * 211 + * @param query - MongoDB query filter 212 + * @param options - Count options (including session for transactions) 213 + * @returns Number of matching documents 214 + */ 215 + async count( 216 + query: Filter<Infer<T>>, 217 + options?: CountDocumentsOptions 218 + ): Promise<number> { 219 + return await core.count(this.collection, query, options); 220 + } 221 + 222 + /** 223 + * Execute an aggregation pipeline 224 + * 225 + * @param pipeline - MongoDB aggregation pipeline 226 + * @param options - Aggregate options (including session for transactions) 227 + * @returns Array of aggregation results 228 + */ 229 + async aggregate( 230 + pipeline: Document[], 231 + options?: AggregateOptions 232 + ): Promise<Document[]> { 233 + return await core.aggregate(this.collection, pipeline, options); 234 + } 235 + 236 + // ============================================================================ 237 + // Pagination (delegated to pagination.ts) 238 + // ============================================================================ 239 + 240 + /** 241 + * Find documents with pagination support 242 + * 243 + * @param query - MongoDB query filter 244 + * @param options - Pagination options (skip, limit, sort) 245 + * @returns Array of matching documents 246 + */ 247 + async findPaginated( 248 + query: Filter<Infer<T>>, 249 + options: { skip?: number; limit?: number; sort?: Document } = {}, 250 + ): Promise<(WithId<Infer<T>>)[]> { 251 + return await pagination.findPaginated(this.collection, query, options); 252 + } 253 + 254 + // ============================================================================ 255 + // Index Management (delegated to indexes.ts) 256 + // ============================================================================ 257 + 258 + /** 259 + * Create a single index on the collection 260 + * 261 + * @param keys - Index specification (e.g., { email: 1 } or { name: "text" }) 262 + * @param options - Index creation options (unique, sparse, expireAfterSeconds, etc.) 263 + * @returns The name of the created index 264 + */ 265 + async createIndex( 266 + keys: IndexSpecification, 267 + options?: CreateIndexesOptions, 268 + ): Promise<string> { 269 + return await indexes.createIndex(this.collection, keys, options); 270 + } 271 + 272 + /** 273 + * Create multiple indexes on the collection 274 + * 275 + * @param indexes - Array of index descriptions 276 + * @param options - Index creation options 277 + * @returns Array of index names created 278 + */ 279 + async createIndexes( 280 + indexList: IndexDescription[], 281 + options?: CreateIndexesOptions, 282 + ): Promise<string[]> { 283 + return await indexes.createIndexes(this.collection, indexList, options); 284 + } 285 + 286 + /** 287 + * Drop a single index from the collection 288 + * 289 + * @param index - Index name or specification 290 + * @param options - Drop index options 291 + */ 292 + async dropIndex( 293 + index: string | IndexSpecification, 294 + options?: DropIndexesOptions, 295 + ): Promise<void> { 296 + return await indexes.dropIndex(this.collection, index, options); 297 + } 298 + 299 + /** 300 + * Drop all indexes from the collection (except _id index) 301 + * 302 + * @param options - Drop index options 303 + */ 304 + async dropIndexes(options?: DropIndexesOptions): Promise<void> { 305 + return await indexes.dropIndexes(this.collection, options); 306 + } 307 + 308 + /** 309 + * List all indexes on the collection 310 + * 311 + * @param options - List indexes options 312 + * @returns Array of index information 313 + */ 314 + async listIndexes( 315 + options?: ListIndexesOptions, 316 + ): Promise<IndexDescription[]> { 317 + return await indexes.listIndexes(this.collection, options); 318 + } 319 + 320 + /** 321 + * Get index information by name 322 + * 323 + * @param indexName - Name of the index 324 + * @returns Index description or null if not found 325 + */ 326 + async getIndex(indexName: string): Promise<IndexDescription | null> { 327 + return await indexes.getIndex(this.collection, indexName); 328 + } 329 + 330 + /** 331 + * Check if an index exists 332 + * 333 + * @param indexName - Name of the index 334 + * @returns True if index exists, false otherwise 335 + */ 336 + async indexExists(indexName: string): Promise<boolean> { 337 + return await indexes.indexExists(this.collection, indexName); 338 + } 339 + 340 + /** 341 + * Synchronize indexes - create indexes if they don't exist, update if they differ 342 + * 343 + * This is useful for ensuring indexes match your schema definition 344 + * 345 + * @param indexes - Array of index descriptions to synchronize 346 + * @param options - Options for index creation 347 + * @returns Array of index names that were created 348 + */ 349 + async syncIndexes( 350 + indexList: IndexDescription[], 351 + options?: CreateIndexesOptions, 352 + ): Promise<string[]> { 353 + return await indexes.syncIndexes(this.collection, indexList, options); 354 + } 355 + }
+180
model/indexes.ts
··· 1 + import type { 2 + Collection, 3 + CreateIndexesOptions, 4 + DropIndexesOptions, 5 + IndexDescription, 6 + IndexSpecification, 7 + ListIndexesOptions, 8 + } from "mongodb"; 9 + import type { Schema, Infer } from "../types.ts"; 10 + 11 + /** 12 + * Index management operations for the Model class 13 + * 14 + * This module contains all index-related operations including creation, 15 + * deletion, listing, and synchronization of indexes. 16 + */ 17 + 18 + /** 19 + * Create a single index on the collection 20 + * 21 + * @param collection - MongoDB collection 22 + * @param keys - Index specification (e.g., { email: 1 } or { name: "text" }) 23 + * @param options - Index creation options (unique, sparse, expireAfterSeconds, etc.) 24 + * @returns The name of the created index 25 + */ 26 + export async function createIndex<T extends Schema>( 27 + collection: Collection<Infer<T>>, 28 + keys: IndexSpecification, 29 + options?: CreateIndexesOptions, 30 + ): Promise<string> { 31 + return await collection.createIndex(keys, options); 32 + } 33 + 34 + /** 35 + * Create multiple indexes on the collection 36 + * 37 + * @param collection - MongoDB collection 38 + * @param indexes - Array of index descriptions 39 + * @param options - Index creation options 40 + * @returns Array of index names created 41 + */ 42 + export async function createIndexes<T extends Schema>( 43 + collection: Collection<Infer<T>>, 44 + indexes: IndexDescription[], 45 + options?: CreateIndexesOptions, 46 + ): Promise<string[]> { 47 + return await collection.createIndexes(indexes, options); 48 + } 49 + 50 + /** 51 + * Drop a single index from the collection 52 + * 53 + * @param collection - MongoDB collection 54 + * @param index - Index name or specification 55 + * @param options - Drop index options 56 + */ 57 + export async function dropIndex<T extends Schema>( 58 + collection: Collection<Infer<T>>, 59 + index: string | IndexSpecification, 60 + options?: DropIndexesOptions, 61 + ): Promise<void> { 62 + await collection.dropIndex(index as string, options); 63 + } 64 + 65 + /** 66 + * Drop all indexes from the collection (except _id index) 67 + * 68 + * @param collection - MongoDB collection 69 + * @param options - Drop index options 70 + */ 71 + export async function dropIndexes<T extends Schema>( 72 + collection: Collection<Infer<T>>, 73 + options?: DropIndexesOptions 74 + ): Promise<void> { 75 + await collection.dropIndexes(options); 76 + } 77 + 78 + /** 79 + * List all indexes on the collection 80 + * 81 + * @param collection - MongoDB collection 82 + * @param options - List indexes options 83 + * @returns Array of index information 84 + */ 85 + export async function listIndexes<T extends Schema>( 86 + collection: Collection<Infer<T>>, 87 + options?: ListIndexesOptions, 88 + ): Promise<IndexDescription[]> { 89 + const indexes = await collection.listIndexes(options).toArray(); 90 + return indexes as IndexDescription[]; 91 + } 92 + 93 + /** 94 + * Get index information by name 95 + * 96 + * @param collection - MongoDB collection 97 + * @param indexName - Name of the index 98 + * @returns Index description or null if not found 99 + */ 100 + export async function getIndex<T extends Schema>( 101 + collection: Collection<Infer<T>>, 102 + indexName: string 103 + ): Promise<IndexDescription | null> { 104 + const indexes = await listIndexes(collection); 105 + return indexes.find((idx) => idx.name === indexName) || null; 106 + } 107 + 108 + /** 109 + * Check if an index exists 110 + * 111 + * @param collection - MongoDB collection 112 + * @param indexName - Name of the index 113 + * @returns True if index exists, false otherwise 114 + */ 115 + export async function indexExists<T extends Schema>( 116 + collection: Collection<Infer<T>>, 117 + indexName: string 118 + ): Promise<boolean> { 119 + const index = await getIndex(collection, indexName); 120 + return index !== null; 121 + } 122 + 123 + /** 124 + * Synchronize indexes - create indexes if they don't exist, update if they differ 125 + * 126 + * This is useful for ensuring indexes match your schema definition 127 + * 128 + * @param collection - MongoDB collection 129 + * @param indexes - Array of index descriptions to synchronize 130 + * @param options - Options for index creation 131 + * @returns Array of index names that were created 132 + */ 133 + export async function syncIndexes<T extends Schema>( 134 + collection: Collection<Infer<T>>, 135 + indexes: IndexDescription[], 136 + options?: CreateIndexesOptions, 137 + ): Promise<string[]> { 138 + const existingIndexes = await listIndexes(collection); 139 + const indexesToCreate: IndexDescription[] = []; 140 + 141 + for (const index of indexes) { 142 + const indexName = index.name || generateIndexName(index.key); 143 + const existingIndex = existingIndexes.find( 144 + (idx) => idx.name === indexName, 145 + ); 146 + 147 + if (!existingIndex) { 148 + indexesToCreate.push(index); 149 + } else if ( 150 + JSON.stringify(existingIndex.key) !== JSON.stringify(index.key) 151 + ) { 152 + // Index exists but keys differ - drop and recreate 153 + await dropIndex(collection, indexName); 154 + indexesToCreate.push(index); 155 + } 156 + // If index exists and matches, skip it 157 + } 158 + 159 + const created: string[] = []; 160 + if (indexesToCreate.length > 0) { 161 + const names = await createIndexes(collection, indexesToCreate, options); 162 + created.push(...names); 163 + } 164 + 165 + return created; 166 + } 167 + 168 + /** 169 + * Generate index name from key specification 170 + * 171 + * @param keys - Index specification 172 + * @returns Generated index name 173 + */ 174 + export function generateIndexName(keys: IndexSpecification): string { 175 + if (typeof keys === "string") { 176 + return keys; 177 + } 178 + const entries = Object.entries(keys as Record<string, number | string>); 179 + return entries.map(([field, direction]) => `${field}_${direction}`).join("_"); 180 + }
+43
model/pagination.ts
··· 1 + import type { 2 + Collection, 3 + Document, 4 + Filter, 5 + WithId, 6 + } from "mongodb"; 7 + import type { Schema, Infer } from "../types.ts"; 8 + 9 + /** 10 + * Pagination operations for the Model class 11 + * 12 + * This module contains pagination-related functionality for finding documents 13 + * with skip, limit, and sort options. 14 + */ 15 + 16 + /** 17 + * Find documents with pagination support 18 + * 19 + * @param collection - MongoDB collection 20 + * @param query - MongoDB query filter 21 + * @param options - Pagination options (skip, limit, sort) 22 + * @returns Array of matching documents 23 + * 24 + * @example 25 + * ```ts 26 + * const users = await findPaginated(collection, 27 + * { age: { $gte: 18 } }, 28 + * { skip: 0, limit: 10, sort: { createdAt: -1 } } 29 + * ); 30 + * ``` 31 + */ 32 + export async function findPaginated<T extends Schema>( 33 + collection: Collection<Infer<T>>, 34 + query: Filter<Infer<T>>, 35 + options: { skip?: number; limit?: number; sort?: Document } = {}, 36 + ): Promise<(WithId<Infer<T>>)[]> { 37 + return await collection 38 + .find(query) 39 + .skip(options.skip ?? 0) 40 + .limit(options.limit ?? 10) 41 + .sort(options.sort ?? {}) 42 + .toArray(); 43 + }
+75
model/validation.ts
··· 1 + import type { z } from "@zod/zod"; 2 + import type { Schema, Infer, Input } from "../types.ts"; 3 + import { ValidationError, AsyncValidationError } from "../errors.ts"; 4 + 5 + /** 6 + * Validate data for insert operations using Zod schema 7 + * 8 + * @param schema - Zod schema to validate against 9 + * @param data - Data to validate 10 + * @returns Validated and typed data 11 + * @throws {ValidationError} If validation fails 12 + * @throws {AsyncValidationError} If async validation is detected 13 + */ 14 + export function parse<T extends Schema>(schema: T, data: Input<T>): Infer<T> { 15 + const result = schema.safeParse(data); 16 + 17 + // Check for async validation 18 + if (result instanceof Promise) { 19 + throw new AsyncValidationError(); 20 + } 21 + 22 + if (!result.success) { 23 + throw new ValidationError(result.error.issues, "insert"); 24 + } 25 + return result.data as Infer<T>; 26 + } 27 + 28 + /** 29 + * Validate partial data for update operations using Zod schema 30 + * 31 + * @param schema - Zod schema to validate against 32 + * @param data - Partial data to validate 33 + * @returns Validated and typed partial data 34 + * @throws {ValidationError} If validation fails 35 + * @throws {AsyncValidationError} If async validation is detected 36 + */ 37 + export function parsePartial<T extends Schema>( 38 + schema: T, 39 + data: Partial<z.infer<T>>, 40 + ): Partial<z.infer<T>> { 41 + const result = schema.partial().safeParse(data); 42 + 43 + // Check for async validation 44 + if (result instanceof Promise) { 45 + throw new AsyncValidationError(); 46 + } 47 + 48 + if (!result.success) { 49 + throw new ValidationError(result.error.issues, "update"); 50 + } 51 + return result.data as Partial<z.infer<T>>; 52 + } 53 + 54 + /** 55 + * Validate data for replace operations using Zod schema 56 + * 57 + * @param schema - Zod schema to validate against 58 + * @param data - Data to validate 59 + * @returns Validated and typed data 60 + * @throws {ValidationError} If validation fails 61 + * @throws {AsyncValidationError} If async validation is detected 62 + */ 63 + export function parseReplace<T extends Schema>(schema: T, data: Input<T>): Infer<T> { 64 + const result = schema.safeParse(data); 65 + 66 + // Check for async validation 67 + if (result instanceof Promise) { 68 + throw new AsyncValidationError(); 69 + } 70 + 71 + if (!result.success) { 72 + throw new ValidationError(result.error.issues, "replace"); 73 + } 74 + return result.data as Infer<T>; 75 + }
-11
schema.ts
··· 1 - import type { z } from "@zod/zod"; 2 - import type { ObjectId } from "mongodb"; 3 - 4 - type Schema = z.ZodObject; 5 - type Infer<T extends Schema> = z.infer<T>; 6 - 7 - export type InferModel<T extends Schema> = Infer<T> & { 8 - _id?: ObjectId; 9 - }; 10 - 11 - export type Input<T extends Schema> = z.input<T>;
+6 -6
tests/errors_test.ts
··· 46 46 47 47 await assertRejects( 48 48 async () => { 49 - await UserModel.insertOne({ name: "", email: "invalid" } as any); 49 + await UserModel.insertOne({ name: "", email: "invalid" }); 50 50 }, 51 51 ValidationError, 52 52 "Validation failed on insert" ··· 65 65 const UserModel = new Model("users", userSchema); 66 66 67 67 try { 68 - await UserModel.insertOne({ name: "", email: "invalid" } as any); 68 + await UserModel.insertOne({ name: "", email: "invalid" }); 69 69 throw new Error("Should have thrown ValidationError"); 70 70 } catch (error) { 71 71 assert(error instanceof ValidationError); ··· 116 116 117 117 await assertRejects( 118 118 async () => { 119 - await UserModel.replaceOne({ name: "Test" }, { name: "", email: "invalid" } as any); 119 + await UserModel.replaceOne({ name: "Test" }, { name: "", email: "invalid" }); 120 120 }, 121 121 ValidationError, 122 122 "Validation failed on replace" ··· 191 191 // Make sure not connected 192 192 await disconnect(); 193 193 194 - const { getDb } = await import("../client.ts"); 194 + const { getDb } = await import("../client/connection.ts"); 195 195 196 196 try { 197 197 getDb(); ··· 218 218 name: "", 219 219 email: "not-an-email", 220 220 age: -10, 221 - } as any); 221 + }); 222 222 throw new Error("Should have thrown ValidationError"); 223 223 } catch (error) { 224 224 assert(error instanceof ValidationError); ··· 249 249 const UserModel = new Model("users", userSchema); 250 250 251 251 try { 252 - await UserModel.insertOne({ name: "", email: "invalid" } as any); 252 + await UserModel.insertOne({ name: "", email: "invalid" }); 253 253 } catch (error) { 254 254 assert(error instanceof ValidationError); 255 255 assertEquals(error.name, "ValidationError");
+1 -1
tests/transactions_test.ts
··· 28 28 // Clean up database 29 29 if (replSet) { 30 30 try { 31 - const { getDb } = await import("../client.ts"); 31 + const { getDb } = await import("../client/connection.ts"); 32 32 const db = getDb(); 33 33 await db.dropDatabase(); 34 34 } catch {
+25
types.ts
··· 1 + import type { z } from "@zod/zod"; 2 + import type { Document, ObjectId } from "mongodb"; 3 + 4 + /** 5 + * Type alias for Zod schema objects 6 + */ 7 + export type Schema = z.ZodObject<z.ZodRawShape>; 8 + 9 + /** 10 + * Infer the TypeScript type from a Zod schema, including MongoDB Document 11 + */ 12 + export type Infer<T extends Schema> = z.infer<T> & Document; 13 + 14 + 15 + /** 16 + * Infer the model type from a Zod schema, including MongoDB Document and ObjectId 17 + */ 18 + export type InferModel<T extends Schema> = Infer<T> & { 19 + _id?: ObjectId; 20 + }; 21 + 22 + /** 23 + * Infer the input type for a Zod schema (handles defaults) 24 + */ 25 + export type Input<T extends Schema> = z.input<T>;