···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";
2export {
3 connect,
4 disconnect,
···8 withTransaction,
9 type ConnectOptions,
10 type HealthCheckResult
11-} from "./client.ts";
12-export { Model } from "./model.ts";
13export {
14 NozzleError,
15 ValidationError,
···1+export type { Schema, Infer, Input } from "./types.ts";
2export {
3 connect,
4 disconnect,
···8 withTransaction,
9 type ConnectOptions,
10 type HealthCheckResult
11+} from "./client/index.ts";
12+export { Model } from "./model/index.ts";
13export {
14 NozzleError,
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-}
···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>;