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
4 changes: 2 additions & 2 deletions src/core/auth/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export async function readAuth(): Promise<AuthData> {
const result = AuthDataSchema.safeParse(parsed);

if (!result.success) {
throw new SchemaValidationError("Invalid authentication data", result.error);
throw new SchemaValidationError("Invalid authentication data", result.error, getAuthFilePath());
}

return result.data;
Expand All @@ -52,7 +52,7 @@ export async function writeAuth(authData: AuthData): Promise<void> {
const result = AuthDataSchema.safeParse(authData);

if (!result.success) {
throw new SchemaValidationError("Invalid authentication data", result.error);
throw new SchemaValidationError("Invalid authentication data", result.error, getAuthFilePath());
}

try {
Expand Down
18 changes: 13 additions & 5 deletions src/core/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,16 +189,24 @@ export class ConfigExistsError extends UserError {
* @example
* const result = schema.safeParse(data);
* if (!result.success) {
* throw new SchemaValidationError("Invalid entity file", result.error);
* throw new SchemaValidationError("Invalid entity file", result.error, entityPath);
* }
*/
export class SchemaValidationError extends UserError {
readonly code = "SCHEMA_INVALID";
readonly filePath?: string;

constructor(context: string, zodError: z.ZodError) {
super(`${context}:\n${z.prettifyError(zodError)}`, {
hints: [{ message: "Fix the schema/data structure errors above" }],
});
constructor(context: string, zodError: z.ZodError, filePath?: string) {
const message = filePath
? `${context} in ${filePath}:\n${z.prettifyError(zodError)}`
: `${context}:\n${z.prettifyError(zodError)}`;

const hints: ErrorHint[] = filePath
? [{ message: `Fix the schema/data structure errors in ${filePath}` }]
: [{ message: "Fix the schema/data structure errors above" }];

super(message, { hints });
this.filePath = filePath;
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/core/project/app-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ async function readAppConfig(
const result = AppConfigSchema.safeParse(parsed);

if (!result.success) {
throw new SchemaValidationError(`Invalid app configuration in ${configPath}`, result.error);
throw new SchemaValidationError("Invalid app configuration", result.error, configPath);
}

return result.data;
Expand Down
2 changes: 1 addition & 1 deletion src/core/project/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export async function readProjectConfig(
const result = ProjectConfigSchema.safeParse(parsed);

if (!result.success) {
throw new SchemaValidationError("Invalid project configuration", result.error);
throw new SchemaValidationError("Invalid project configuration", result.error, configPath);
}

const project = result.data;
Expand Down
4 changes: 3 additions & 1 deletion src/core/project/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ const SiteConfigSchema = z.object({
});

export const ProjectConfigSchema = z.object({
name: z.string().min(1, "App name cannot be empty"),
name: z.string({
error: "App name cannot be empty"
}).min(1, "App name cannot be empty"),
description: z.string().optional(),
site: SiteConfigSchema.optional(),
entitiesDir: z.string().optional().default("entities"),
Expand Down
2 changes: 1 addition & 1 deletion src/core/project/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export async function listTemplates(): Promise<Template[]> {
const result = TemplatesConfigSchema.safeParse(parsed);

if (!result.success) {
throw new SchemaValidationError("Invalid templates configuration", result.error);
throw new SchemaValidationError("Invalid templates configuration", result.error, getTemplatesIndexPath());
}

return result.data.templates;
Expand Down
2 changes: 1 addition & 1 deletion src/core/resources/agent/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ async function readAgentFile(agentPath: string): Promise<AgentConfig> {
const result = AgentConfigSchema.safeParse(parsed);

if (!result.success) {
throw new SchemaValidationError(`Invalid agent file at ${agentPath}`, result.error);
throw new SchemaValidationError("Invalid agent file", result.error, agentPath);
}

return result.data;
Expand Down
2 changes: 1 addition & 1 deletion src/core/resources/entity/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ async function readEntityFile(entityPath: string): Promise<Entity> {
const result = EntitySchema.safeParse(parsed);

if (!result.success) {
throw new SchemaValidationError(`Invalid entity file at ${entityPath}`, result.error);
throw new SchemaValidationError("Invalid entity file", result.error, entityPath);
}

return result.data;
Expand Down
4 changes: 2 additions & 2 deletions src/core/resources/function/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export async function readFunctionConfig(
const result = FunctionConfigSchema.safeParse(parsed);

if (!result.success) {
throw new SchemaValidationError(`Invalid function configuration in ${configPath}`, result.error);
throw new SchemaValidationError("Invalid function configuration", result.error, configPath);
}

return result.data;
Expand All @@ -38,7 +38,7 @@ export async function readFunction(configPath: string): Promise<Function> {
const functionData = { ...config, entryPath, files };
const result = FunctionSchema.safeParse(functionData);
if (!result.success) {
throw new SchemaValidationError(`Invalid function in ${configPath}`, result.error);
throw new SchemaValidationError("Invalid function", result.error, configPath);
}

return result.data;
Expand Down
20 changes: 19 additions & 1 deletion tests/core/errors.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ describe("UserError subclasses", () => {
expect(error.message).toBe("Project already exists");
});

it("SchemaValidationError formats ZodError automatically", async () => {
it("SchemaValidationError formats ZodError without filePath", async () => {
const { z } = await import("zod");
const schema = z.object({ name: z.string() });
const result = schema.safeParse({ name: 123 });
Expand All @@ -93,6 +93,24 @@ describe("UserError subclasses", () => {
// Zod prettifyError uses lowercase
expect(error.message).toContain("expected string");
expect(error.message).toContain("name");
expect(error.filePath).toBeUndefined();
} else {
throw new Error("Expected parse to fail");
}
});

it("SchemaValidationError includes filePath in message and hints when provided", async () => {
const { z } = await import("zod");
const schema = z.object({ name: z.string() });
const result = schema.safeParse({ name: 123 });

if (!result.success) {
const error = new SchemaValidationError("Invalid entity file", result.error, "/path/to/entity.jsonc");
expect(error.code).toBe("SCHEMA_INVALID");
expect(error.message).toContain("Invalid entity file in /path/to/entity.jsonc");
expect(error.message).toContain("expected string");
expect(error.filePath).toBe("/path/to/entity.jsonc");
expect(error.hints[0].message).toContain("/path/to/entity.jsonc");
} else {
throw new Error("Expected parse to fail");
}
Expand Down