From 8fa1963d8dad95e1c2b9923ab0e7c7830f843554 Mon Sep 17 00:00:00 2001 From: Prince Codes Date: Sat, 26 Apr 2025 09:52:32 +0100 Subject: [PATCH 1/3] improve error logging for complex types --- src/errors.ts | 88 ++++++++++++++++++++++++++- src/schema/schema.ts | 27 +++++++-- src/types/array.ts | 9 +-- src/types/object.ts | 7 ++- src/types/record.ts | 7 ++- src/types/tuple.ts | 9 +-- tests/types.test.ts | 139 +++++++++++++++++++++++++++++++++++++++++-- 7 files changed, 263 insertions(+), 23 deletions(-) diff --git a/src/errors.ts b/src/errors.ts index fc9f62b..d8da27e 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,3 +1,87 @@ -export class MonarchError extends Error {} +export enum ErrorCodes { + MONARCH_ERROR = "MONARCH_ERROR", + PARSE_ERROR = "PARSE_ERROR", + VALIDATION_ERROR = "VALIDATION_ERROR", +} -export class MonarchParseError extends MonarchError {} +export class MonarchError extends Error { + public code: (typeof ErrorCodes)[keyof typeof ErrorCodes]; + public originalError?: Error; + + constructor( + message: string, + code: (typeof ErrorCodes)[keyof typeof ErrorCodes] = ErrorCodes.MONARCH_ERROR, + originalError?: Error, + ) { + super(message); + this.name = this.constructor.name; + this.code = code; + this.originalError = originalError; + + if (!!originalError && originalError.stack) { + this.stack = `${this.stack}\nCaused by: ${originalError.stack}`; + } else if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor); + } + } +} + +export class MonarchParseError extends MonarchError { + public fieldPath?: (string | number)[]; + constructor( + message: string, + fieldPath?: (string | number)[], + originalError?: Error, + ) { + super(message, ErrorCodes.PARSE_ERROR, originalError); + this.fieldPath = normalizeFieldPath(fieldPath); + } +} + +export class MonarchValidationError extends MonarchError { + constructor( + message: string, + public fieldPath: string, + originalError?: Error, + ) { + super( + formatValidationMessage(fieldPath, message), + ErrorCodes.VALIDATION_ERROR, + originalError, + ); + } +} + +export function normalizeFieldPath( + fieldPath?: string | (string | number)[], +): (string | number)[] { + if (typeof fieldPath === "string") { + return fieldPath.split("."); + } + if (Array.isArray(fieldPath)) { + return fieldPath; + } + return []; +} + +export function formatErrorPath( + path: string | number | (string | number)[], +): string { + return Array.isArray(path) ? path.join(".") : String(path); +} + +export function formatValidationPath(pathSegment: { + schema: string; + field: string; + path?: (string | number)[]; +}): string { + const { schema, field, path = [] } = pathSegment; + return formatErrorPath([schema, field, ...path]); +} + +export function formatValidationMessage( + fieldPath: string, + message: string, +): string { + return `Validation error: '${fieldPath}' ${message}`; +} diff --git a/src/schema/schema.ts b/src/schema/schema.ts index 111a388..bb7344e 100644 --- a/src/schema/schema.ts +++ b/src/schema/schema.ts @@ -1,5 +1,10 @@ import type { Projection } from "../collection/types/query-options"; import { detectProjection } from "../collection/utils/projection"; +import { + MonarchParseError, + MonarchValidationError, + formatValidationPath, +} from "../errors"; import { objectId } from "../types/objectId"; import { type AnyMonarchType, MonarchType } from "../types/type"; import type { Pretty, WithOptionalId } from "../utils/type-helpers"; @@ -52,10 +57,24 @@ export class Schema< // parse fields const types = Schema.types(this); for (const [key, type] of Object.entries(types)) { - const parser = MonarchType.parser(type); - const parsed = parser(input[key as keyof InferSchemaInput]); - if (parsed === undefined) continue; - data[key as keyof typeof data] = parsed; + try { + const parser = MonarchType.parser(type); + const parsed = parser(input[key as keyof InferSchemaInput]); + if (parsed === undefined) continue; + data[key as keyof typeof data] = parsed; + } catch (error) { + if (error instanceof MonarchParseError) { + throw new MonarchValidationError( + error.message, + formatValidationPath({ + schema: this.name, + field: key, + path: error.fieldPath, + }), + ); + } + throw error; + } } return data; } diff --git a/src/types/array.ts b/src/types/array.ts index 11a3b8d..aae2881 100644 --- a/src/types/array.ts +++ b/src/types/array.ts @@ -1,4 +1,4 @@ -import { MonarchParseError } from "../errors"; +import { MonarchParseError, normalizeFieldPath } from "../errors"; import { type AnyMonarchType, MonarchType } from "./type"; import type { InferTypeInput, InferTypeOutput } from "./type-helpers"; @@ -19,9 +19,10 @@ export class MonarchArray extends MonarchType< parsed[index] = parser(value); } catch (error) { if (error instanceof MonarchParseError) { - throw new MonarchParseError( - `element at index '${index}' ${error.message}`, - ); + throw new MonarchParseError(error.message, [ + index, + ...normalizeFieldPath(error.fieldPath), + ]); } throw error; } diff --git a/src/types/object.ts b/src/types/object.ts index b457b68..4b7b409 100644 --- a/src/types/object.ts +++ b/src/types/object.ts @@ -1,4 +1,4 @@ -import { MonarchParseError } from "../errors"; +import { MonarchParseError, normalizeFieldPath } from "../errors"; import { type AnyMonarchType, MonarchType } from "./type"; import type { InferTypeInput, @@ -34,7 +34,10 @@ export class MonarchObject< ); } catch (error) { if (error instanceof MonarchParseError) { - throw new MonarchParseError(`field '${key}' ${error.message}'`); + throw new MonarchParseError(error.message, [ + key, + ...normalizeFieldPath(error.fieldPath), + ]); } throw error; } diff --git a/src/types/record.ts b/src/types/record.ts index 34b03a2..31306f5 100644 --- a/src/types/record.ts +++ b/src/types/record.ts @@ -1,4 +1,4 @@ -import { MonarchParseError } from "../errors"; +import { MonarchParseError, normalizeFieldPath } from "../errors"; import { type AnyMonarchType, MonarchType } from "./type"; import type { InferTypeInput, InferTypeOutput } from "./type-helpers"; @@ -19,7 +19,10 @@ export class MonarchRecord extends MonarchType< parsed[key] = parser(value); } catch (error) { if (error instanceof MonarchParseError) { - throw new MonarchParseError(`field '${key}' ${error.message}'`); + throw new MonarchParseError(error.message, [ + key, + ...normalizeFieldPath(error.fieldPath), + ]); } throw error; } diff --git a/src/types/tuple.ts b/src/types/tuple.ts index eca310f..b2a8255 100644 --- a/src/types/tuple.ts +++ b/src/types/tuple.ts @@ -1,4 +1,4 @@ -import { MonarchParseError } from "../errors"; +import { MonarchParseError, normalizeFieldPath } from "../errors"; import { type AnyMonarchType, MonarchType } from "./type"; import type { InferTypeTupleInput, InferTypeTupleOutput } from "./type-helpers"; @@ -26,9 +26,10 @@ export class MonarchTuple< parsed[index] = parser(input[index]); } catch (error) { if (error instanceof MonarchParseError) { - throw new MonarchParseError( - `element at index '${index}' ${error.message}`, - ); + throw new MonarchParseError(error.message, [ + index, + ...normalizeFieldPath(error.fieldPath), + ]); } throw error; } diff --git a/tests/types.test.ts b/tests/types.test.ts index 7e56b91..733db6e 100644 --- a/tests/types.test.ts +++ b/tests/types.test.ts @@ -202,12 +202,16 @@ describe("Types", () => { expect(() => // @ts-expect-error Schema.toData(schema, { permissions: { canUpdate: "yes" } }), - ).toThrowError("field 'canUpdate' expected 'boolean' received 'string'"); + ).toThrowError( + "'test.permissions.canUpdate' expected 'boolean' received 'string'", + ); // fields are validates in the order they are registered in type expect(() => // @ts-expect-error Schema.toData(schema, { permissions: { role: false } }), - ).toThrowError("field 'canUpdate' expected 'boolean' received 'undefined'"); + ).toThrowError( + "'test.permissions.canUpdate' expected 'boolean' received 'undefined'", + ); // unknwon fields are rejected expect(() => Schema.toData(schema, { @@ -239,7 +243,7 @@ describe("Types", () => { expect(() => // @ts-expect-error Schema.toData(schema, { grades: { math: "50" } }), - ).toThrowError("field 'math' expected 'number' received 'string'"); + ).toThrowError("'test.grades.math' expected 'number' received 'string'"); const data = Schema.toData(schema, { grades: { math: 50 } }); expect(data).toStrictEqual({ grades: { math: 50 } }); }); @@ -255,7 +259,7 @@ describe("Types", () => { ); // @ts-expect-error expect(() => Schema.toData(schema, { items: [] })).toThrowError( - "element at index '0' expected 'number' received 'undefined'", + "'test.items.0' expected 'number' received 'undefined'", ); const data = Schema.toData(schema, { items: [0, "1"] }); expect(data).toStrictEqual({ items: [0, "1"] }); @@ -278,7 +282,7 @@ describe("Types", () => { expect(() => Schema.toData(schema, { items: [] })).not.toThrowError(); // @ts-expect-error expect(() => Schema.toData(schema, { items: [0, "1"] })).toThrowError( - "element at index '1' expected 'number' received 'string'", + "'test.items.1' expected 'number' received 'string'", ); const data = Schema.toData(schema, { items: [0, 1] }); expect(data).toStrictEqual({ items: [0, 1] }); @@ -472,6 +476,131 @@ describe("Types", () => { }); }); + describe("error handling for nested structures", () => { + test("deeply nested objects", () => { + const schema = createSchema("test", { + user: object({ + profile: object({ + contact: object({ + email: string().pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/), + phone: object({ + country: string().length(2), + number: string().pattern(/^\d{10}$/), + }), + }), + }), + }), + }); + + expect(() => + Schema.toData(schema, { + user: { + profile: { + contact: { + email: "invalid", + phone: { country: "USA", number: "123" }, + }, + }, + }, + }), + ).toThrowError( + "'test.user.profile.contact.email' string must match pattern /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/", + ); + + expect(() => + Schema.toData(schema, { + user: { + profile: { + contact: { + email: "test@example.com", + phone: { country: "USA", number: "123" }, + }, + }, + }, + }), + ).toThrowError( + "'test.user.profile.contact.phone.country' string must be exactly 2 characters long", + ); + }); + + test("array of objects", () => { + const schema = createSchema("test", { + users: array( + object({ + name: string().nonEmpty(), + age: number().min(0), + contacts: array( + taggedUnion({ + email: string().pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/), + phone: string().pattern(/^\d{10}$/), + }), + ), + }), + ), + }); + + expect(() => + Schema.toData(schema, { + users: [ + { + name: "John", + age: -1, + contacts: [{ tag: "email", value: "john@example.com" }], + }, + ], + }), + ).toThrowError( + "'test.users.0.age' number must be greater than or equal to 0", + ); + + expect(() => + Schema.toData(schema, { + users: [ + { + name: "John", + age: 30, + contacts: [ + { tag: "phone", value: "1234567890" }, + // @ts-expect-error + { tag: "invalid", value: "john@example.com" }, + ], + }, + ], + }), + ).toThrowError("'test.users.0.contacts.1' unknown tag 'invalid'"); + }); + + test("nested tuples", () => { + const schema = createSchema("test", { + coordinates: array( + tuple([ + number() + .min(-90) + .max(90), // latitude + number() + .min(-180) + .max(180), // longitude + array(string().nonEmpty()), // tags + ]), + ), + }); + + expect(() => + Schema.toData(schema, { + coordinates: [[91, 0, ["north"]]], + }), + ).toThrowError( + "'test.coordinates.0.0' number must be less than or equal to 90", + ); + + expect(() => + Schema.toData(schema, { + coordinates: [[0, 0, [""]]], + }), + ).toThrowError("'test.coordinates.0.2.0' string must not be empty"); + }); + }); + describe("date", () => { const now = new Date(); const past = new Date(now.getTime() - 1000 * 60 * 60 * 24 * 2); // 2 days ago From 7d6d00f98503a897624ad024fa87364c5435d439 Mon Sep 17 00:00:00 2001 From: Prince Codes Date: Sat, 26 Apr 2025 11:32:18 +0100 Subject: [PATCH 2/3] streamline errors --- src/errors.ts | 57 +++++++++------------------------------ src/schema/schema.ts | 4 +-- src/types/array.ts | 12 ++++----- src/types/boolean.ts | 6 ++--- src/types/date.ts | 14 +++++----- src/types/literal.ts | 4 +-- src/types/number.ts | 14 ++++------ src/types/object.ts | 14 +++++----- src/types/objectId.ts | 4 +-- src/types/record.ts | 12 ++++----- src/types/string.ts | 18 ++++++------- src/types/tagged-union.ts | 18 ++++++------- src/types/tuple.ts | 14 +++++----- src/types/type.ts | 4 +-- src/types/union.ts | 8 +++--- 15 files changed, 77 insertions(+), 126 deletions(-) diff --git a/src/errors.ts b/src/errors.ts index d8da27e..ce5874e 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,40 +1,34 @@ export enum ErrorCodes { MONARCH_ERROR = "MONARCH_ERROR", - PARSE_ERROR = "PARSE_ERROR", VALIDATION_ERROR = "VALIDATION_ERROR", } export class MonarchError extends Error { - public code: (typeof ErrorCodes)[keyof typeof ErrorCodes]; - public originalError?: Error; - constructor( message: string, - code: (typeof ErrorCodes)[keyof typeof ErrorCodes] = ErrorCodes.MONARCH_ERROR, - originalError?: Error, + public code: ErrorCodes = ErrorCodes.MONARCH_ERROR, + public cause?: Error, ) { super(message); this.name = this.constructor.name; this.code = code; - this.originalError = originalError; + this.cause = cause; - if (!!originalError && originalError.stack) { - this.stack = `${this.stack}\nCaused by: ${originalError.stack}`; + if (!!cause && cause.stack) { + this.stack = `${this.stack}\nCaused by: ${cause.stack}`; } else if (Error.captureStackTrace) { Error.captureStackTrace(this, this.constructor); } } } -export class MonarchParseError extends MonarchError { - public fieldPath?: (string | number)[]; +export class FieldError extends Error { constructor( message: string, - fieldPath?: (string | number)[], - originalError?: Error, + public fieldPath?: (string | number)[], ) { - super(message, ErrorCodes.PARSE_ERROR, originalError); - this.fieldPath = normalizeFieldPath(fieldPath); + super(message); + this.fieldPath = fieldPath ?? []; } } @@ -42,46 +36,21 @@ export class MonarchValidationError extends MonarchError { constructor( message: string, public fieldPath: string, - originalError?: Error, + cause?: Error, ) { super( - formatValidationMessage(fieldPath, message), + `Validation error: '${fieldPath}' ${message}`, ErrorCodes.VALIDATION_ERROR, - originalError, + cause, ); } } -export function normalizeFieldPath( - fieldPath?: string | (string | number)[], -): (string | number)[] { - if (typeof fieldPath === "string") { - return fieldPath.split("."); - } - if (Array.isArray(fieldPath)) { - return fieldPath; - } - return []; -} - -export function formatErrorPath( - path: string | number | (string | number)[], -): string { - return Array.isArray(path) ? path.join(".") : String(path); -} - export function formatValidationPath(pathSegment: { schema: string; field: string; path?: (string | number)[]; }): string { const { schema, field, path = [] } = pathSegment; - return formatErrorPath([schema, field, ...path]); -} - -export function formatValidationMessage( - fieldPath: string, - message: string, -): string { - return `Validation error: '${fieldPath}' ${message}`; + return [schema, field, ...path].join("."); } diff --git a/src/schema/schema.ts b/src/schema/schema.ts index bb7344e..3de3237 100644 --- a/src/schema/schema.ts +++ b/src/schema/schema.ts @@ -1,7 +1,7 @@ import type { Projection } from "../collection/types/query-options"; import { detectProjection } from "../collection/utils/projection"; import { - MonarchParseError, + FieldError, MonarchValidationError, formatValidationPath, } from "../errors"; @@ -63,7 +63,7 @@ export class Schema< if (parsed === undefined) continue; data[key as keyof typeof data] = parsed; } catch (error) { - if (error instanceof MonarchParseError) { + if (error instanceof FieldError) { throw new MonarchValidationError( error.message, formatValidationPath({ diff --git a/src/types/array.ts b/src/types/array.ts index aae2881..153105e 100644 --- a/src/types/array.ts +++ b/src/types/array.ts @@ -1,4 +1,4 @@ -import { MonarchParseError, normalizeFieldPath } from "../errors"; +import { FieldError } from "../errors"; import { type AnyMonarchType, MonarchType } from "./type"; import type { InferTypeInput, InferTypeOutput } from "./type-helpers"; @@ -18,10 +18,10 @@ export class MonarchArray extends MonarchType< const parser = MonarchType.parser(type); parsed[index] = parser(value); } catch (error) { - if (error instanceof MonarchParseError) { - throw new MonarchParseError(error.message, [ + if (error instanceof FieldError) { + throw new FieldError(error.message, [ index, - ...normalizeFieldPath(error.fieldPath), + ...(error.fieldPath ?? []), ]); } throw error; @@ -29,9 +29,7 @@ export class MonarchArray extends MonarchType< } return parsed; } - throw new MonarchParseError( - `expected 'array' received '${typeof input}'`, - ); + throw new FieldError(`expected 'array' received '${typeof input}'`); }); } } diff --git a/src/types/boolean.ts b/src/types/boolean.ts index 958803e..e683336 100644 --- a/src/types/boolean.ts +++ b/src/types/boolean.ts @@ -1,4 +1,4 @@ -import { MonarchParseError } from "../errors"; +import { FieldError } from "../errors"; import { MonarchType } from "./type"; export const boolean = () => new MonarchBoolean(); @@ -7,9 +7,7 @@ export class MonarchBoolean extends MonarchType { constructor() { super((input) => { if (typeof input === "boolean") return input; - throw new MonarchParseError( - `expected 'boolean' received '${typeof input}'`, - ); + throw new FieldError(`expected 'boolean' received '${typeof input}'`); }); } } diff --git a/src/types/date.ts b/src/types/date.ts index d71d83a..8aa813b 100644 --- a/src/types/date.ts +++ b/src/types/date.ts @@ -1,4 +1,4 @@ -import { MonarchParseError } from "../errors"; +import { FieldError } from "../errors"; import { MonarchType } from "./type"; export const date = () => new MonarchDate(); @@ -7,7 +7,7 @@ export class MonarchDate extends MonarchType { constructor() { super((input) => { if (input instanceof Date) return input; - throw new MonarchParseError(`expected 'Date' received '${typeof input}'`); + throw new FieldError(`expected 'Date' received '${typeof input}'`); }); } @@ -15,7 +15,7 @@ export class MonarchDate extends MonarchType { return date().extend(this, { preParse: (input) => { if (input > afterDate) return input; - throw new MonarchParseError(`date must be after ${afterDate}`); + throw new FieldError(`date must be after ${afterDate}`); }, }); } @@ -24,7 +24,7 @@ export class MonarchDate extends MonarchType { return date().extend(this, { preParse: (input) => { if (input > targetDate) { - throw new MonarchParseError( + throw new FieldError( `date must be before ${targetDate.toISOString()}`, ); } @@ -51,7 +51,7 @@ export class MonarchDateString extends MonarchType { if (typeof input === "string" && !Number.isNaN(Date.parse(input))) { return new Date(input); } - throw new MonarchParseError( + throw new FieldError( `expected 'ISO Date string' received '${typeof input}'`, ); }); @@ -62,7 +62,7 @@ export class MonarchDateString extends MonarchType { preParse: (input) => { const date = new Date(input); if (date > afterDate) return input; - throw new MonarchParseError(`date must be after ${afterDate}`); + throw new FieldError(`date must be after ${afterDate}`); }, }); } @@ -72,7 +72,7 @@ export class MonarchDateString extends MonarchType { preParse: (input) => { const date = new Date(input); if (date > targetDate) { - throw new MonarchParseError( + throw new FieldError( `date must be before ${targetDate.toISOString()}`, ); } diff --git a/src/types/literal.ts b/src/types/literal.ts index 19119d1..79463ee 100644 --- a/src/types/literal.ts +++ b/src/types/literal.ts @@ -1,4 +1,4 @@ -import { MonarchParseError } from "../errors"; +import { FieldError } from "../errors"; import { MonarchType } from "./type"; export const literal = (...values: T[]) => @@ -11,7 +11,7 @@ export class MonarchLiteral< super((input) => { const _values = new Set(values); if (_values.has(input)) return input; - throw new MonarchParseError( + throw new FieldError( `unknown value '${input}', literal may only specify known values`, ); }); diff --git a/src/types/number.ts b/src/types/number.ts index 3ce8113..5f64c3e 100644 --- a/src/types/number.ts +++ b/src/types/number.ts @@ -1,4 +1,4 @@ -import { MonarchParseError } from "../errors"; +import { FieldError } from "../errors"; import { MonarchType } from "./type"; export const number = () => new MonarchNumber(); @@ -7,9 +7,7 @@ export class MonarchNumber extends MonarchType { constructor() { super((input) => { if (typeof input === "number") return input; - throw new MonarchParseError( - `expected 'number' received '${typeof input}'`, - ); + throw new FieldError(`expected 'number' received '${typeof input}'`); }); } @@ -17,7 +15,7 @@ export class MonarchNumber extends MonarchType { return number().extend(this, { preParse: (input) => { if (input < value) { - throw new MonarchParseError( + throw new FieldError( `number must be greater than or equal to ${value}`, ); } @@ -30,9 +28,7 @@ export class MonarchNumber extends MonarchType { return number().extend(this, { preParse: (input) => { if (input > value) { - throw new MonarchParseError( - `number must be less than or equal to ${value}`, - ); + throw new FieldError(`number must be less than or equal to ${value}`); } return input; }, @@ -51,7 +47,7 @@ export class MonarchNumber extends MonarchType { return number().extend(this, { postParse: (input) => { if (input % value !== 0) { - throw new MonarchParseError(`number must be a multiple of ${value}`); + throw new FieldError(`number must be a multiple of ${value}`); } return input; }, diff --git a/src/types/object.ts b/src/types/object.ts index 4b7b409..4d923b6 100644 --- a/src/types/object.ts +++ b/src/types/object.ts @@ -1,4 +1,4 @@ -import { MonarchParseError, normalizeFieldPath } from "../errors"; +import { FieldError } from "../errors"; import { type AnyMonarchType, MonarchType } from "./type"; import type { InferTypeInput, @@ -17,7 +17,7 @@ export class MonarchObject< if (typeof input === "object" && input !== null) { for (const key of Object.keys(input)) { if (!(key in types)) { - throw new MonarchParseError( + throw new FieldError( `unknown field '${key}', object may only specify known fields`, ); } @@ -33,10 +33,10 @@ export class MonarchObject< input[key as keyof typeof input] as InferTypeInput, ); } catch (error) { - if (error instanceof MonarchParseError) { - throw new MonarchParseError(error.message, [ + if (error instanceof FieldError) { + throw new FieldError(error.message, [ key, - ...normalizeFieldPath(error.fieldPath), + ...(error.fieldPath ?? []), ]); } throw error; @@ -44,9 +44,7 @@ export class MonarchObject< } return parsed; } - throw new MonarchParseError( - `expected 'object' received '${typeof input}'`, - ); + throw new FieldError(`expected 'object' received '${typeof input}'`); }); } } diff --git a/src/types/objectId.ts b/src/types/objectId.ts index e0ce08e..8679383 100644 --- a/src/types/objectId.ts +++ b/src/types/objectId.ts @@ -1,5 +1,5 @@ import { ObjectId } from "mongodb"; -import { MonarchParseError } from "../errors"; +import { FieldError } from "../errors"; import { MonarchType } from "./type"; export const objectId = () => new MonarchObjectId(); @@ -8,7 +8,7 @@ export class MonarchObjectId extends MonarchType { constructor() { super((input) => { if (ObjectId.isValid(input)) return new ObjectId(input); - throw new MonarchParseError( + throw new FieldError( `expected valid ObjectId received '${typeof input}' ${input}`, ); }); diff --git a/src/types/record.ts b/src/types/record.ts index 31306f5..c761c40 100644 --- a/src/types/record.ts +++ b/src/types/record.ts @@ -1,4 +1,4 @@ -import { MonarchParseError, normalizeFieldPath } from "../errors"; +import { FieldError } from "../errors"; import { type AnyMonarchType, MonarchType } from "./type"; import type { InferTypeInput, InferTypeOutput } from "./type-helpers"; @@ -18,10 +18,10 @@ export class MonarchRecord extends MonarchType< const parser = MonarchType.parser(type); parsed[key] = parser(value); } catch (error) { - if (error instanceof MonarchParseError) { - throw new MonarchParseError(error.message, [ + if (error instanceof FieldError) { + throw new FieldError(error.message, [ key, - ...normalizeFieldPath(error.fieldPath), + ...(error.fieldPath ?? []), ]); } throw error; @@ -29,9 +29,7 @@ export class MonarchRecord extends MonarchType< } return parsed; } - throw new MonarchParseError( - `expected 'object' received '${typeof input}'`, - ); + throw new FieldError(`expected 'object' received '${typeof input}'`); }); } } diff --git a/src/types/string.ts b/src/types/string.ts index b6287aa..f62d27a 100644 --- a/src/types/string.ts +++ b/src/types/string.ts @@ -1,4 +1,4 @@ -import { MonarchParseError } from "../errors"; +import { FieldError } from "../errors"; import { MonarchType } from "./type"; export const string = () => new MonarchString(); @@ -7,9 +7,7 @@ export class MonarchString extends MonarchType { constructor() { super((input) => { if (typeof input === "string") return input; - throw new MonarchParseError( - `expected 'string' received '${typeof input}'`, - ); + throw new FieldError(`expected 'string' received '${typeof input}'`); }); } @@ -29,7 +27,7 @@ export class MonarchString extends MonarchType { return string().extend(this, { postParse: (input) => { if (input.length < length) { - throw new MonarchParseError( + throw new FieldError( `string must be at least ${length} characters long`, ); } @@ -42,7 +40,7 @@ export class MonarchString extends MonarchType { return string().extend(this, { postParse: (input) => { if (input.length > length) { - throw new MonarchParseError( + throw new FieldError( `string must be at most ${length} characters long`, ); } @@ -55,7 +53,7 @@ export class MonarchString extends MonarchType { return string().extend(this, { postParse: (input) => { if (input.length !== length) { - throw new MonarchParseError( + throw new FieldError( `string must be exactly ${length} characters long`, ); } @@ -68,7 +66,7 @@ export class MonarchString extends MonarchType { return string().extend(this, { postParse: (input) => { if (!regex.test(input)) { - throw new MonarchParseError(`string must match pattern ${regex}`); + throw new FieldError(`string must match pattern ${regex}`); } return input; }, @@ -85,7 +83,7 @@ export class MonarchString extends MonarchType { return string().extend(this, { preParse: (input) => { if (input.length === 0) { - throw new MonarchParseError("string must not be empty"); + throw new FieldError("string must not be empty"); } return input; }, @@ -96,7 +94,7 @@ export class MonarchString extends MonarchType { return string().extend(this, { preParse: (input) => { if (!input.includes(searchString)) { - throw new MonarchParseError(`string must include "${searchString}"`); + throw new FieldError(`string must include "${searchString}"`); } return input; }, diff --git a/src/types/tagged-union.ts b/src/types/tagged-union.ts index 0675a0d..c446acc 100644 --- a/src/types/tagged-union.ts +++ b/src/types/tagged-union.ts @@ -1,4 +1,4 @@ -import { MonarchParseError } from "../errors"; +import { FieldError } from "../errors"; import { type AnyMonarchType, MonarchType } from "./type"; import type { InferTypeTaggedUnionInput, @@ -19,15 +19,15 @@ export class MonarchTaggedUnion< super((input) => { if (typeof input === "object" && input !== null) { if (!("tag" in input)) { - throw new MonarchParseError("missing field 'tag' in tagged union"); + throw new FieldError("missing field 'tag' in tagged union"); } if (!("value" in input)) { - throw new MonarchParseError("missing field 'value' in tagged union"); + throw new FieldError("missing field 'value' in tagged union"); } if (Object.keys(input).length > 2) { for (const key of Object.keys(input)) { if (key !== "tag" && key !== "value") { - throw new MonarchParseError( + throw new FieldError( `unknown field '${key}', tagged union may only specify 'tag' and 'value' fields`, ); } @@ -35,23 +35,21 @@ export class MonarchTaggedUnion< } const type = variants[input.tag]; if (!type) { - throw new MonarchParseError(`unknown tag '${input.tag.toString()}'`); + throw new FieldError(`unknown tag '${input.tag.toString()}'`); } try { const parser = MonarchType.parser(type); return { tag: input.tag, value: parser(input.value) }; } catch (error) { - if (error instanceof MonarchParseError) { - throw new MonarchParseError( + if (error instanceof FieldError) { + throw new FieldError( `invalid value for tag '${input.tag.toString()}' ${error.message}'`, ); } throw error; } } - throw new MonarchParseError( - `expected 'object' received '${typeof input}'`, - ); + throw new FieldError(`expected 'object' received '${typeof input}'`); }); } } diff --git a/src/types/tuple.ts b/src/types/tuple.ts index b2a8255..6a4a4f4 100644 --- a/src/types/tuple.ts +++ b/src/types/tuple.ts @@ -1,4 +1,4 @@ -import { MonarchParseError, normalizeFieldPath } from "../errors"; +import { FieldError } from "../errors"; import { type AnyMonarchType, MonarchType } from "./type"; import type { InferTypeTupleInput, InferTypeTupleOutput } from "./type-helpers"; @@ -15,7 +15,7 @@ export class MonarchTuple< super((input) => { if (Array.isArray(input)) { if (input.length > types.length) { - throw new MonarchParseError( + throw new FieldError( `expected array with ${types.length} elements received ${input.length} elements`, ); } @@ -25,10 +25,10 @@ export class MonarchTuple< const parser = MonarchType.parser(type); parsed[index] = parser(input[index]); } catch (error) { - if (error instanceof MonarchParseError) { - throw new MonarchParseError(error.message, [ + if (error instanceof FieldError) { + throw new FieldError(error.message, [ index, - ...normalizeFieldPath(error.fieldPath), + ...(error.fieldPath ?? []), ]); } throw error; @@ -36,9 +36,7 @@ export class MonarchTuple< } return parsed; } - throw new MonarchParseError( - `expected 'array' received '${typeof input}'`, - ); + throw new FieldError(`expected 'array' received '${typeof input}'`); }); } } diff --git a/src/types/type.ts b/src/types/type.ts index a8706d3..41b4817 100644 --- a/src/types/type.ts +++ b/src/types/type.ts @@ -1,4 +1,4 @@ -import { MonarchParseError } from "../errors"; +import { FieldError } from "../errors"; import type { InferTypeInput, InferTypeOutput } from "./type-helpers"; export type Parser = (input: Input) => Output; @@ -92,7 +92,7 @@ export class MonarchType { return type( pipeParser(this._parser, (input) => { const valid = fn(input); - if (!valid) throw new MonarchParseError(message); + if (!valid) throw new FieldError(message); return input; }), this._updater, diff --git a/src/types/union.ts b/src/types/union.ts index 9f56610..db6450d 100644 --- a/src/types/union.ts +++ b/src/types/union.ts @@ -1,4 +1,4 @@ -import { MonarchParseError } from "../errors"; +import { FieldError } from "../errors"; import { type AnyMonarchType, MonarchType } from "./type"; import type { InferTypeUnionInput, InferTypeUnionOutput } from "./type-helpers"; @@ -16,9 +16,9 @@ export class MonarchUnion< const parser = MonarchType.parser(type); return parser(input); } catch (error) { - if (error instanceof MonarchParseError) { + if (error instanceof FieldError) { if (index === variants.length - 1) { - throw new MonarchParseError( + throw new FieldError( `no matching variant found for union type: ${error.message}`, ); } @@ -27,7 +27,7 @@ export class MonarchUnion< throw error; } } - throw new MonarchParseError( + throw new FieldError( `expected one of union variants but received '${typeof input}'`, ); }); From 1d8ec513225b1ea80353d79fb69eaaa701fca7b6 Mon Sep 17 00:00:00 2001 From: Prince Codes Date: Sun, 18 May 2025 19:14:24 +0100 Subject: [PATCH 3/3] export default mongodb client and error --- src/index.ts | 2 +- src/types/type.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index da9942f..732f870 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,7 +14,7 @@ import { tuple } from "./types/tuple"; import { defaulted, nullable, optional } from "./types/type"; import { union } from "./types/union"; -export { ObjectId } from "mongodb"; +export { MongoClient, MongoError, ObjectId } from "mongodb"; export { Collection } from "./collection/collection"; export { createClient, diff --git a/src/types/type.ts b/src/types/type.ts index 41b4817..b0eb47e 100644 --- a/src/types/type.ts +++ b/src/types/type.ts @@ -57,6 +57,10 @@ export class MonarchType { return optional(this); } + public isOptional() { + return this.isInstanceOf(MonarchOptional); + } + public default(defaultInput: TInput | (() => TInput)) { return defaulted( this,