Skip to content

Commit

Permalink
Normalized validation error zodios (#28)
Browse files Browse the repository at this point in the history
  • Loading branch information
eduwardo authored Apr 16, 2024
1 parent 4e7953f commit b9152fd
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 17 deletions.
39 changes: 32 additions & 7 deletions packages/models/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,27 @@ export class ApiError<T> extends Error {
public title: string;
public detail: string;
public correlationId?: string;
public errors?: string[];

constructor({
code,
title,
detail,
correlationId,
errors,
}: {
code: T;
title: string;
detail: string;
correlationId?: string;
errors?: string[];
}) {
super(detail);
this.code = code;
this.title = title;
this.detail = detail;
this.correlationId = correlationId;
this.errors = errors;
}
}

Expand All @@ -53,19 +57,30 @@ export function makeApiProblemBuilder<T extends string>(errors: {
return (error, httpMapper) => {
const makeProblem = (
httpStatus: number,
{ code, title, detail, correlationId }: ApiError<T | CommonErrorCodes>,
{
code,
title,
detail,
correlationId,
errors,
}: ApiError<T | CommonErrorCodes>,
): Problem => ({
type: "about:blank",
title,
status: httpStatus,
detail,
correlationId,
errors: [
{
code: allErrors[code],
detail,
},
],
errors: errors
? errors.map((detail) => ({
code: allErrors[code],
detail,
}))
: [
{
code: allErrors[code],
detail,
},
],
});

return match<unknown, Problem>(error)
Expand All @@ -78,6 +93,7 @@ export function makeApiProblemBuilder<T extends string>(errors: {

const errorCodes = {
genericError: "9991",
validationFailed: "9992",
} as const;

export type CommonErrorCodes = keyof typeof errorCodes;
Expand All @@ -89,3 +105,12 @@ export function genericError(details: string): ApiError<CommonErrorCodes> {
title: "Unexpected error",
});
}

export function validationFailed(errors: string[]): ApiError<CommonErrorCodes> {
return new ApiError({
detail: "Validation failed",
errors: errors,
code: "validationFailed",
title: "Bad Request",
});
}
5 changes: 4 additions & 1 deletion packages/probing-api/src/routers/eserviceRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
isActive,
} from "../utilities/enumUtils.js";
import { ApiEServiceContent } from "../model/eservice.js";
import validationErrorHandler from "../utilities/validationErrorHandler.js";

const eServiceRouter =
(
Expand All @@ -30,7 +31,9 @@ const eServiceRouter =
(operationsApiClient: ZodiosInstance<Api>) => {
const operationsService: OperationsService =
operationsServiceBuilder(operationsApiClient);
const router = ctx.router(api.api);
const router = ctx.router(api.api, {
validationErrorHandler,
});

router
.post(
Expand Down
42 changes: 42 additions & 0 deletions packages/probing-api/src/utilities/validationErrorHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Request, Response, NextFunction } from "express";
import {
Problem,
genericError,
validationFailed,
} from "pagopa-interop-probing-models";
import { P, match } from "ts-pattern";
import { makeApiProblem } from "../model/domain/errors.js";
import { z } from "zod";

interface ZodValidationError {
context: string;
error: z.ZodIssue[];
}

const validationErrorHandler = (
error: unknown,
_req: Request,
res: Response,
next: NextFunction,
): void => {
if (!error) return next();
else {
const problem = match<unknown, Problem>(error)
.with(
P.shape({ context: P.string, error: P.array(P.any) }),
(e: ZodValidationError) => {
const errors = e.error.map(
(issue) => `${e.context}: ${JSON.stringify(issue)}`,
);
return makeApiProblem(validationFailed(errors), () => 400);
},
)
.otherwise((e) => {
return makeApiProblem(genericError(`${e}`), () => 500);
});

res.status(problem.status).json(problem).end();
}
};

export default validationErrorHandler;
21 changes: 13 additions & 8 deletions packages/probing-api/test/router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,10 @@ describe("eService Router", () => {
)
.set("Content-Type", "application/json");

expect(response.text).toContain(`"context":"body"`);
expect(response.text).toContain("eServiceState");
expect(response.text).toContain("Validation failed");
expect(response.text).toContain("body");
expect(response.text).toContain("Required");
expect(response.text).toContain("eServiceState");
expect(response.status).toBe(400);
});

Expand Down Expand Up @@ -220,9 +221,10 @@ describe("eService Router", () => {
)
.set("Content-Type", "application/json");

expect(response.text).toContain(`"context":"body"`);
expect(response.text).toContain("probingEnabled");
expect(response.text).toContain("Validation failed");
expect(response.text).toContain("body");
expect(response.text).toContain("Required");
expect(response.text).toContain("probingEnabled");
expect(response.status).toBe(400);
});

Expand Down Expand Up @@ -326,10 +328,11 @@ describe("eService Router", () => {
)
.set("Content-Type", "application/json");

expect(response.text).toContain(`"context":"body"`);
expect(response.text).toContain("Validation failed");
expect(response.text).toContain("body");
expect(response.text).toContain("Required");
expect(response.text).toContain("startTime");
expect(response.text).toContain("endTime");
expect(response.text).toContain("Required");
expect(response.status).toBe(400);
});

Expand Down Expand Up @@ -420,7 +423,8 @@ describe("eService Router", () => {
offset,
});

expect(response.text).toContain(`"context":"query.limit"`);
expect(response.text).toContain("Validation failed");
expect(response.text).toContain("query.limit");
expect(response.text).toContain("Required");
expect(response.status).toBe(400);
});
Expand All @@ -447,7 +451,8 @@ describe("eService Router", () => {
limit,
});

expect(response.text).toContain(`"context":"query.offset"`);
expect(response.text).toContain("Validation failed");
expect(response.text).toContain("query.offset");
expect(response.text).toContain("Required");
expect(response.status).toBe(400);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@ import { ExpressContext, ZodiosContext } from "pagopa-interop-probing-commons";
import { StatisticsService } from "../services/statisticsService.js";
import { api } from "../model/generated/api.js";
import { statisticsErrorMapper } from "../utilities/errorMappers.js";
import validationErrorHandler from "../utilities/validationErrorHandler.js";

const statisticsRouter = (
ctx: ZodiosContext,
): ((
statisticsService: StatisticsService,
) => ZodiosRouter<ZodiosEndpointDefinitions, ExpressContext>) => {
return (statisticsService: StatisticsService) => {
const router = ctx.router(api.api);
const router = ctx.router(api.api, {
validationErrorHandler,
});

router
.get("/telemetryData/eservices/:eserviceRecordId", async (req, res) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Request, Response, NextFunction } from "express";
import {
Problem,
genericError,
validationFailed,
} from "pagopa-interop-probing-models";
import { P, match } from "ts-pattern";
import { makeApiProblem } from "../model/domain/errors.js";
import { z } from "zod";

interface ZodValidationError {
context: string;
error: z.ZodIssue[];
}

const validationErrorHandler = (
error: unknown,
_req: Request,
res: Response,
next: NextFunction,
): void => {
if (!error) return next();
else {
const problem = match<unknown, Problem>(error)
.with(
P.shape({ context: P.string, error: P.array(P.any) }),
(e: ZodValidationError) => {
const errors = e.error.map(
(issue) => `${e.context}: ${JSON.stringify(issue)}`,
);
return makeApiProblem(validationFailed(errors), () => 400);
},
)
.otherwise((e) => {
return makeApiProblem(genericError(`${e}`), () => 500);
});

res.status(problem.status).json(problem).end();
}
};

export default validationErrorHandler;

0 comments on commit b9152fd

Please sign in to comment.