import type { z } from "@zod/zod"; import type { AggregateOptions, BulkWriteOptions, Collection, CountDocumentsOptions, DeleteOptions, DeleteResult, Document, Filter, FindOneAndReplaceOptions, FindOneAndUpdateOptions, FindOptions, InsertManyResult, InsertOneOptions, InsertOneResult, ModifyResult, OptionalUnlessRequiredId, ReplaceOptions, UpdateFilter, UpdateOptions, UpdateResult, WithId, } from "mongodb"; import { ObjectId } from "mongodb"; import type { Infer, Input, Schema } from "../types.ts"; import { applyDefaultsForUpsert, 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( collection: Collection>, schema: T, data: Input, options?: InsertOneOptions, ): Promise>> { const validatedData = parse(schema, data); return await collection.insertOne( validatedData as OptionalUnlessRequiredId>, 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( collection: Collection>, schema: T, data: Input[], options?: BulkWriteOptions, ): Promise>> { const validatedData = data.map((item) => parse(schema, item)); return await collection.insertMany( validatedData as OptionalUnlessRequiredId>[], 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( collection: Collection>, query: Filter>, options?: FindOptions, ): Promise<(WithId>)[]> { 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( collection: Collection>, query: Filter>, options?: FindOptions, ): Promise> | 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( collection: Collection>, id: string | ObjectId, options?: FindOptions, ): Promise> | null> { const objectId = typeof id === "string" ? new ObjectId(id) : id; return await findOne( collection, { _id: objectId } as Filter>, options, ); } /** * Update multiple documents matching the query * * Case handling: * - If upsert: false (or undefined) → Normal update, no defaults applied * - If upsert: true → Defaults added to $setOnInsert for new document creation * * @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 and upsert flag) * @returns Update result */ export async function update( collection: Collection>, schema: T, query: Filter>, data: Partial>, options?: UpdateOptions, ): Promise>> { const validatedData = parsePartial(schema, data); let updateDoc: UpdateFilter> = { $set: validatedData as Partial>, }; // If this is an upsert, apply defaults using $setOnInsert if (options?.upsert) { updateDoc = applyDefaultsForUpsert(schema, query, updateDoc); } return await collection.updateMany(query, updateDoc, options); } /** * Update a single document matching the query * * Case handling: * - If upsert: false (or undefined) → Normal update, no defaults applied * - If upsert: true → Defaults added to $setOnInsert for new document creation * * @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 and upsert flag) * @returns Update result */ export async function updateOne( collection: Collection>, schema: T, query: Filter>, data: Partial>, options?: UpdateOptions, ): Promise>> { const validatedData = parsePartial(schema, data); let updateDoc: UpdateFilter> = { $set: validatedData as Partial>, }; // If this is an upsert, apply defaults using $setOnInsert if (options?.upsert) { updateDoc = applyDefaultsForUpsert(schema, query, updateDoc); } return await collection.updateOne(query, updateDoc, options); } /** * Replace a single document matching the query * * Case handling: * - If upsert: false (or undefined) → Normal replace on existing doc, no additional defaults * - If upsert: true → Defaults applied via parse() since we're passing a full document * * Note: For replace operations, defaults are automatically applied by the schema's * parse() function which treats missing fields as candidates for defaults. This works * for both regular replaces and upsert-creates since we're providing a full document. * * @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 and upsert flag) * @returns Update result */ export async function replaceOne( collection: Collection>, schema: T, query: Filter>, data: Input, options?: ReplaceOptions, ): Promise>> { // parseReplace will apply all schema defaults to missing fields // This works correctly for both regular replaces and upsert-created documents const validatedData = parseReplace(schema, data); // Remove _id from validatedData for replaceOne (it will use the query's _id) const { _id, ...withoutId } = validatedData as Infer & { _id?: unknown }; return await collection.replaceOne( query, withoutId as Infer, options, ); } /** * Find a single document and update it * * Case handling: * - If upsert: false (or undefined) → Normal update * - If upsert: true → Defaults added to $setOnInsert for new document creation */ export async function findOneAndUpdate( collection: Collection>, schema: T, query: Filter>, data: Partial>, options?: FindOneAndUpdateOptions, ): Promise>> { const validatedData = parsePartial(schema, data); let updateDoc: UpdateFilter> = { $set: validatedData as Partial>, }; if (options?.upsert) { updateDoc = applyDefaultsForUpsert(schema, query, updateDoc); } const resolvedOptions: FindOneAndUpdateOptions & { includeResultMetadata: true; } = { ...(options ?? {}), includeResultMetadata: true as const, }; return await collection.findOneAndUpdate(query, updateDoc, resolvedOptions); } /** * Find a single document and replace it * * Defaults are applied via parseReplace(), which fills in missing fields * for both normal replacements and upsert-created documents. */ export async function findOneAndReplace( collection: Collection>, schema: T, query: Filter>, data: Input, options?: FindOneAndReplaceOptions, ): Promise>> { const validatedData = parseReplace(schema, data); const { _id, ...withoutId } = validatedData as Infer & { _id?: unknown }; const resolvedOptions: FindOneAndReplaceOptions & { includeResultMetadata: true; } = { ...(options ?? {}), includeResultMetadata: true as const, }; return await collection.findOneAndReplace( query, withoutId as Infer, resolvedOptions, ); } /** * 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( collection: Collection>, query: Filter>, options?: DeleteOptions, ): Promise { 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( collection: Collection>, query: Filter>, options?: DeleteOptions, ): Promise { 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( collection: Collection>, query: Filter>, options?: CountDocumentsOptions, ): Promise { 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( collection: Collection>, pipeline: Document[], options?: AggregateOptions, ): Promise { return await collection.aggregate(pipeline, options).toArray(); }