Thin MongoDB ODM built for Standard Schema
mongodb zod deno

re-org

knotbin.com 80092f8a 413c7a59

verified
-241
client.ts
···
-
import {
-
type Db,
-
type MongoClientOptions,
-
type ClientSession,
-
type TransactionOptions,
-
MongoClient
-
} from "mongodb";
-
import { ConnectionError } from "./errors.ts";
-
-
interface Connection {
-
client: MongoClient;
-
db: Db;
-
}
-
-
let connection: Connection | null = null;
-
-
export interface ConnectOptions extends MongoClientOptions {};
-
-
/**
-
* Health check details of the MongoDB connection
-
*
-
* @property healthy - Overall health status of the connection
-
* @property connected - Whether a connection is established
-
* @property responseTimeMs - Response time in milliseconds (if connection is healthy)
-
* @property error - Error message if health check failed
-
* @property timestamp - Timestamp when health check was performed
-
*/
-
export interface HealthCheckResult {
-
healthy: boolean;
-
connected: boolean;
-
responseTimeMs?: number;
-
error?: string;
-
timestamp: Date;
-
}
-
-
/**
-
* Connect to MongoDB with connection pooling, retry logic, and resilience options
-
*
-
* The MongoDB driver handles connection pooling and automatic retries.
-
* Retry logic is enabled by default for both reads and writes in MongoDB 4.2+.
-
*
-
* @param uri - MongoDB connection string
-
* @param dbName - Name of the database to connect to
-
* @param options - Connection options (pooling, retries, timeouts, etc.)
-
*
-
* @example
-
* Basic connection with pooling:
-
* ```ts
-
* await connect("mongodb://localhost:27017", "mydb", {
-
* maxPoolSize: 10,
-
* minPoolSize: 2,
-
* maxIdleTimeMS: 30000,
-
* connectTimeoutMS: 10000,
-
* socketTimeoutMS: 45000,
-
* });
-
* ```
-
*
-
* @example
-
* Production-ready connection with retry logic and resilience:
-
* ```ts
-
* await connect("mongodb://localhost:27017", "mydb", {
-
* // Connection pooling
-
* maxPoolSize: 10,
-
* minPoolSize: 2,
-
*
-
* // Automatic retry logic (enabled by default)
-
* retryReads: true, // Retry failed read operations
-
* retryWrites: true, // Retry failed write operations
-
*
-
* // Timeouts
-
* connectTimeoutMS: 10000, // Initial connection timeout
-
* socketTimeoutMS: 45000, // Socket operation timeout
-
* serverSelectionTimeoutMS: 10000, // Server selection timeout
-
*
-
* // Connection resilience
-
* maxIdleTimeMS: 30000, // Close idle connections
-
* heartbeatFrequencyMS: 10000, // Server health check interval
-
*
-
* // Optional: Compression for reduced bandwidth
-
* compressors: ['snappy', 'zlib'],
-
* });
-
* ```
-
*/
-
export async function connect(
-
uri: string,
-
dbName: string,
-
options?: ConnectOptions,
-
): Promise<Connection> {
-
if (connection) {
-
return connection;
-
}
-
-
try {
-
const client = new MongoClient(uri, options);
-
await client.connect();
-
const db = client.db(dbName);
-
-
connection = { client, db };
-
return connection;
-
} catch (error) {
-
throw new ConnectionError(
-
`Failed to connect to MongoDB: ${error instanceof Error ? error.message : String(error)}`,
-
uri
-
);
-
}
-
}
-
-
export async function disconnect(): Promise<void> {
-
if (connection) {
-
await connection.client.close();
-
connection = null;
-
}
-
}
-
-
/**
-
* Start a new client session for transactions
-
*
-
* Sessions must be ended when done using `endSession()`
-
*
-
* @example
-
* ```ts
-
* const session = await startSession();
-
* try {
-
* // use session
-
* } finally {
-
* await endSession(session);
-
* }
-
* ```
-
*/
-
export function startSession(): ClientSession {
-
if (!connection) {
-
throw new ConnectionError("MongoDB not connected. Call connect() first.");
-
}
-
return connection.client.startSession();
-
}
-
-
/**
-
* End a client session
-
*
-
* @param session - The session to end
-
*/
-
export async function endSession(session: ClientSession): Promise<void> {
-
await session.endSession();
-
}
-
-
/**
-
* Execute a function within a transaction
-
*
-
* Automatically handles session creation, transaction start/commit/abort, and cleanup.
-
* If the callback throws an error, the transaction is automatically aborted.
-
*
-
* @param callback - Async function to execute within the transaction. Receives the session as parameter.
-
* @param options - Optional transaction options (read/write concern, etc.)
-
* @returns The result from the callback function
-
*
-
* @example
-
* ```ts
-
* const result = await withTransaction(async (session) => {
-
* await UserModel.insertOne({ name: "Alice" }, { session });
-
* await OrderModel.insertOne({ userId: "123", total: 100 }, { session });
-
* return { success: true };
-
* });
-
* ```
-
*/
-
export async function withTransaction<T>(
-
callback: (session: ClientSession) => Promise<T>,
-
options?: TransactionOptions
-
): Promise<T> {
-
const session = await startSession();
-
-
try {
-
let result: T;
-
-
await session.withTransaction(async () => {
-
result = await callback(session);
-
}, options);
-
-
return result!;
-
} finally {
-
await endSession(session);
-
}
-
}
-
-
export function getDb(): Db {
-
if (!connection) {
-
throw new ConnectionError("MongoDB not connected. Call connect() first.");
-
}
-
return connection.db;
-
}
-
-
/**
-
* Check the health of the MongoDB connection
-
*
-
* Performs a ping operation to verify the database is responsive
-
* and returns detailed health information including response time.
-
*
-
* @example
-
* ```ts
-
* const health = await healthCheck();
-
* if (health.healthy) {
-
* console.log(`Database healthy (${health.responseTimeMs}ms)`);
-
* } else {
-
* console.error(`Database unhealthy: ${health.error}`);
-
* }
-
* ```
-
*/
-
export async function healthCheck(): Promise<HealthCheckResult> {
-
const timestamp = new Date();
-
-
// Check if connection exists
-
if (!connection) {
-
return {
-
healthy: false,
-
connected: false,
-
error: "No active connection. Call connect() first.",
-
timestamp,
-
};
-
}
-
-
try {
-
// Measure ping response time
-
const startTime = performance.now();
-
await connection.db.admin().ping();
-
const endTime = performance.now();
-
const responseTimeMs = Math.round(endTime - startTime);
-
-
return {
-
healthy: true,
-
connected: true,
-
responseTimeMs,
-
timestamp,
-
};
-
} catch (error) {
-
return {
-
healthy: false,
-
connected: true,
-
error: error instanceof Error ? error.message : String(error),
-
timestamp,
-
};
-
}
-
}
+126
client/connection.ts
···
+
import { type Db, type MongoClientOptions, MongoClient } from "mongodb";
+
import { ConnectionError } from "../errors.ts";
+
+
/**
+
* Connection management module
+
*
+
* Handles MongoDB connection lifecycle including connect, disconnect,
+
* and connection state management.
+
*/
+
+
export interface Connection {
+
client: MongoClient;
+
db: Db;
+
}
+
+
export interface ConnectOptions extends MongoClientOptions {}
+
+
// Singleton connection state
+
let connection: Connection | null = null;
+
+
/**
+
* Connect to MongoDB with connection pooling, retry logic, and resilience options
+
*
+
* The MongoDB driver handles connection pooling and automatic retries.
+
* Retry logic is enabled by default for both reads and writes in MongoDB 4.2+.
+
*
+
* @param uri - MongoDB connection string
+
* @param dbName - Name of the database to connect to
+
* @param options - Connection options (pooling, retries, timeouts, etc.)
+
* @returns Connection object with client and db
+
*
+
* @example
+
* Basic connection with pooling:
+
* ```ts
+
* await connect("mongodb://localhost:27017", "mydb", {
+
* maxPoolSize: 10,
+
* minPoolSize: 2,
+
* maxIdleTimeMS: 30000,
+
* connectTimeoutMS: 10000,
+
* socketTimeoutMS: 45000,
+
* });
+
* ```
+
*
+
* @example
+
* Production-ready connection with retry logic and resilience:
+
* ```ts
+
* await connect("mongodb://localhost:27017", "mydb", {
+
* // Connection pooling
+
* maxPoolSize: 10,
+
* minPoolSize: 2,
+
*
+
* // Automatic retry logic (enabled by default)
+
* retryReads: true, // Retry failed read operations
+
* retryWrites: true, // Retry failed write operations
+
*
+
* // Timeouts
+
* connectTimeoutMS: 10000, // Initial connection timeout
+
* socketTimeoutMS: 45000, // Socket operation timeout
+
* serverSelectionTimeoutMS: 10000, // Server selection timeout
+
*
+
* // Connection resilience
+
* maxIdleTimeMS: 30000, // Close idle connections
+
* heartbeatFrequencyMS: 10000, // Server health check interval
+
*
+
* // Optional: Compression for reduced bandwidth
+
* compressors: ['snappy', 'zlib'],
+
* });
+
* ```
+
*/
+
export async function connect(
+
uri: string,
+
dbName: string,
+
options?: ConnectOptions,
+
): Promise<Connection> {
+
if (connection) {
+
return connection;
+
}
+
+
try {
+
const client = new MongoClient(uri, options);
+
await client.connect();
+
const db = client.db(dbName);
+
+
connection = { client, db };
+
return connection;
+
} catch (error) {
+
throw new ConnectionError(
+
`Failed to connect to MongoDB: ${error instanceof Error ? error.message : String(error)}`,
+
uri
+
);
+
}
+
}
+
+
/**
+
* Disconnect from MongoDB and clean up resources
+
*/
+
export async function disconnect(): Promise<void> {
+
if (connection) {
+
await connection.client.close();
+
connection = null;
+
}
+
}
+
+
/**
+
* Get the current database connection
+
*
+
* @returns MongoDB Db instance
+
* @throws {ConnectionError} If not connected
+
* @internal
+
*/
+
export function getDb(): Db {
+
if (!connection) {
+
throw new ConnectionError("MongoDB not connected. Call connect() first.");
+
}
+
return connection.db;
+
}
+
+
/**
+
* Get the current connection state
+
*
+
* @returns Connection object or null if not connected
+
* @internal
+
*/
+
export function getConnection(): Connection | null {
+
return connection;
+
}
+80
client/health.ts
···
+
import { getConnection } from "./connection.ts";
+
+
/**
+
* Health check module
+
*
+
* Provides functionality for monitoring MongoDB connection health
+
* including ping operations and response time measurement.
+
*/
+
+
/**
+
* Health check details of the MongoDB connection
+
*
+
* @property healthy - Overall health status of the connection
+
* @property connected - Whether a connection is established
+
* @property responseTimeMs - Response time in milliseconds (if connection is healthy)
+
* @property error - Error message if health check failed
+
* @property timestamp - Timestamp when health check was performed
+
*/
+
export interface HealthCheckResult {
+
healthy: boolean;
+
connected: boolean;
+
responseTimeMs?: number;
+
error?: string;
+
timestamp: Date;
+
}
+
+
/**
+
* Check the health of the MongoDB connection
+
*
+
* Performs a ping operation to verify the database is responsive
+
* and returns detailed health information including response time.
+
*
+
* @returns Health check result with status and metrics
+
*
+
* @example
+
* ```ts
+
* const health = await healthCheck();
+
* if (health.healthy) {
+
* console.log(`Database healthy (${health.responseTimeMs}ms)`);
+
* } else {
+
* console.error(`Database unhealthy: ${health.error}`);
+
* }
+
* ```
+
*/
+
export async function healthCheck(): Promise<HealthCheckResult> {
+
const timestamp = new Date();
+
const connection = getConnection();
+
+
// Check if connection exists
+
if (!connection) {
+
return {
+
healthy: false,
+
connected: false,
+
error: "No active connection. Call connect() first.",
+
timestamp,
+
};
+
}
+
+
try {
+
// Measure ping response time
+
const startTime = performance.now();
+
await connection.db.admin().ping();
+
const endTime = performance.now();
+
const responseTimeMs = Math.round(endTime - startTime);
+
+
return {
+
healthy: true,
+
connected: true,
+
responseTimeMs,
+
timestamp,
+
};
+
} catch (error) {
+
return {
+
healthy: false,
+
connected: true,
+
error: error instanceof Error ? error.message : String(error),
+
timestamp,
+
};
+
}
+
}
+30
client/index.ts
···
+
/**
+
* Client module - MongoDB connection and session management
+
*
+
* This module provides all client-level functionality including:
+
* - Connection management (connect, disconnect)
+
* - Health monitoring (healthCheck)
+
* - Transaction support (startSession, endSession, withTransaction)
+
*/
+
+
// Re-export connection management
+
export {
+
connect,
+
disconnect,
+
getDb,
+
type ConnectOptions,
+
type Connection,
+
} from "./connection.ts";
+
+
// Re-export health monitoring
+
export {
+
healthCheck,
+
type HealthCheckResult,
+
} from "./health.ts";
+
+
// Re-export transaction management
+
export {
+
startSession,
+
endSession,
+
withTransaction,
+
} from "./transactions.ts";
+83
client/transactions.ts
···
+
import type { ClientSession, TransactionOptions } from "mongodb";
+
import { getConnection } from "./connection.ts";
+
import { ConnectionError } from "../errors.ts";
+
+
/**
+
* Transaction management module
+
*
+
* Provides session and transaction management functionality including
+
* automatic transaction handling and manual session control.
+
*/
+
+
/**
+
* Start a new client session for transactions
+
*
+
* Sessions must be ended when done using `endSession()`
+
*
+
* @returns New MongoDB ClientSession
+
* @throws {ConnectionError} If not connected
+
*
+
* @example
+
* ```ts
+
* const session = startSession();
+
* try {
+
* // use session
+
* } finally {
+
* await endSession(session);
+
* }
+
* ```
+
*/
+
export function startSession(): ClientSession {
+
const connection = getConnection();
+
if (!connection) {
+
throw new ConnectionError("MongoDB not connected. Call connect() first.");
+
}
+
return connection.client.startSession();
+
}
+
+
/**
+
* End a client session
+
*
+
* @param session - The session to end
+
*/
+
export async function endSession(session: ClientSession): Promise<void> {
+
await session.endSession();
+
}
+
+
/**
+
* Execute a function within a transaction
+
*
+
* Automatically handles session creation, transaction start/commit/abort, and cleanup.
+
* If the callback throws an error, the transaction is automatically aborted.
+
*
+
* @param callback - Async function to execute within the transaction. Receives the session as parameter.
+
* @param options - Optional transaction options (read/write concern, etc.)
+
* @returns The result from the callback function
+
*
+
* @example
+
* ```ts
+
* const result = await withTransaction(async (session) => {
+
* await UserModel.insertOne({ name: "Alice" }, { session });
+
* await OrderModel.insertOne({ userId: "123", total: 100 }, { session });
+
* return { success: true };
+
* });
+
* ```
+
*/
+
export async function withTransaction<T>(
+
callback: (session: ClientSession) => Promise<T>,
+
options?: TransactionOptions
+
): Promise<T> {
+
const session = startSession();
+
+
try {
+
let result: T;
+
+
await session.withTransaction(async () => {
+
result = await callback(session);
+
}, options);
+
+
return result!;
+
} finally {
+
await endSession(session);
+
}
+
}
+3 -3
mod.ts
···
-
export { type InferModel, type Input } from "./schema.ts";
+
export type { Schema, Infer, Input } from "./types.ts";
export {
connect,
disconnect,
···
withTransaction,
type ConnectOptions,
type HealthCheckResult
-
} from "./client.ts";
-
export { Model } from "./model.ts";
+
} from "./client/index.ts";
+
export { Model } from "./model/index.ts";
export {
NozzleError,
ValidationError,
-350
model.ts
···
-
import type { z } from "@zod/zod";
-
import type {
-
Collection,
-
CreateIndexesOptions,
-
DeleteResult,
-
Document,
-
DropIndexesOptions,
-
Filter,
-
IndexDescription,
-
IndexSpecification,
-
InsertManyResult,
-
InsertOneResult,
-
InsertOneOptions,
-
FindOptions,
-
UpdateOptions,
-
ReplaceOptions,
-
DeleteOptions,
-
CountDocumentsOptions,
-
AggregateOptions,
-
ListIndexesOptions,
-
OptionalUnlessRequiredId,
-
UpdateResult,
-
WithId,
-
BulkWriteOptions,
-
} from "mongodb";
-
import { ObjectId } from "mongodb";
-
import { getDb } from "./client.ts";
-
import { ValidationError, AsyncValidationError } from "./errors.ts";
-
-
// Type alias for cleaner code - Zod schema
-
type Schema = z.ZodObject;
-
type Infer<T extends Schema> = z.infer<T> & Document;
-
type Input<T extends Schema> = z.input<T>;
-
-
// Helper function to validate data using Zod
-
function parse<T extends Schema>(schema: T, data: Input<T>): Infer<T> {
-
const result = schema.safeParse(data);
-
-
// Check for async validation
-
if (result instanceof Promise) {
-
throw new AsyncValidationError();
-
}
-
-
if (!result.success) {
-
throw new ValidationError(result.error.issues, "insert");
-
}
-
return result.data as Infer<T>;
-
}
-
-
// Helper function to validate partial update data using Zod's partial()
-
function parsePartial<T extends Schema>(
-
schema: T,
-
data: Partial<z.infer<T>>,
-
): Partial<z.infer<T>> {
-
const result = schema.partial().safeParse(data);
-
-
// Check for async validation
-
if (result instanceof Promise) {
-
throw new AsyncValidationError();
-
}
-
-
if (!result.success) {
-
throw new ValidationError(result.error.issues, "update");
-
}
-
return result.data as Partial<z.infer<T>>;
-
}
-
-
// Helper function to validate replace data using Zod
-
function parseReplace<T extends Schema>(schema: T, data: Input<T>): Infer<T> {
-
const result = schema.safeParse(data);
-
-
// Check for async validation
-
if (result instanceof Promise) {
-
throw new AsyncValidationError();
-
}
-
-
if (!result.success) {
-
throw new ValidationError(result.error.issues, "replace");
-
}
-
return result.data as Infer<T>;
-
}
-
-
export class Model<T extends Schema> {
-
private collection: Collection<Infer<T>>;
-
private schema: T;
-
-
constructor(collectionName: string, schema: T) {
-
this.collection = getDb().collection<Infer<T>>(collectionName);
-
this.schema = schema;
-
}
-
-
async insertOne(
-
data: Input<T>,
-
options?: InsertOneOptions
-
): Promise<InsertOneResult<Infer<T>>> {
-
const validatedData = parse(this.schema, data);
-
return await this.collection.insertOne(
-
validatedData as OptionalUnlessRequiredId<Infer<T>>,
-
options
-
);
-
}
-
-
async insertMany(
-
data: Input<T>[],
-
options?: BulkWriteOptions
-
): Promise<InsertManyResult<Infer<T>>> {
-
const validatedData = data.map((item) => parse(this.schema, item));
-
return await this.collection.insertMany(
-
validatedData as OptionalUnlessRequiredId<Infer<T>>[],
-
options
-
);
-
}
-
-
async find(
-
query: Filter<Infer<T>>,
-
options?: FindOptions
-
): Promise<(WithId<Infer<T>>)[]> {
-
return await this.collection.find(query, options).toArray();
-
}
-
-
async findOne(
-
query: Filter<Infer<T>>,
-
options?: FindOptions
-
): Promise<WithId<Infer<T>> | null> {
-
return await this.collection.findOne(query, options);
-
}
-
-
async findById(
-
id: string | ObjectId,
-
options?: FindOptions
-
): Promise<WithId<Infer<T>> | null> {
-
const objectId = typeof id === "string" ? new ObjectId(id) : id;
-
return await this.findOne({ _id: objectId } as Filter<Infer<T>>, options);
-
}
-
-
async update(
-
query: Filter<Infer<T>>,
-
data: Partial<z.infer<T>>,
-
options?: UpdateOptions
-
): Promise<UpdateResult<Infer<T>>> {
-
const validatedData = parsePartial(this.schema, data);
-
return await this.collection.updateMany(
-
query,
-
{ $set: validatedData as Partial<Infer<T>> },
-
options
-
);
-
}
-
-
async updateOne(
-
query: Filter<Infer<T>>,
-
data: Partial<z.infer<T>>,
-
options?: UpdateOptions
-
): Promise<UpdateResult<Infer<T>>> {
-
const validatedData = parsePartial(this.schema, data);
-
return await this.collection.updateOne(
-
query,
-
{ $set: validatedData as Partial<Infer<T>> },
-
options
-
);
-
}
-
-
async replaceOne(
-
query: Filter<Infer<T>>,
-
data: Input<T>,
-
options?: ReplaceOptions
-
): Promise<UpdateResult<Infer<T>>> {
-
const validatedData = parseReplace(this.schema, data);
-
// Remove _id from validatedData for replaceOne (it will use the query's _id)
-
const { _id, ...withoutId } = validatedData as Infer<T> & { _id?: unknown };
-
return await this.collection.replaceOne(
-
query,
-
withoutId as Infer<T>,
-
options
-
);
-
}
-
-
async delete(
-
query: Filter<Infer<T>>,
-
options?: DeleteOptions
-
): Promise<DeleteResult> {
-
return await this.collection.deleteMany(query, options);
-
}
-
-
async deleteOne(
-
query: Filter<Infer<T>>,
-
options?: DeleteOptions
-
): Promise<DeleteResult> {
-
return await this.collection.deleteOne(query, options);
-
}
-
-
async count(
-
query: Filter<Infer<T>>,
-
options?: CountDocumentsOptions
-
): Promise<number> {
-
return await this.collection.countDocuments(query, options);
-
}
-
-
async aggregate(
-
pipeline: Document[],
-
options?: AggregateOptions
-
): Promise<Document[]> {
-
return await this.collection.aggregate(pipeline, options).toArray();
-
}
-
-
// Pagination support for find
-
async findPaginated(
-
query: Filter<Infer<T>>,
-
options: { skip?: number; limit?: number; sort?: Document } = {},
-
): Promise<(WithId<Infer<T>>)[]> {
-
return await this.collection
-
.find(query)
-
.skip(options.skip ?? 0)
-
.limit(options.limit ?? 10)
-
.sort(options.sort ?? {})
-
.toArray();
-
}
-
-
// Index Management Methods
-
-
/**
-
* Create a single index on the collection
-
* @param keys - Index specification (e.g., { email: 1 } or { name: "text" })
-
* @param options - Index creation options (unique, sparse, expireAfterSeconds, etc.)
-
* @returns The name of the created index
-
*/
-
async createIndex(
-
keys: IndexSpecification,
-
options?: CreateIndexesOptions,
-
): Promise<string> {
-
return await this.collection.createIndex(keys, options);
-
}
-
-
/**
-
* Create multiple indexes on the collection
-
* @param indexes - Array of index descriptions
-
* @param options - Index creation options
-
* @returns Array of index names created
-
*/
-
async createIndexes(
-
indexes: IndexDescription[],
-
options?: CreateIndexesOptions,
-
): Promise<string[]> {
-
return await this.collection.createIndexes(indexes, options);
-
}
-
-
/**
-
* Drop a single index from the collection
-
* @param index - Index name or specification
-
* @param options - Drop index options
-
*/
-
async dropIndex(
-
index: string | IndexSpecification,
-
options?: DropIndexesOptions,
-
): Promise<void> {
-
// MongoDB driver accepts string or IndexSpecification
-
await this.collection.dropIndex(index as string, options);
-
}
-
-
/**
-
* Drop all indexes from the collection (except _id index)
-
* @param options - Drop index options
-
*/
-
async dropIndexes(options?: DropIndexesOptions): Promise<void> {
-
await this.collection.dropIndexes(options);
-
}
-
-
/**
-
* List all indexes on the collection
-
* @param options - List indexes options
-
* @returns Array of index information
-
*/
-
async listIndexes(
-
options?: ListIndexesOptions,
-
): Promise<IndexDescription[]> {
-
const indexes = await this.collection.listIndexes(options).toArray();
-
return indexes as IndexDescription[];
-
}
-
-
/**
-
* Get index information by name
-
* @param indexName - Name of the index
-
* @returns Index description or null if not found
-
*/
-
async getIndex(indexName: string): Promise<IndexDescription | null> {
-
const indexes = await this.listIndexes();
-
return indexes.find((idx) => idx.name === indexName) || null;
-
}
-
-
/**
-
* Check if an index exists
-
* @param indexName - Name of the index
-
* @returns True if index exists, false otherwise
-
*/
-
async indexExists(indexName: string): Promise<boolean> {
-
const index = await this.getIndex(indexName);
-
return index !== null;
-
}
-
-
/**
-
* Synchronize indexes - create indexes if they don't exist, update if they differ
-
* This is useful for ensuring indexes match your schema definition
-
* @param indexes - Array of index descriptions to synchronize
-
* @param options - Options for index creation
-
*/
-
async syncIndexes(
-
indexes: IndexDescription[],
-
options?: CreateIndexesOptions,
-
): Promise<string[]> {
-
const existingIndexes = await this.listIndexes();
-
-
const indexesToCreate: IndexDescription[] = [];
-
-
for (const index of indexes) {
-
const indexName = index.name || this._generateIndexName(index.key);
-
const existingIndex = existingIndexes.find(
-
(idx) => idx.name === indexName,
-
);
-
-
if (!existingIndex) {
-
indexesToCreate.push(index);
-
} else if (
-
JSON.stringify(existingIndex.key) !== JSON.stringify(index.key)
-
) {
-
// Index exists but keys differ - drop and recreate
-
await this.dropIndex(indexName);
-
indexesToCreate.push(index);
-
}
-
// If index exists and matches, skip it
-
}
-
-
const created: string[] = [];
-
if (indexesToCreate.length > 0) {
-
const names = await this.createIndexes(indexesToCreate, options);
-
created.push(...names);
-
}
-
-
return created;
-
}
-
-
/**
-
* Helper method to generate index name from key specification
-
*/
-
private _generateIndexName(keys: IndexSpecification): string {
-
if (typeof keys === "string") {
-
return keys;
-
}
-
const entries = Object.entries(keys as Record<string, number | string>);
-
return entries.map(([field, direction]) => `${field}_${direction}`).join("_");
-
}
-
}
+264
model/core.ts
···
+
import type { z } from "@zod/zod";
+
import type {
+
Collection,
+
DeleteResult,
+
Document,
+
Filter,
+
InsertManyResult,
+
InsertOneResult,
+
InsertOneOptions,
+
FindOptions,
+
UpdateOptions,
+
ReplaceOptions,
+
DeleteOptions,
+
CountDocumentsOptions,
+
AggregateOptions,
+
OptionalUnlessRequiredId,
+
UpdateResult,
+
WithId,
+
BulkWriteOptions,
+
} from "mongodb";
+
import { ObjectId } from "mongodb";
+
import type { Schema, Infer, Input } from "../types.ts";
+
import { parse, parsePartial, parseReplace } from "./validation.ts";
+
+
/**
+
* Core CRUD operations for the Model class
+
*
+
* This module contains all basic create, read, update, and delete operations
+
* with automatic Zod validation and transaction support.
+
*/
+
+
/**
+
* Insert a single document into the collection
+
*
+
* @param collection - MongoDB collection
+
* @param schema - Zod schema for validation
+
* @param data - Document data to insert
+
* @param options - Insert options (including session for transactions)
+
* @returns Insert result with insertedId
+
*/
+
export async function insertOne<T extends Schema>(
+
collection: Collection<Infer<T>>,
+
schema: T,
+
data: Input<T>,
+
options?: InsertOneOptions
+
): Promise<InsertOneResult<Infer<T>>> {
+
const validatedData = parse(schema, data);
+
return await collection.insertOne(
+
validatedData as OptionalUnlessRequiredId<Infer<T>>,
+
options
+
);
+
}
+
+
/**
+
* Insert multiple documents into the collection
+
*
+
* @param collection - MongoDB collection
+
* @param schema - Zod schema for validation
+
* @param data - Array of document data to insert
+
* @param options - Insert options (including session for transactions)
+
* @returns Insert result with insertedIds
+
*/
+
export async function insertMany<T extends Schema>(
+
collection: Collection<Infer<T>>,
+
schema: T,
+
data: Input<T>[],
+
options?: BulkWriteOptions
+
): Promise<InsertManyResult<Infer<T>>> {
+
const validatedData = data.map((item) => parse(schema, item));
+
return await collection.insertMany(
+
validatedData as OptionalUnlessRequiredId<Infer<T>>[],
+
options
+
);
+
}
+
+
/**
+
* Find multiple documents matching the query
+
*
+
* @param collection - MongoDB collection
+
* @param query - MongoDB query filter
+
* @param options - Find options (including session for transactions)
+
* @returns Array of matching documents
+
*/
+
export async function find<T extends Schema>(
+
collection: Collection<Infer<T>>,
+
query: Filter<Infer<T>>,
+
options?: FindOptions
+
): Promise<(WithId<Infer<T>>)[]> {
+
return await collection.find(query, options).toArray();
+
}
+
+
/**
+
* Find a single document matching the query
+
*
+
* @param collection - MongoDB collection
+
* @param query - MongoDB query filter
+
* @param options - Find options (including session for transactions)
+
* @returns Matching document or null if not found
+
*/
+
export async function findOne<T extends Schema>(
+
collection: Collection<Infer<T>>,
+
query: Filter<Infer<T>>,
+
options?: FindOptions
+
): Promise<WithId<Infer<T>> | null> {
+
return await collection.findOne(query, options);
+
}
+
+
/**
+
* Find a document by its MongoDB ObjectId
+
*
+
* @param collection - MongoDB collection
+
* @param id - Document ID (string or ObjectId)
+
* @param options - Find options (including session for transactions)
+
* @returns Matching document or null if not found
+
*/
+
export async function findById<T extends Schema>(
+
collection: Collection<Infer<T>>,
+
id: string | ObjectId,
+
options?: FindOptions
+
): Promise<WithId<Infer<T>> | null> {
+
const objectId = typeof id === "string" ? new ObjectId(id) : id;
+
return await findOne(collection, { _id: objectId } as Filter<Infer<T>>, options);
+
}
+
+
/**
+
* Update multiple documents matching the query
+
*
+
* @param collection - MongoDB collection
+
* @param schema - Zod schema for validation
+
* @param query - MongoDB query filter
+
* @param data - Partial data to update
+
* @param options - Update options (including session for transactions)
+
* @returns Update result
+
*/
+
export async function update<T extends Schema>(
+
collection: Collection<Infer<T>>,
+
schema: T,
+
query: Filter<Infer<T>>,
+
data: Partial<z.infer<T>>,
+
options?: UpdateOptions
+
): Promise<UpdateResult<Infer<T>>> {
+
const validatedData = parsePartial(schema, data);
+
return await collection.updateMany(
+
query,
+
{ $set: validatedData as Partial<Infer<T>> },
+
options
+
);
+
}
+
+
/**
+
* Update a single document matching the query
+
*
+
* @param collection - MongoDB collection
+
* @param schema - Zod schema for validation
+
* @param query - MongoDB query filter
+
* @param data - Partial data to update
+
* @param options - Update options (including session for transactions)
+
* @returns Update result
+
*/
+
export async function updateOne<T extends Schema>(
+
collection: Collection<Infer<T>>,
+
schema: T,
+
query: Filter<Infer<T>>,
+
data: Partial<z.infer<T>>,
+
options?: UpdateOptions
+
): Promise<UpdateResult<Infer<T>>> {
+
const validatedData = parsePartial(schema, data);
+
return await collection.updateOne(
+
query,
+
{ $set: validatedData as Partial<Infer<T>> },
+
options
+
);
+
}
+
+
/**
+
* Replace a single document matching the query
+
*
+
* @param collection - MongoDB collection
+
* @param schema - Zod schema for validation
+
* @param query - MongoDB query filter
+
* @param data - Complete document data for replacement
+
* @param options - Replace options (including session for transactions)
+
* @returns Update result
+
*/
+
export async function replaceOne<T extends Schema>(
+
collection: Collection<Infer<T>>,
+
schema: T,
+
query: Filter<Infer<T>>,
+
data: Input<T>,
+
options?: ReplaceOptions
+
): Promise<UpdateResult<Infer<T>>> {
+
const validatedData = parseReplace(schema, data);
+
// Remove _id from validatedData for replaceOne (it will use the query's _id)
+
const { _id, ...withoutId } = validatedData as Infer<T> & { _id?: unknown };
+
return await collection.replaceOne(
+
query,
+
withoutId as Infer<T>,
+
options
+
);
+
}
+
+
/**
+
* Delete multiple documents matching the query
+
*
+
* @param collection - MongoDB collection
+
* @param query - MongoDB query filter
+
* @param options - Delete options (including session for transactions)
+
* @returns Delete result
+
*/
+
export async function deleteMany<T extends Schema>(
+
collection: Collection<Infer<T>>,
+
query: Filter<Infer<T>>,
+
options?: DeleteOptions
+
): Promise<DeleteResult> {
+
return await collection.deleteMany(query, options);
+
}
+
+
/**
+
* Delete a single document matching the query
+
*
+
* @param collection - MongoDB collection
+
* @param query - MongoDB query filter
+
* @param options - Delete options (including session for transactions)
+
* @returns Delete result
+
*/
+
export async function deleteOne<T extends Schema>(
+
collection: Collection<Infer<T>>,
+
query: Filter<Infer<T>>,
+
options?: DeleteOptions
+
): Promise<DeleteResult> {
+
return await collection.deleteOne(query, options);
+
}
+
+
/**
+
* Count documents matching the query
+
*
+
* @param collection - MongoDB collection
+
* @param query - MongoDB query filter
+
* @param options - Count options (including session for transactions)
+
* @returns Number of matching documents
+
*/
+
export async function count<T extends Schema>(
+
collection: Collection<Infer<T>>,
+
query: Filter<Infer<T>>,
+
options?: CountDocumentsOptions
+
): Promise<number> {
+
return await collection.countDocuments(query, options);
+
}
+
+
/**
+
* Execute an aggregation pipeline
+
*
+
* @param collection - MongoDB collection
+
* @param pipeline - MongoDB aggregation pipeline
+
* @param options - Aggregate options (including session for transactions)
+
* @returns Array of aggregation results
+
*/
+
export async function aggregate<T extends Schema>(
+
collection: Collection<Infer<T>>,
+
pipeline: Document[],
+
options?: AggregateOptions
+
): Promise<Document[]> {
+
return await collection.aggregate(pipeline, options).toArray();
+
}
+355
model/index.ts
···
+
import type { z } from "@zod/zod";
+
import type {
+
Collection,
+
CreateIndexesOptions,
+
DeleteResult,
+
Document,
+
DropIndexesOptions,
+
Filter,
+
IndexDescription,
+
IndexSpecification,
+
InsertManyResult,
+
InsertOneResult,
+
InsertOneOptions,
+
FindOptions,
+
UpdateOptions,
+
ReplaceOptions,
+
DeleteOptions,
+
CountDocumentsOptions,
+
AggregateOptions,
+
ListIndexesOptions,
+
UpdateResult,
+
WithId,
+
BulkWriteOptions,
+
} from "mongodb";
+
import type { ObjectId } from "mongodb";
+
import { getDb } from "../client/connection.ts";
+
import type { Schema, Infer, Input } from "../types.ts";
+
import * as core from "./core.ts";
+
import * as indexes from "./indexes.ts";
+
import * as pagination from "./pagination.ts";
+
+
/**
+
* Model class for type-safe MongoDB operations
+
*
+
* Provides a clean API for CRUD operations, pagination, and index management
+
* with automatic Zod validation and TypeScript type safety.
+
*
+
* @example
+
* ```ts
+
* const userSchema = z.object({
+
* name: z.string(),
+
* email: z.string().email(),
+
* });
+
*
+
* const UserModel = new Model("users", userSchema);
+
* await UserModel.insertOne({ name: "Alice", email: "alice@example.com" });
+
* ```
+
*/
+
export class Model<T extends Schema> {
+
private collection: Collection<Infer<T>>;
+
private schema: T;
+
+
constructor(collectionName: string, schema: T) {
+
this.collection = getDb().collection<Infer<T>>(collectionName);
+
this.schema = schema;
+
}
+
+
// ============================================================================
+
// CRUD Operations (delegated to core.ts)
+
// ============================================================================
+
+
/**
+
* Insert a single document into the collection
+
*
+
* @param data - Document data to insert
+
* @param options - Insert options (including session for transactions)
+
* @returns Insert result with insertedId
+
*/
+
async insertOne(
+
data: Input<T>,
+
options?: InsertOneOptions
+
): Promise<InsertOneResult<Infer<T>>> {
+
return await core.insertOne(this.collection, this.schema, data, options);
+
}
+
+
/**
+
* Insert multiple documents into the collection
+
*
+
* @param data - Array of document data to insert
+
* @param options - Insert options (including session for transactions)
+
* @returns Insert result with insertedIds
+
*/
+
async insertMany(
+
data: Input<T>[],
+
options?: BulkWriteOptions
+
): Promise<InsertManyResult<Infer<T>>> {
+
return await core.insertMany(this.collection, this.schema, data, options);
+
}
+
+
/**
+
* Find multiple documents matching the query
+
*
+
* @param query - MongoDB query filter
+
* @param options - Find options (including session for transactions)
+
* @returns Array of matching documents
+
*/
+
async find(
+
query: Filter<Infer<T>>,
+
options?: FindOptions
+
): Promise<(WithId<Infer<T>>)[]> {
+
return await core.find(this.collection, query, options);
+
}
+
+
/**
+
* Find a single document matching the query
+
*
+
* @param query - MongoDB query filter
+
* @param options - Find options (including session for transactions)
+
* @returns Matching document or null if not found
+
*/
+
async findOne(
+
query: Filter<Infer<T>>,
+
options?: FindOptions
+
): Promise<WithId<Infer<T>> | null> {
+
return await core.findOne(this.collection, query, options);
+
}
+
+
/**
+
* Find a document by its MongoDB ObjectId
+
*
+
* @param id - Document ID (string or ObjectId)
+
* @param options - Find options (including session for transactions)
+
* @returns Matching document or null if not found
+
*/
+
async findById(
+
id: string | ObjectId,
+
options?: FindOptions
+
): Promise<WithId<Infer<T>> | null> {
+
return await core.findById(this.collection, id, options);
+
}
+
+
/**
+
* Update multiple documents matching the query
+
*
+
* @param query - MongoDB query filter
+
* @param data - Partial data to update
+
* @param options - Update options (including session for transactions)
+
* @returns Update result
+
*/
+
async update(
+
query: Filter<Infer<T>>,
+
data: Partial<z.infer<T>>,
+
options?: UpdateOptions
+
): Promise<UpdateResult<Infer<T>>> {
+
return await core.update(this.collection, this.schema, query, data, options);
+
}
+
+
/**
+
* Update a single document matching the query
+
*
+
* @param query - MongoDB query filter
+
* @param data - Partial data to update
+
* @param options - Update options (including session for transactions)
+
* @returns Update result
+
*/
+
async updateOne(
+
query: Filter<Infer<T>>,
+
data: Partial<z.infer<T>>,
+
options?: UpdateOptions
+
): Promise<UpdateResult<Infer<T>>> {
+
return await core.updateOne(this.collection, this.schema, query, data, options);
+
}
+
+
/**
+
* Replace a single document matching the query
+
*
+
* @param query - MongoDB query filter
+
* @param data - Complete document data for replacement
+
* @param options - Replace options (including session for transactions)
+
* @returns Update result
+
*/
+
async replaceOne(
+
query: Filter<Infer<T>>,
+
data: Input<T>,
+
options?: ReplaceOptions
+
): Promise<UpdateResult<Infer<T>>> {
+
return await core.replaceOne(this.collection, this.schema, query, data, options);
+
}
+
+
/**
+
* Delete multiple documents matching the query
+
*
+
* @param query - MongoDB query filter
+
* @param options - Delete options (including session for transactions)
+
* @returns Delete result
+
*/
+
async delete(
+
query: Filter<Infer<T>>,
+
options?: DeleteOptions
+
): Promise<DeleteResult> {
+
return await core.deleteMany(this.collection, query, options);
+
}
+
+
/**
+
* Delete a single document matching the query
+
*
+
* @param query - MongoDB query filter
+
* @param options - Delete options (including session for transactions)
+
* @returns Delete result
+
*/
+
async deleteOne(
+
query: Filter<Infer<T>>,
+
options?: DeleteOptions
+
): Promise<DeleteResult> {
+
return await core.deleteOne(this.collection, query, options);
+
}
+
+
/**
+
* Count documents matching the query
+
*
+
* @param query - MongoDB query filter
+
* @param options - Count options (including session for transactions)
+
* @returns Number of matching documents
+
*/
+
async count(
+
query: Filter<Infer<T>>,
+
options?: CountDocumentsOptions
+
): Promise<number> {
+
return await core.count(this.collection, query, options);
+
}
+
+
/**
+
* Execute an aggregation pipeline
+
*
+
* @param pipeline - MongoDB aggregation pipeline
+
* @param options - Aggregate options (including session for transactions)
+
* @returns Array of aggregation results
+
*/
+
async aggregate(
+
pipeline: Document[],
+
options?: AggregateOptions
+
): Promise<Document[]> {
+
return await core.aggregate(this.collection, pipeline, options);
+
}
+
+
// ============================================================================
+
// Pagination (delegated to pagination.ts)
+
// ============================================================================
+
+
/**
+
* Find documents with pagination support
+
*
+
* @param query - MongoDB query filter
+
* @param options - Pagination options (skip, limit, sort)
+
* @returns Array of matching documents
+
*/
+
async findPaginated(
+
query: Filter<Infer<T>>,
+
options: { skip?: number; limit?: number; sort?: Document } = {},
+
): Promise<(WithId<Infer<T>>)[]> {
+
return await pagination.findPaginated(this.collection, query, options);
+
}
+
+
// ============================================================================
+
// Index Management (delegated to indexes.ts)
+
// ============================================================================
+
+
/**
+
* Create a single index on the collection
+
*
+
* @param keys - Index specification (e.g., { email: 1 } or { name: "text" })
+
* @param options - Index creation options (unique, sparse, expireAfterSeconds, etc.)
+
* @returns The name of the created index
+
*/
+
async createIndex(
+
keys: IndexSpecification,
+
options?: CreateIndexesOptions,
+
): Promise<string> {
+
return await indexes.createIndex(this.collection, keys, options);
+
}
+
+
/**
+
* Create multiple indexes on the collection
+
*
+
* @param indexes - Array of index descriptions
+
* @param options - Index creation options
+
* @returns Array of index names created
+
*/
+
async createIndexes(
+
indexList: IndexDescription[],
+
options?: CreateIndexesOptions,
+
): Promise<string[]> {
+
return await indexes.createIndexes(this.collection, indexList, options);
+
}
+
+
/**
+
* Drop a single index from the collection
+
*
+
* @param index - Index name or specification
+
* @param options - Drop index options
+
*/
+
async dropIndex(
+
index: string | IndexSpecification,
+
options?: DropIndexesOptions,
+
): Promise<void> {
+
return await indexes.dropIndex(this.collection, index, options);
+
}
+
+
/**
+
* Drop all indexes from the collection (except _id index)
+
*
+
* @param options - Drop index options
+
*/
+
async dropIndexes(options?: DropIndexesOptions): Promise<void> {
+
return await indexes.dropIndexes(this.collection, options);
+
}
+
+
/**
+
* List all indexes on the collection
+
*
+
* @param options - List indexes options
+
* @returns Array of index information
+
*/
+
async listIndexes(
+
options?: ListIndexesOptions,
+
): Promise<IndexDescription[]> {
+
return await indexes.listIndexes(this.collection, options);
+
}
+
+
/**
+
* Get index information by name
+
*
+
* @param indexName - Name of the index
+
* @returns Index description or null if not found
+
*/
+
async getIndex(indexName: string): Promise<IndexDescription | null> {
+
return await indexes.getIndex(this.collection, indexName);
+
}
+
+
/**
+
* Check if an index exists
+
*
+
* @param indexName - Name of the index
+
* @returns True if index exists, false otherwise
+
*/
+
async indexExists(indexName: string): Promise<boolean> {
+
return await indexes.indexExists(this.collection, indexName);
+
}
+
+
/**
+
* Synchronize indexes - create indexes if they don't exist, update if they differ
+
*
+
* This is useful for ensuring indexes match your schema definition
+
*
+
* @param indexes - Array of index descriptions to synchronize
+
* @param options - Options for index creation
+
* @returns Array of index names that were created
+
*/
+
async syncIndexes(
+
indexList: IndexDescription[],
+
options?: CreateIndexesOptions,
+
): Promise<string[]> {
+
return await indexes.syncIndexes(this.collection, indexList, options);
+
}
+
}
+180
model/indexes.ts
···
+
import type {
+
Collection,
+
CreateIndexesOptions,
+
DropIndexesOptions,
+
IndexDescription,
+
IndexSpecification,
+
ListIndexesOptions,
+
} from "mongodb";
+
import type { Schema, Infer } from "../types.ts";
+
+
/**
+
* Index management operations for the Model class
+
*
+
* This module contains all index-related operations including creation,
+
* deletion, listing, and synchronization of indexes.
+
*/
+
+
/**
+
* Create a single index on the collection
+
*
+
* @param collection - MongoDB collection
+
* @param keys - Index specification (e.g., { email: 1 } or { name: "text" })
+
* @param options - Index creation options (unique, sparse, expireAfterSeconds, etc.)
+
* @returns The name of the created index
+
*/
+
export async function createIndex<T extends Schema>(
+
collection: Collection<Infer<T>>,
+
keys: IndexSpecification,
+
options?: CreateIndexesOptions,
+
): Promise<string> {
+
return await collection.createIndex(keys, options);
+
}
+
+
/**
+
* Create multiple indexes on the collection
+
*
+
* @param collection - MongoDB collection
+
* @param indexes - Array of index descriptions
+
* @param options - Index creation options
+
* @returns Array of index names created
+
*/
+
export async function createIndexes<T extends Schema>(
+
collection: Collection<Infer<T>>,
+
indexes: IndexDescription[],
+
options?: CreateIndexesOptions,
+
): Promise<string[]> {
+
return await collection.createIndexes(indexes, options);
+
}
+
+
/**
+
* Drop a single index from the collection
+
*
+
* @param collection - MongoDB collection
+
* @param index - Index name or specification
+
* @param options - Drop index options
+
*/
+
export async function dropIndex<T extends Schema>(
+
collection: Collection<Infer<T>>,
+
index: string | IndexSpecification,
+
options?: DropIndexesOptions,
+
): Promise<void> {
+
await collection.dropIndex(index as string, options);
+
}
+
+
/**
+
* Drop all indexes from the collection (except _id index)
+
*
+
* @param collection - MongoDB collection
+
* @param options - Drop index options
+
*/
+
export async function dropIndexes<T extends Schema>(
+
collection: Collection<Infer<T>>,
+
options?: DropIndexesOptions
+
): Promise<void> {
+
await collection.dropIndexes(options);
+
}
+
+
/**
+
* List all indexes on the collection
+
*
+
* @param collection - MongoDB collection
+
* @param options - List indexes options
+
* @returns Array of index information
+
*/
+
export async function listIndexes<T extends Schema>(
+
collection: Collection<Infer<T>>,
+
options?: ListIndexesOptions,
+
): Promise<IndexDescription[]> {
+
const indexes = await collection.listIndexes(options).toArray();
+
return indexes as IndexDescription[];
+
}
+
+
/**
+
* Get index information by name
+
*
+
* @param collection - MongoDB collection
+
* @param indexName - Name of the index
+
* @returns Index description or null if not found
+
*/
+
export async function getIndex<T extends Schema>(
+
collection: Collection<Infer<T>>,
+
indexName: string
+
): Promise<IndexDescription | null> {
+
const indexes = await listIndexes(collection);
+
return indexes.find((idx) => idx.name === indexName) || null;
+
}
+
+
/**
+
* Check if an index exists
+
*
+
* @param collection - MongoDB collection
+
* @param indexName - Name of the index
+
* @returns True if index exists, false otherwise
+
*/
+
export async function indexExists<T extends Schema>(
+
collection: Collection<Infer<T>>,
+
indexName: string
+
): Promise<boolean> {
+
const index = await getIndex(collection, indexName);
+
return index !== null;
+
}
+
+
/**
+
* Synchronize indexes - create indexes if they don't exist, update if they differ
+
*
+
* This is useful for ensuring indexes match your schema definition
+
*
+
* @param collection - MongoDB collection
+
* @param indexes - Array of index descriptions to synchronize
+
* @param options - Options for index creation
+
* @returns Array of index names that were created
+
*/
+
export async function syncIndexes<T extends Schema>(
+
collection: Collection<Infer<T>>,
+
indexes: IndexDescription[],
+
options?: CreateIndexesOptions,
+
): Promise<string[]> {
+
const existingIndexes = await listIndexes(collection);
+
const indexesToCreate: IndexDescription[] = [];
+
+
for (const index of indexes) {
+
const indexName = index.name || generateIndexName(index.key);
+
const existingIndex = existingIndexes.find(
+
(idx) => idx.name === indexName,
+
);
+
+
if (!existingIndex) {
+
indexesToCreate.push(index);
+
} else if (
+
JSON.stringify(existingIndex.key) !== JSON.stringify(index.key)
+
) {
+
// Index exists but keys differ - drop and recreate
+
await dropIndex(collection, indexName);
+
indexesToCreate.push(index);
+
}
+
// If index exists and matches, skip it
+
}
+
+
const created: string[] = [];
+
if (indexesToCreate.length > 0) {
+
const names = await createIndexes(collection, indexesToCreate, options);
+
created.push(...names);
+
}
+
+
return created;
+
}
+
+
/**
+
* Generate index name from key specification
+
*
+
* @param keys - Index specification
+
* @returns Generated index name
+
*/
+
export function generateIndexName(keys: IndexSpecification): string {
+
if (typeof keys === "string") {
+
return keys;
+
}
+
const entries = Object.entries(keys as Record<string, number | string>);
+
return entries.map(([field, direction]) => `${field}_${direction}`).join("_");
+
}
+43
model/pagination.ts
···
+
import type {
+
Collection,
+
Document,
+
Filter,
+
WithId,
+
} from "mongodb";
+
import type { Schema, Infer } from "../types.ts";
+
+
/**
+
* Pagination operations for the Model class
+
*
+
* This module contains pagination-related functionality for finding documents
+
* with skip, limit, and sort options.
+
*/
+
+
/**
+
* Find documents with pagination support
+
*
+
* @param collection - MongoDB collection
+
* @param query - MongoDB query filter
+
* @param options - Pagination options (skip, limit, sort)
+
* @returns Array of matching documents
+
*
+
* @example
+
* ```ts
+
* const users = await findPaginated(collection,
+
* { age: { $gte: 18 } },
+
* { skip: 0, limit: 10, sort: { createdAt: -1 } }
+
* );
+
* ```
+
*/
+
export async function findPaginated<T extends Schema>(
+
collection: Collection<Infer<T>>,
+
query: Filter<Infer<T>>,
+
options: { skip?: number; limit?: number; sort?: Document } = {},
+
): Promise<(WithId<Infer<T>>)[]> {
+
return await collection
+
.find(query)
+
.skip(options.skip ?? 0)
+
.limit(options.limit ?? 10)
+
.sort(options.sort ?? {})
+
.toArray();
+
}
+75
model/validation.ts
···
+
import type { z } from "@zod/zod";
+
import type { Schema, Infer, Input } from "../types.ts";
+
import { ValidationError, AsyncValidationError } from "../errors.ts";
+
+
/**
+
* Validate data for insert operations using Zod schema
+
*
+
* @param schema - Zod schema to validate against
+
* @param data - Data to validate
+
* @returns Validated and typed data
+
* @throws {ValidationError} If validation fails
+
* @throws {AsyncValidationError} If async validation is detected
+
*/
+
export function parse<T extends Schema>(schema: T, data: Input<T>): Infer<T> {
+
const result = schema.safeParse(data);
+
+
// Check for async validation
+
if (result instanceof Promise) {
+
throw new AsyncValidationError();
+
}
+
+
if (!result.success) {
+
throw new ValidationError(result.error.issues, "insert");
+
}
+
return result.data as Infer<T>;
+
}
+
+
/**
+
* Validate partial data for update operations using Zod schema
+
*
+
* @param schema - Zod schema to validate against
+
* @param data - Partial data to validate
+
* @returns Validated and typed partial data
+
* @throws {ValidationError} If validation fails
+
* @throws {AsyncValidationError} If async validation is detected
+
*/
+
export function parsePartial<T extends Schema>(
+
schema: T,
+
data: Partial<z.infer<T>>,
+
): Partial<z.infer<T>> {
+
const result = schema.partial().safeParse(data);
+
+
// Check for async validation
+
if (result instanceof Promise) {
+
throw new AsyncValidationError();
+
}
+
+
if (!result.success) {
+
throw new ValidationError(result.error.issues, "update");
+
}
+
return result.data as Partial<z.infer<T>>;
+
}
+
+
/**
+
* Validate data for replace operations using Zod schema
+
*
+
* @param schema - Zod schema to validate against
+
* @param data - Data to validate
+
* @returns Validated and typed data
+
* @throws {ValidationError} If validation fails
+
* @throws {AsyncValidationError} If async validation is detected
+
*/
+
export function parseReplace<T extends Schema>(schema: T, data: Input<T>): Infer<T> {
+
const result = schema.safeParse(data);
+
+
// Check for async validation
+
if (result instanceof Promise) {
+
throw new AsyncValidationError();
+
}
+
+
if (!result.success) {
+
throw new ValidationError(result.error.issues, "replace");
+
}
+
return result.data as Infer<T>;
+
}
-11
schema.ts
···
-
import type { z } from "@zod/zod";
-
import type { ObjectId } from "mongodb";
-
-
type Schema = z.ZodObject;
-
type Infer<T extends Schema> = z.infer<T>;
-
-
export type InferModel<T extends Schema> = Infer<T> & {
-
_id?: ObjectId;
-
};
-
-
export type Input<T extends Schema> = z.input<T>;
+6 -6
tests/errors_test.ts
···
await assertRejects(
async () => {
-
await UserModel.insertOne({ name: "", email: "invalid" } as any);
+
await UserModel.insertOne({ name: "", email: "invalid" });
},
ValidationError,
"Validation failed on insert"
···
const UserModel = new Model("users", userSchema);
try {
-
await UserModel.insertOne({ name: "", email: "invalid" } as any);
+
await UserModel.insertOne({ name: "", email: "invalid" });
throw new Error("Should have thrown ValidationError");
} catch (error) {
assert(error instanceof ValidationError);
···
await assertRejects(
async () => {
-
await UserModel.replaceOne({ name: "Test" }, { name: "", email: "invalid" } as any);
+
await UserModel.replaceOne({ name: "Test" }, { name: "", email: "invalid" });
},
ValidationError,
"Validation failed on replace"
···
// Make sure not connected
await disconnect();
-
const { getDb } = await import("../client.ts");
+
const { getDb } = await import("../client/connection.ts");
try {
getDb();
···
name: "",
email: "not-an-email",
age: -10,
-
} as any);
+
});
throw new Error("Should have thrown ValidationError");
} catch (error) {
assert(error instanceof ValidationError);
···
const UserModel = new Model("users", userSchema);
try {
-
await UserModel.insertOne({ name: "", email: "invalid" } as any);
+
await UserModel.insertOne({ name: "", email: "invalid" });
} catch (error) {
assert(error instanceof ValidationError);
assertEquals(error.name, "ValidationError");
+1 -1
tests/transactions_test.ts
···
// Clean up database
if (replSet) {
try {
-
const { getDb } = await import("../client.ts");
+
const { getDb } = await import("../client/connection.ts");
const db = getDb();
await db.dropDatabase();
} catch {
+25
types.ts
···
+
import type { z } from "@zod/zod";
+
import type { Document, ObjectId } from "mongodb";
+
+
/**
+
* Type alias for Zod schema objects
+
*/
+
export type Schema = z.ZodObject<z.ZodRawShape>;
+
+
/**
+
* Infer the TypeScript type from a Zod schema, including MongoDB Document
+
*/
+
export type Infer<T extends Schema> = z.infer<T> & Document;
+
+
+
/**
+
* Infer the model type from a Zod schema, including MongoDB Document and ObjectId
+
*/
+
export type InferModel<T extends Schema> = Infer<T> & {
+
_id?: ObjectId;
+
};
+
+
/**
+
* Infer the input type for a Zod schema (handles defaults)
+
*/
+
export type Input<T extends Schema> = z.input<T>;