Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 55 additions & 2 deletions src/errors.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,56 @@
export class MonarchError extends Error {}
export enum ErrorCodes {
MONARCH_ERROR = "MONARCH_ERROR",
VALIDATION_ERROR = "VALIDATION_ERROR",
}

export class MonarchParseError extends MonarchError {}
export class MonarchError extends Error {
constructor(
message: string,
public code: ErrorCodes = ErrorCodes.MONARCH_ERROR,
public cause?: Error,
) {
super(message);
this.name = this.constructor.name;
this.code = code;
this.cause = cause;

if (!!cause && cause.stack) {
this.stack = `${this.stack}\nCaused by: ${cause.stack}`;
} else if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
}

export class FieldError extends Error {
constructor(
message: string,
public fieldPath?: (string | number)[],
) {
super(message);
this.fieldPath = fieldPath ?? [];
}
}

export class MonarchValidationError extends MonarchError {
constructor(
message: string,
public fieldPath: string,
cause?: Error,
) {
super(
`Validation error: '${fieldPath}' ${message}`,
ErrorCodes.VALIDATION_ERROR,
cause,
);
}
}

export function formatValidationPath(pathSegment: {
schema: string;
field: string;
path?: (string | number)[];
}): string {
const { schema, field, path = [] } = pathSegment;
return [schema, field, ...path].join(".");
}
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
27 changes: 23 additions & 4 deletions src/schema/schema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import type { Projection } from "../collection/types/query-options";
import { detectProjection } from "../collection/utils/projection";
import {
FieldError,
MonarchValidationError,
formatValidationPath,
} from "../errors";
import { objectId } from "../types/objectId";
import { type AnyMonarchType, MonarchType } from "../types/type";
import type { Pretty, WithOptionalId } from "../utils/type-helpers";
Expand Down Expand Up @@ -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<this>]);
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<this>]);
if (parsed === undefined) continue;
data[key as keyof typeof data] = parsed;
} catch (error) {
if (error instanceof FieldError) {
throw new MonarchValidationError(
error.message,
formatValidationPath({
schema: this.name,
field: key,
path: error.fieldPath,
}),
);
}
throw error;
}
}
return data;
}
Expand Down
15 changes: 7 additions & 8 deletions src/types/array.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MonarchParseError } from "../errors";
import { FieldError } from "../errors";
import { type AnyMonarchType, MonarchType } from "./type";
import type { InferTypeInput, InferTypeOutput } from "./type-helpers";

Expand All @@ -18,19 +18,18 @@ export class MonarchArray<T extends AnyMonarchType> extends MonarchType<
const parser = MonarchType.parser(type);
parsed[index] = parser(value);
} catch (error) {
if (error instanceof MonarchParseError) {
throw new MonarchParseError(
`element at index '${index}' ${error.message}`,
);
if (error instanceof FieldError) {
throw new FieldError(error.message, [
index,
...(error.fieldPath ?? []),
]);
}
throw error;
}
}
return parsed;
}
throw new MonarchParseError(
`expected 'array' received '${typeof input}'`,
);
throw new FieldError(`expected 'array' received '${typeof input}'`);
});
}
}
6 changes: 2 additions & 4 deletions src/types/boolean.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MonarchParseError } from "../errors";
import { FieldError } from "../errors";
import { MonarchType } from "./type";

export const boolean = () => new MonarchBoolean();
Expand All @@ -7,9 +7,7 @@ export class MonarchBoolean extends MonarchType<boolean, boolean> {
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}'`);
});
}
}
14 changes: 7 additions & 7 deletions src/types/date.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MonarchParseError } from "../errors";
import { FieldError } from "../errors";
import { MonarchType } from "./type";

export const date = () => new MonarchDate();
Expand All @@ -7,15 +7,15 @@ export class MonarchDate extends MonarchType<Date, Date> {
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}'`);
});
}

public after(afterDate: Date) {
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}`);
},
});
}
Expand All @@ -24,7 +24,7 @@ export class MonarchDate extends MonarchType<Date, Date> {
return date().extend(this, {
preParse: (input) => {
if (input > targetDate) {
throw new MonarchParseError(
throw new FieldError(
`date must be before ${targetDate.toISOString()}`,
);
}
Expand All @@ -51,7 +51,7 @@ export class MonarchDateString extends MonarchType<string, Date> {
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}'`,
);
});
Expand All @@ -62,7 +62,7 @@ export class MonarchDateString extends MonarchType<string, Date> {
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}`);
},
});
}
Expand All @@ -72,7 +72,7 @@ export class MonarchDateString extends MonarchType<string, Date> {
preParse: (input) => {
const date = new Date(input);
if (date > targetDate) {
throw new MonarchParseError(
throw new FieldError(
`date must be before ${targetDate.toISOString()}`,
);
}
Expand Down
4 changes: 2 additions & 2 deletions src/types/literal.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MonarchParseError } from "../errors";
import { FieldError } from "../errors";
import { MonarchType } from "./type";

export const literal = <T extends string | number | boolean>(...values: T[]) =>
Expand All @@ -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`,
);
});
Expand Down
14 changes: 5 additions & 9 deletions src/types/number.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MonarchParseError } from "../errors";
import { FieldError } from "../errors";
import { MonarchType } from "./type";

export const number = () => new MonarchNumber();
Expand All @@ -7,17 +7,15 @@ export class MonarchNumber extends MonarchType<number, number> {
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}'`);
});
}

public min(value: number) {
return number().extend(this, {
preParse: (input) => {
if (input < value) {
throw new MonarchParseError(
throw new FieldError(
`number must be greater than or equal to ${value}`,
);
}
Expand All @@ -30,9 +28,7 @@ export class MonarchNumber extends MonarchType<number, number> {
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;
},
Expand All @@ -51,7 +47,7 @@ export class MonarchNumber extends MonarchType<number, number> {
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;
},
Expand Down
15 changes: 8 additions & 7 deletions src/types/object.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MonarchParseError } from "../errors";
import { FieldError } from "../errors";
import { type AnyMonarchType, MonarchType } from "./type";
import type {
InferTypeInput,
Expand All @@ -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`,
);
}
Expand All @@ -33,17 +33,18 @@ export class MonarchObject<
input[key as keyof typeof input] as InferTypeInput<T[keyof T]>,
);
} catch (error) {
if (error instanceof MonarchParseError) {
throw new MonarchParseError(`field '${key}' ${error.message}'`);
if (error instanceof FieldError) {
throw new FieldError(error.message, [
key,
...(error.fieldPath ?? []),
]);
}
throw error;
}
}
return parsed;
}
throw new MonarchParseError(
`expected 'object' received '${typeof input}'`,
);
throw new FieldError(`expected 'object' received '${typeof input}'`);
});
}
}
4 changes: 2 additions & 2 deletions src/types/objectId.ts
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -8,7 +8,7 @@ export class MonarchObjectId extends MonarchType<ObjectId | string, ObjectId> {
constructor() {
super((input) => {
if (ObjectId.isValid(input)) return new ObjectId(input);
throw new MonarchParseError(
throw new FieldError(
`expected valid ObjectId received '${typeof input}' ${input}`,
);
});
Expand Down
13 changes: 7 additions & 6 deletions src/types/record.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MonarchParseError } from "../errors";
import { FieldError } from "../errors";
import { type AnyMonarchType, MonarchType } from "./type";
import type { InferTypeInput, InferTypeOutput } from "./type-helpers";

Expand All @@ -18,17 +18,18 @@ export class MonarchRecord<T extends AnyMonarchType> extends MonarchType<
const parser = MonarchType.parser(type);
parsed[key] = parser(value);
} catch (error) {
if (error instanceof MonarchParseError) {
throw new MonarchParseError(`field '${key}' ${error.message}'`);
if (error instanceof FieldError) {
throw new FieldError(error.message, [
key,
...(error.fieldPath ?? []),
]);
}
throw error;
}
}
return parsed;
}
throw new MonarchParseError(
`expected 'object' received '${typeof input}'`,
);
throw new FieldError(`expected 'object' received '${typeof input}'`);
});
}
}
Loading