···1+import type { z } from "@zod/zod";
2+3+// Type for Zod validation issues
4+type ValidationIssue = z.ZodIssue;
5+6+/**
7+ * Base error class for all Nozzle errors
8+ */
9+export class NozzleError extends Error {
10+ constructor(message: string) {
11+ super(message);
12+ this.name = this.constructor.name;
13+ // Maintains proper stack trace for where error was thrown (only available on V8)
14+ if (Error.captureStackTrace) {
15+ Error.captureStackTrace(this, this.constructor);
16+ }
17+ }
18+}
19+20+/**
21+ * Validation error with structured issue details
22+ * Thrown when data fails schema validation
23+ */
24+export class ValidationError extends NozzleError {
25+ public readonly issues: ValidationIssue[];
26+ public readonly operation: "insert" | "update" | "replace";
27+28+ constructor(issues: ValidationIssue[], operation: "insert" | "update" | "replace") {
29+ const message = ValidationError.formatIssues(issues);
30+ super(`Validation failed on ${operation}: ${message}`);
31+ this.issues = issues;
32+ this.operation = operation;
33+ }
34+35+ private static formatIssues(issues: ValidationIssue[]): string {
36+ return issues.map(issue => {
37+ const path = issue.path.join('.');
38+ return `${path || 'root'}: ${issue.message}`;
39+ }).join('; ');
40+ }
41+42+ /**
43+ * Get validation errors grouped by field
44+ */
45+ public getFieldErrors(): Record<string, string[]> {
46+ const fieldErrors: Record<string, string[]> = {};
47+ for (const issue of this.issues) {
48+ const field = issue.path.join('.') || 'root';
49+ if (!fieldErrors[field]) {
50+ fieldErrors[field] = [];
51+ }
52+ fieldErrors[field].push(issue.message);
53+ }
54+ return fieldErrors;
55+ }
56+}
57+58+/**
59+ * Connection error
60+ * Thrown when database connection fails or is not established
61+ */
62+export class ConnectionError extends NozzleError {
63+ public readonly uri?: string;
64+65+ constructor(message: string, uri?: string) {
66+ super(message);
67+ this.uri = uri;
68+ }
69+}
70+71+/**
72+ * Configuration error
73+ * Thrown when invalid configuration options are provided
74+ */
75+export class ConfigurationError extends NozzleError {
76+ public readonly option?: string;
77+78+ constructor(message: string, option?: string) {
79+ super(message);
80+ this.option = option;
81+ }
82+}
83+84+/**
85+ * Document not found error
86+ * Thrown when a required document is not found
87+ */
88+export class DocumentNotFoundError extends NozzleError {
89+ public readonly query: unknown;
90+ public readonly collection: string;
91+92+ constructor(collection: string, query: unknown) {
93+ super(`Document not found in collection '${collection}'`);
94+ this.collection = collection;
95+ this.query = query;
96+ }
97+}
98+99+/**
100+ * Operation error
101+ * Thrown when a database operation fails
102+ */
103+export class OperationError extends NozzleError {
104+ public readonly operation: string;
105+ public readonly collection?: string;
106+ public override readonly cause?: Error;
107+108+ constructor(operation: string, message: string, collection?: string, cause?: Error) {
109+ super(`${operation} operation failed: ${message}`);
110+ this.operation = operation;
111+ this.collection = collection;
112+ this.cause = cause;
113+ }
114+}
115+116+/**
117+ * Async validation not supported error
118+ * Thrown when async validation is attempted
119+ */
120+export class AsyncValidationError extends NozzleError {
121+ constructor() {
122+ super(
123+ "Async validation is not currently supported. " +
124+ "Please use synchronous validation schemas."
125+ );
126+ }
127+}
+9
mod.ts
···1export { type InferModel, type Input } from "./schema.ts";
2export { connect, disconnect, healthCheck, type ConnectOptions, type HealthCheckResult } from "./client.ts";
3export { Model } from "./model.ts";
000000000
···1export { type InferModel, type Input } from "./schema.ts";
2export { connect, disconnect, healthCheck, type ConnectOptions, type HealthCheckResult } from "./client.ts";
3export { Model } from "./model.ts";
4+export {
5+ NozzleError,
6+ ValidationError,
7+ ConnectionError,
8+ ConfigurationError,
9+ DocumentNotFoundError,
10+ OperationError,
11+ AsyncValidationError,
12+} from "./errors.ts";
+31-3
model.ts
···17} from "mongodb";
18import { ObjectId } from "mongodb";
19import { getDb } from "./client.ts";
02021// Type alias for cleaner code - Zod schema
22type Schema = z.ZodObject;
···26// Helper function to validate data using Zod
27function parse<T extends Schema>(schema: T, data: Input<T>): Infer<T> {
28 const result = schema.safeParse(data);
00000029 if (!result.success) {
30- throw new Error(`Validation failed: ${JSON.stringify(result.error.issues)}`);
31 }
32 return result.data as Infer<T>;
33}
···38 data: Partial<z.infer<T>>,
39): Partial<z.infer<T>> {
40 const result = schema.partial().safeParse(data);
00000041 if (!result.success) {
42- throw new Error(`Update validation failed: ${JSON.stringify(result.error.issues)}`);
43 }
44 return result.data as Partial<z.infer<T>>;
00000000000000045}
4647export class Model<T extends Schema> {
···100 query: Filter<Infer<T>>,
101 data: Input<T>,
102 ): Promise<UpdateResult<Infer<T>>> {
103- const validatedData = parse(this.schema, data);
104 // Remove _id from validatedData for replaceOne (it will use the query's _id)
105 const { _id, ...withoutId } = validatedData as Infer<T> & { _id?: unknown };
106 return await this.collection.replaceOne(
···17} from "mongodb";
18import { ObjectId } from "mongodb";
19import { getDb } from "./client.ts";
20+import { ValidationError, AsyncValidationError } from "./errors.ts";
2122// Type alias for cleaner code - Zod schema
23type Schema = z.ZodObject;
···27// Helper function to validate data using Zod
28function parse<T extends Schema>(schema: T, data: Input<T>): Infer<T> {
29 const result = schema.safeParse(data);
30+31+ // Check for async validation
32+ if (result instanceof Promise) {
33+ throw new AsyncValidationError();
34+ }
35+36 if (!result.success) {
37+ throw new ValidationError(result.error.issues, "insert");
38 }
39 return result.data as Infer<T>;
40}
···45 data: Partial<z.infer<T>>,
46): Partial<z.infer<T>> {
47 const result = schema.partial().safeParse(data);
48+49+ // Check for async validation
50+ if (result instanceof Promise) {
51+ throw new AsyncValidationError();
52+ }
53+54 if (!result.success) {
55+ throw new ValidationError(result.error.issues, "update");
56 }
57 return result.data as Partial<z.infer<T>>;
58+}
59+60+// Helper function to validate replace data using Zod
61+function parseReplace<T extends Schema>(schema: T, data: Input<T>): Infer<T> {
62+ const result = schema.safeParse(data);
63+64+ // Check for async validation
65+ if (result instanceof Promise) {
66+ throw new AsyncValidationError();
67+ }
68+69+ if (!result.success) {
70+ throw new ValidationError(result.error.issues, "replace");
71+ }
72+ return result.data as Infer<T>;
73}
7475export class Model<T extends Schema> {
···128 query: Filter<Infer<T>>,
129 data: Input<T>,
130 ): Promise<UpdateResult<Infer<T>>> {
131+ const validatedData = parseReplace(this.schema, data);
132 // Remove _id from validatedData for replaceOne (it will use the query's _id)
133 const { _id, ...withoutId } = validatedData as Infer<T> & { _id?: unknown };
134 return await this.collection.replaceOne(