Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/vast-terms-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"monarch-orm": patch
---

Add path to error message
39 changes: 38 additions & 1 deletion src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,41 @@ export class MonarchError extends Error {}
/**
* Schema parsing and validation error.
*/
export class MonarchParseError extends MonarchError {}
export class MonarchParseError extends MonarchError {
private path: (string | number)[];
private cause?: MonarchParseError;

constructor(message: string);
constructor(cause: { path: string | number; error: MonarchParseError });
constructor(
error:
| string
| {
path: string | number;
error: MonarchParseError;
},
) {
let message: string;
let path: (string | number)[] = [];
let cause: MonarchParseError | undefined;

if (typeof error === "string") {
message = error;
} else {
cause = error.error.cause ?? error.error;
path = [error.path, ...error.error.path];

const pathString = path.reduce((acc, p, i) => {
if (typeof p === "number") {
return `${acc}[${p}]`;
}
return i === 0 ? p : `${acc}.${p}`;
}, "");
message = `${pathString}: ${cause.message}`;
}

super(message);
this.path = path;
this.cause = cause;
}
}
16 changes: 12 additions & 4 deletions src/schema/schema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Projection } from "../collection/types/query-options";
import { detectProjection } from "../collection/utils/projection";
import { MonarchParseError } 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 @@ -114,10 +115,17 @@ export class Schema<
// parse fields
const types = Schema.types(schema);
for (const [key, type] of Object.entries(types)) {
const parser = MonarchType.parser(type as AnyMonarchType);
const parsed = parser(input[key as keyof InferSchemaInput<T>]);
if (parsed === undefined) continue;
data[key as keyof typeof data] = parsed;
try {
const parser = MonarchType.parser(type as AnyMonarchType);
const parsed = parser(input[key as keyof InferSchemaInput<T>]);
if (parsed === undefined) continue;
data[key as keyof typeof data] = parsed;
} catch (error) {
if (error instanceof MonarchParseError) {
throw new MonarchParseError({ path: key, error });
}
throw error;
}
}
return data;
}
Expand Down
2 changes: 1 addition & 1 deletion src/types/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export class MonarchArray<T extends AnyMonarchType> extends MonarchType<InferTyp
parsed[index] = parser(value);
} catch (error) {
if (error instanceof MonarchParseError) {
throw new MonarchParseError(`element at index '${index}' ${error.message}`);
throw new MonarchParseError({ path: index, error });
}
throw error;
}
Expand Down
2 changes: 1 addition & 1 deletion src/types/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export class MonarchObject<T extends Record<string, AnyMonarchType>> extends Mon
parsed[key as keyof typeof parsed] = parser(input[key as keyof typeof input] as InferTypeInput<T[keyof T]>);
} catch (error) {
if (error instanceof MonarchParseError) {
throw new MonarchParseError(`field '${key}' ${error.message}'`);
throw new MonarchParseError({ path: key, error });
}
throw error;
}
Expand Down
2 changes: 1 addition & 1 deletion src/types/objectId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export class MonarchObjectId extends MonarchType<ObjectId | string, ObjectId> {
constructor() {
super((input) => {
if (ObjectId.isValid(input)) return new ObjectId(input);
throw new MonarchParseError(`expected valid ObjectId received '${typeof input}' ${input}`);
throw new MonarchParseError(`expected 'ObjectId' received '${typeof input}' ${input}`);
});
}
}
2 changes: 1 addition & 1 deletion src/types/record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export class MonarchRecord<T extends AnyMonarchType> extends MonarchType<
parsed[key] = parser(value);
} catch (error) {
if (error instanceof MonarchParseError) {
throw new MonarchParseError(`field '${key}' ${error.message}'`);
throw new MonarchParseError({ path: key, error });
}
throw error;
}
Expand Down
6 changes: 4 additions & 2 deletions src/types/tuple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ export class MonarchTuple<T extends [AnyMonarchType, ...AnyMonarchType[]]> exten
super((input) => {
if (Array.isArray(input)) {
if (input.length !== types.length) {
throw new MonarchParseError(`expected array with ${types.length} elements received ${input.length} elements`);
throw new MonarchParseError(
`expected 'array' with ${types.length} elements received ${input.length} elements`,
);
}
const parsed = [] as InferTypeTupleOutput<T>;
for (const [index, type] of types.entries()) {
Expand All @@ -32,7 +34,7 @@ export class MonarchTuple<T extends [AnyMonarchType, ...AnyMonarchType[]]> exten
parsed[index] = parser(input[index]);
} catch (error) {
if (error instanceof MonarchParseError) {
throw new MonarchParseError(`element at index '${index}' ${error.message}`);
throw new MonarchParseError({ path: index, error });
}
throw error;
}
Expand Down
2 changes: 1 addition & 1 deletion src/types/union.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export class MonarchUnion<T extends [AnyMonarchType, ...AnyMonarchType[]]> exten
throw error;
}
}
throw new MonarchParseError(`expected one of union variants but received '${typeof input}'`);
throw new MonarchParseError(`expected 'union' variant received '${typeof input}'`);
});
}
}
Expand Down
2 changes: 1 addition & 1 deletion tests/query/insert-find.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ describe("Insert and Find Operations", async () => {
it("rejects invalid ObjectId string", async () => {
await expect(async () => {
await collections.users.insertOne({ _id: "not_an_object_id", ...mockUsers[0] });
}).rejects.toThrowError("expected valid ObjectId received");
}).rejects.toThrowError("expected 'ObjectId'");
});

it("inserts empty document with default values", async () => {
Expand Down
4 changes: 2 additions & 2 deletions tests/types/array.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { describe, expect, test } from "vitest";
import { Schema, createSchema } from "../../src";
import { array, number, string } from "../../src/types";

describe("array()", () => {
describe("array", () => {
test("validates array type", () => {
const schema = createSchema("test", {
items: array(number()),
Expand All @@ -14,7 +14,7 @@ describe("array()", () => {
expect(() => Schema.encode(schema, { items: [] })).not.toThrowError();
// @ts-expect-error
expect(() => Schema.encode(schema, { items: [0, "1"] })).toThrowError(
"element at index '1' expected 'number' received 'string'",
"items[1]: expected 'number' received 'string'",
);
const data = Schema.encode(schema, { items: [0, 1] });
expect(data).toStrictEqual({ items: [0, 1] });
Expand Down
2 changes: 1 addition & 1 deletion tests/types/binary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { createDatabase, createSchema, Schema } from "../../src";
import { binary } from "../../src/types";
import { createMockDatabase } from "../mock";

describe("binary()", () => {
describe("binary", () => {
test("validates Binary type", () => {
const schema = createSchema("test", {
data: binary(),
Expand Down
2 changes: 1 addition & 1 deletion tests/types/boolean.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { describe, expect, test } from "vitest";
import { Schema, createSchema } from "../../src";
import { boolean } from "../../src/types";

describe("boolean()", () => {
describe("boolean", () => {
test("validates boolean type", () => {
const schema = createSchema("test", {
isActive: boolean(),
Expand Down
2 changes: 1 addition & 1 deletion tests/types/decimal128.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { createDatabase, createSchema, Schema } from "../../src";
import { decimal128 } from "../../src/types";
import { createMockDatabase } from "../mock";

describe("decimal128()", () => {
describe("decimal128", () => {
test("validates Decimal128 type", () => {
const schema = createSchema("test", {
value: decimal128(),
Expand Down
2 changes: 1 addition & 1 deletion tests/types/long.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { createDatabase, createSchema, Schema } from "../../src";
import { long } from "../../src/types";
import { createMockDatabase } from "../mock";

describe("long()", () => {
describe("long", () => {
test("validates Long type", () => {
const schema = createSchema("test", {
value: long(),
Expand Down
42 changes: 39 additions & 3 deletions tests/types/object.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, test } from "vitest";
import { Schema, createSchema } from "../../src";
import { boolean, literal, object } from "../../src/types";
import { array, boolean, literal, number, object, string } from "../../src/types";

describe("object", () => {
test("object", () => {
Expand All @@ -17,12 +17,12 @@ describe("object", () => {
expect(() =>
// @ts-expect-error
Schema.encode(schema, { permissions: { canUpdate: "yes" } }),
).toThrowError("field 'canUpdate' expected 'boolean' received 'string'");
).toThrowError("permissions.canUpdate: expected 'boolean' received 'string'");
// fields are validates in the order they are registered in type
expect(() =>
// @ts-expect-error
Schema.encode(schema, { permissions: { role: false } }),
).toThrowError("field 'canUpdate' expected 'boolean' received 'undefined'");
).toThrowError("permissions.canUpdate: expected 'boolean' received 'undefined'");
// unknwon fields are rejected
expect(() =>
Schema.encode(schema, {
Expand All @@ -37,4 +37,40 @@ describe("object", () => {
permissions: { canUpdate: true, canDelete: false, role: "moderator" },
});
});

test("nested object", () => {
const schema = createSchema("test", {
user: object({
name: string(),
profile: object({
age: number(),
tags: array(string()),
}),
}),
});

// nested object field error
expect(() =>
Schema.encode(schema, {
// @ts-expect-error
user: { name: "John", profile: { age: "thirty", tags: [] } },
}),
).toThrowError("user.profile.age: expected 'number' received 'string'");

// nested array element error
expect(() =>
Schema.encode(schema, {
// @ts-expect-error
user: { name: "John", profile: { age: 30, tags: ["valid", 123] } },
}),
).toThrowError("user.profile.tags[1]: expected 'string' received 'number'");

// valid data
const data = Schema.encode(schema, {
user: { name: "John", profile: { age: 30, tags: ["developer", "typescript"] } },
});
expect(data).toStrictEqual({
user: { name: "John", profile: { age: 30, tags: ["developer", "typescript"] } },
});
});
});
6 changes: 3 additions & 3 deletions tests/types/objectid.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { createDatabase, createSchema, Schema } from "../../src";
import { objectId } from "../../src/types";
import { createMockDatabase } from "../mock";

describe("objectId()", () => {
describe("objectId", () => {
test("validates ObjectId type", () => {
const schema = createSchema("test", {
id: objectId(),
Expand Down Expand Up @@ -32,9 +32,9 @@ describe("objectId()", () => {
id: objectId(),
});

expect(() => Schema.encode(schema, { id: "invalid" })).toThrowError("expected valid ObjectId");
expect(() => Schema.encode(schema, { id: "invalid" })).toThrowError("expected 'ObjectId'");
// @ts-expect-error
expect(() => Schema.encode(schema, { id: {} })).toThrowError("expected valid ObjectId");
expect(() => Schema.encode(schema, { id: {} })).toThrowError("expected 'ObjectId'");
});

test("works with nullable and optional", () => {
Expand Down
2 changes: 1 addition & 1 deletion tests/types/record.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe("record", () => {
expect(() =>
// @ts-expect-error
Schema.encode(schema, { grades: { math: "50" } }),
).toThrowError("field 'math' expected 'number' received 'string'");
).toThrowError("grades.math: expected 'number' received 'string'");
const data = Schema.encode(schema, { grades: { math: 50 } });
expect(data).toStrictEqual({ grades: { math: 50 } });
});
Expand Down
8 changes: 6 additions & 2 deletions tests/types/tuple.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,17 @@ describe("tuple", () => {
expect(() => Schema.encode(schema, {})).toThrowError("expected 'array' received 'undefined'");
// @ts-expect-error
expect(() => Schema.encode(schema, { items: [] })).toThrowError(
"expected array with 2 elements received 0 elements",
"expected 'array' with 2 elements received 0 elements",
);
const data = Schema.encode(schema, { items: [0, "1"] });
expect(data).toStrictEqual({ items: [0, "1"] });
// @ts-expect-error
expect(() => Schema.encode(schema, { items: [0, 1] })).toThrowError(
"items[1]: expected 'string' received 'number'",
);
// @ts-expect-error
expect(() => Schema.encode(schema, { items: [1, "1", 2] })).toThrowError(
"expected array with 2 elements received 3 elements",
"expected 'array' with 2 elements received 3 elements",
);
});
});
2 changes: 1 addition & 1 deletion tests/types/type.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { pipe, type } from "../../src/types";
const simpleString = () => type<string>((input) => input);
const simpleNumber = () => type<number>((input) => input);

describe("type()", () => {
describe("type", () => {
it("validates and transforms input", () => {
const schema = createSchema("users", {
age: simpleNumber()
Expand Down