add zod validation for internal Request and Response message
All checks were successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/push/lint Pipeline was successful
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/manual/build Pipeline was successful
ci/woodpecker/manual/lint Pipeline was successful
ci/woodpecker/manual/test Pipeline was successful
ci/woodpecker/pull_request_closed/build Pipeline was successful
ci/woodpecker/pull_request_closed/lint Pipeline was successful
ci/woodpecker/pull_request_closed/test Pipeline was successful

This commit is contained in:
qpismont 2024-01-11 23:03:46 +01:00
parent 702e174c93
commit c47862f376
5 changed files with 93 additions and 37 deletions

2
mod.ts
View file

@ -1,2 +1,4 @@
export * from "./src/service.ts"; export * from "./src/service.ts";
export * from "./src/adaptors/nats.ts"; export * from "./src/adaptors/nats.ts";
export * from "./src/error.ts";
export type { Request, Response } from "./src/messages.ts";

View file

@ -1,13 +1,23 @@
import { z } from "zod";
export interface Request<T> { export interface Request<T> {
service: string; service: string;
subject: string; subject: string;
data?: T; data?: T;
} }
export interface Message<T> { export const InternalRequestSchema = z.object({
from: string; from: z.string(),
data?: T; data: z.optional(z.any()),
} });
export const InternalResponseSchema = z.object({
data: z.optional(z.any()),
statusCode: z.number(),
});
export type InternalRequest = z.infer<typeof InternalRequestSchema>;
export type InternalResponse = z.infer<typeof InternalResponseSchema>;
export interface Response<T> { export interface Response<T> {
data?: T; data?: T;

View file

@ -1,7 +1,13 @@
import { ServiceError } from "nats";
import Adaptor from "./adaptors/adaptor.ts"; import Adaptor from "./adaptors/adaptor.ts";
import { RequestError } from "./error.ts"; import { RequestError } from "./error.ts";
import { Message, Request, Response } from "./messages.ts"; import {
InternalRequest,
InternalRequestSchema,
InternalResponse,
InternalResponseSchema,
Request,
Response,
} from "./messages.ts";
import { RouteSubscribeTypeFn } from "./types.ts"; import { RouteSubscribeTypeFn } from "./types.ts";
import { z } from "zod"; import { z } from "zod";
@ -28,40 +34,62 @@ export default class Service {
this.adaptors[adaptor].subscribe( this.adaptors[adaptor].subscribe(
`${this.name}.${subject}`, `${this.name}.${subject}`,
async (rawReq) => { async (rawReq) => {
const msg: Request<T> = JSON.parse(rawReq); const rawReqJson = JSON.parse(rawReq);
const internalRequestJson = InternalRequestSchema.safeParse(rawReqJson);
if (!internalRequestJson.success) {
return JSON.stringify(
{
statusCode: 400,
data: "bad request structure",
} satisfies InternalResponse,
);
}
const message: Message<z.infer<T>> = { from: this.name }; const internalRequest = internalRequestJson.data;
const req = {
service: internalRequest.from,
subject: subject,
} as Request<z.infer<T>>;
if (msg.data && schema) { if (internalRequest.data && schema) {
const validate = schema.safeParse(msg.data); const validate = schema.safeParse(internalRequest.data);
if (!validate.success) { if (!validate.success) {
return JSON.stringify( return JSON.stringify(
{ {
statusCode: 400, statusCode: 400,
data: validate.error.toString(), data: validate.error,
} satisfies Response<string>, } satisfies InternalResponse,
); );
} else { } else {
message.data = validate.data; req.data = validate.data;
} }
} }
let res;
try { try {
res = await fn(message); const res = await fn(req);
const internalResponse = {
statusCode: res.statusCode,
data: res.data,
} satisfies InternalResponse;
return JSON.stringify(internalResponse);
} catch (err) { } catch (err) {
if (err instanceof ServiceError) { if (err instanceof RequestError) {
res = { data: err.message, statusCode: err.code } as Response< return JSON.stringify(
string {
>; statusCode: err.statusCode,
data: err.message,
} satisfies InternalResponse,
);
} else { } else {
res = { data: "unknow error append", statusCode: 500 } as Response< return JSON.stringify(
string {
>; statusCode: 500,
data: err?.message || "unknow error apend",
} satisfies InternalResponse,
);
} }
} }
return JSON.stringify(res);
}, },
); );
} }
@ -75,24 +103,40 @@ export default class Service {
throw new Error(`${adaptor} adaptor not exist`); throw new Error(`${adaptor} adaptor not exist`);
} }
const rawReq = JSON.stringify(req); const internalRequest = {
from: this.name,
data: req.data,
} satisfies InternalRequest;
const internalRequestJson = JSON.stringify(internalRequest);
try { try {
const rawRes = await this.adaptors[adaptor].request( const rawRes = await this.adaptors[adaptor].request(
`${req.service}.${req.subject}`, `${req.service}.${req.subject}`,
rawReq, internalRequestJson,
); );
const res: Response<z.infer<U>> = JSON.parse(rawRes); const rawResJson: unknown = JSON.parse(rawRes);
const internalResponseJson = InternalResponseSchema.safeParse(rawResJson);
if (res.statusCode < 200 || res.statusCode >= 299) { if (!internalResponseJson.success) {
throw new RequestError("error while request", res.statusCode); throw new RequestError(internalResponseJson.error.toString(), 500);
} }
if (res.data && schema) { const internalResponse = internalResponseJson.data;
const validate = schema.safeParse(res.data); if (
internalResponse.statusCode < 200 || internalResponse.statusCode >= 299
) {
throw new RequestError(
internalResponse.data,
internalResponse.statusCode,
);
}
const res: Response<z.infer<U>> = {
statusCode: internalResponse.statusCode,
};
if (internalResponse.data && schema) {
const validate = schema.safeParse(internalResponse.data);
if (!validate.success) { if (!validate.success) {
throw new ServiceError(400, validate.error.toString()); throw new RequestError(validate.error.message, 400);
} else { } else {
res.data = validate.data; res.data = validate.data;
} }

View file

@ -1,6 +1,6 @@
import { Message, Response } from "./messages.ts"; import { Request, Response } from "./messages.ts";
export type AdaptorSubscribeTypeFn = (msg: string) => Promise<string>; export type AdaptorSubscribeTypeFn = (msg: string) => Promise<string>;
export type RouteSubscribeTypeFn<T, U> = ( export type RouteSubscribeTypeFn<T, U> = (
msg: Message<T>, msg: Request<T>,
) => Promise<Response<U>>; ) => Promise<Response<U>>;

View file

@ -1,9 +1,9 @@
import { z } from "zod"; import { z } from "zod";
import NatsAdaptor from "../src/adaptors/nats.ts"; import NatsAdaptor from "../src/adaptors/nats.ts";
import { Message } from "../src/messages.ts";
import Service from "../src/service.ts"; import Service from "../src/service.ts";
import { assertEquals, assertRejects, assertThrows } from "std/assert/mod.ts"; import { assertEquals, assertRejects, assertThrows } from "std/assert/mod.ts";
import { afterEach, beforeEach, it } from "std/testing/bdd.ts"; import { afterEach, beforeEach, it } from "std/testing/bdd.ts";
import { RequestError } from "../src/error.ts";
let srv!: Service; let srv!: Service;
@ -55,7 +55,7 @@ it("request error", {
srv.addAdaptor(adaptorName, new NatsAdaptor({ servers: [natsServer] })); srv.addAdaptor(adaptorName, new NatsAdaptor({ servers: [natsServer] }));
srv.subscribe(adaptorName, subject, async (msg) => { srv.subscribe(adaptorName, subject, async (msg) => {
return { data: msg.data, statusCode: statusCodeExpected }; throw new RequestError("request error", 500);
}, z.string()); }, z.string());
await srv.listen(); await srv.listen();