diff --git a/package.json b/package.json index 80fffaf..3db13a4 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,10 @@ }, "devDependencies": { "@hazmi35/eslint-config": "^13.3.1", + "@hono/zod-validator": "^0.1.11", "@tsconfig/node-lts": "^20.1.1", "@types/node": "^20.11.20", - "tsx": "^3.12.2" + "tsx": "^3.12.2", + "zod": "^3.22.4" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0050c3e..6dbfee2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,6 +19,9 @@ devDependencies: '@hazmi35/eslint-config': specifier: ^13.3.1 version: 13.3.1(@eslint/eslintrc@3.0.1)(@stylistic/eslint-plugin@1.6.2)(@typescript-eslint/eslint-plugin@7.0.2)(@typescript-eslint/parser@7.0.2)(eslint-config-prettier@9.1.0)(eslint-import-resolver-typescript@3.6.1)(eslint-plugin-import@2.29.1)(eslint-plugin-jsdoc@48.2.0)(eslint-plugin-n@16.6.2)(eslint-plugin-promise@6.1.1)(eslint-plugin-tsdoc@0.2.17)(eslint-plugin-unicorn@51.0.1)(eslint@8.56.0)(globals@14.0.0)(typescript@5.3.3) + '@hono/zod-validator': + specifier: ^0.1.11 + version: 0.1.11(hono@4.0.5)(zod@3.22.4) '@tsconfig/node-lts': specifier: ^20.1.1 version: 20.1.1 @@ -28,6 +31,9 @@ devDependencies: tsx: specifier: ^3.12.2 version: 3.14.0 + zod: + specifier: ^3.22.4 + version: 3.22.4 packages: @@ -360,6 +366,16 @@ packages: engines: {node: '>=18.14.1'} dev: false + /@hono/zod-validator@0.1.11(hono@4.0.5)(zod@3.22.4): + resolution: {integrity: sha512-PQXeHUP0+36qpRt8yfeD7N2jbK3ETlGvSN6dMof/HwUC/APRokQRjpXZm4rrlG71Ft0aWE01+Bm4XejqPie5Uw==} + peerDependencies: + hono: '>=3.9.0' + zod: ^3.19.1 + dependencies: + hono: 4.0.5 + zod: 3.22.4 + dev: true + /@humanwhocodes/config-array@0.11.14: resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} engines: {node: '>=10.10.0'} @@ -1788,7 +1804,6 @@ packages: /hono@4.0.5: resolution: {integrity: sha512-6LEGL1Pf3+dLjVA0NJxAB/3FJ6S3W5qxd/XOG7Wl9YOrpMRZT9lt83R4Ojs8dO6GbAUSutI7zTyjStnSn9sbEg==} engines: {node: '>=16.0.0'} - dev: false /hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} @@ -2784,3 +2799,7 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} dev: true + + /zod@3.22.4: + resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} + dev: true diff --git a/src/App/Decorator/Validator.ts b/src/App/Decorator/Validator.ts new file mode 100644 index 0000000..f4c5318 --- /dev/null +++ b/src/App/Decorator/Validator.ts @@ -0,0 +1,25 @@ +import "reflect-metadata"; +import { zValidator } from "@hono/zod-validator"; +import type { ValidationTargets } from "hono"; +import { z } from "zod"; +import { MetadataValidatorConstant } from "App/Types/ControllerConstant.js"; +import type Controller from "Controller/Controller.js"; + +type ZodData = { [VK in keyof V]: any }; + +/** + * Validate data for route. + * + * @param method - Target of request + * @param data - Data to validate + */ +export default function validator(method: keyof ValidationTargets, data: ZodData): Function { + return function decorate(target: Controller, propKey: string, descriptor: PropertyDescriptor): void { + const targetFunc = descriptor.value as Function; + Reflect.defineMetadata( + MetadataValidatorConstant, + zValidator(method, z.object(data)), + targetFunc + ); + }; +} diff --git a/src/App/Function/InjectController.ts b/src/App/Function/InjectController.ts index 0de5205..a5fb58a 100644 --- a/src/App/Function/InjectController.ts +++ b/src/App/Function/InjectController.ts @@ -1,6 +1,6 @@ import { join } from "node:path"; import { NoDefaultExportError, IsNotConstructorError } from "App/Error/AppError.js"; -import type { DefaultHonoApp } from "Controller/Controller.js"; +import type { DefaultHonoApp } from "App/Types/ControllerTypes.js"; import Controller from "Controller/Controller.js"; import traversalFileScan from "./TraversalFileScan.js"; diff --git a/src/App/Function/OnError.ts b/src/App/Function/OnError.ts new file mode 100644 index 0000000..415b3b5 --- /dev/null +++ b/src/App/Function/OnError.ts @@ -0,0 +1,11 @@ +import type { Context, Env } from "hono"; + +export default function onError(err: Error, c: Context): Response { + const errMessage = err.message; + + console.error(err); + return c.json({ + message: "Something happened, but don't worry maybe it's you or our developer.", + errMessage + }, 500); +} diff --git a/src/App/Types/ControllerConstant.ts b/src/App/Types/ControllerConstant.ts index 0195474..15a580c 100644 --- a/src/App/Types/ControllerConstant.ts +++ b/src/App/Types/ControllerConstant.ts @@ -1 +1,2 @@ export const MetadataConstant = "agniRouting"; +export const MetadataValidatorConstant = "agniValidator"; diff --git a/src/App/Types/ControllerTypes.ts b/src/App/Types/ControllerTypes.ts index 928fbfd..e003957 100644 --- a/src/App/Types/ControllerTypes.ts +++ b/src/App/Types/ControllerTypes.ts @@ -1,5 +1,5 @@ import type { Hono, Context, Env } from "hono"; -import type { BlankSchema, H, BlankInput } from "hono/types"; +import type { BlankSchema, H, BlankInput, Input, ValidationTargets } from "hono/types"; export type AgniRoutingMetadata = { path: string; @@ -10,3 +10,9 @@ export type AgniSupportedMethod = "delete" | "get" | "patch" | "post" | "put"; export type DefaultHonoApp = Hono; export type DefaultHonoContext = Context; export type DefaultHonoFunctionContext = H; + +export type AgniInput = Input & { + in: { [K in keyof ValidationTargets]: V }; + out: { [K in keyof ValidationTargets]: V }; +}; +export type HonoInputContext = Context>; diff --git a/src/Controller/Controller.ts b/src/Controller/Controller.ts index da5bb50..621600c 100644 --- a/src/Controller/Controller.ts +++ b/src/Controller/Controller.ts @@ -1,9 +1,9 @@ import "reflect-metadata"; import type { Factory } from "hono/factory"; import { createFactory } from "hono/factory"; -import type { HandlerInterface } from "hono/types"; +import type { HandlerInterface, MiddlewareHandler } from "hono/types"; import { InsufficientControllerMethodError, NotSupportedMethodError } from "App/Error/AppError.js"; -import { MetadataConstant } from "App/Types/ControllerConstant.js"; +import { MetadataConstant, MetadataValidatorConstant } from "App/Types/ControllerConstant.js"; import type { AgniRoutingMetadata, AgniSupportedMethod, @@ -33,16 +33,27 @@ export default class Controller { if (!Reflect.hasMetadata(MetadataConstant, func)) return; const metadata = Reflect.getMetadata(MetadataConstant, func) as AgniRoutingMetadata; + const metadataKeys = Reflect.getMetadataKeys(func); const honoApp = (this.app as Record)[metadata.method] as HandlerInterface | undefined; if (typeof honoApp !== "function") { throw new NotSupportedMethodError(); } /** - * TODO [2024-02-25]: Add support for middleware, multi handler/middleware - * and validator using Zod Validate + * TODO [2024-02-27]: Add support for middleware, multi handler/middleware */ - const handlers = this._honoFactory.createHandlers(func); + const ctx: DefaultHonoFunctionContext[] = []; + if (metadataKeys.includes(MetadataValidatorConstant)) { + const middleware = Reflect.getMetadata(MetadataValidatorConstant, func) as MiddlewareHandler; + ctx.push(this._honoFactory.createMiddleware(middleware)); + } + + ctx.push(func); + + // TODO [2024-03-01]: How to bypass this? + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + const handlers = this._honoFactory.createHandlers(...ctx); honoApp(metadata.path, ...handlers); } } diff --git a/src/Controller/HelloController.ts b/src/Controller/HelloController.ts index 1874429..96504b5 100644 --- a/src/Controller/HelloController.ts +++ b/src/Controller/HelloController.ts @@ -1,10 +1,25 @@ +import { z } from "zod"; import httpRoute from "App/Decorator/HttpRoute.js"; -import type { DefaultHonoContext } from "App/Types/ControllerTypes.js"; +import validator from "App/Decorator/Validator.js"; +import type { DefaultHonoContext, HonoInputContext } from "App/Types/ControllerTypes.js"; import Controller from "./Controller.js"; +type HelloValidator = { + message: string; +}; + export default class HelloController extends Controller { @httpRoute("get", "/") public hellow(c: DefaultHonoContext): Response { return c.text("Say hello world!"); } + + @httpRoute("post", "/api") + @validator("json", { + message: z.string() + }) + public helloApi(c: HonoInputContext): Response { + const { message } = c.req.valid("json"); + return c.json(`Message: ${message}`); + } } diff --git a/src/Initialize.ts b/src/Initialize.ts index c329183..3e11945 100644 --- a/src/Initialize.ts +++ b/src/Initialize.ts @@ -1,5 +1,6 @@ import { logger } from "hono/logger"; import injectController from "App/Function/InjectController.js"; +import onError from "App/Function/OnError.js"; import type { DefaultHonoApp } from "App/Types/ControllerTypes.js"; export async function initialize(app: DefaultHonoApp): Promise { @@ -12,6 +13,9 @@ export async function initialize(app: DefaultHonoApp): Promise { // Initialize Logger app.use(logger()); + // When error is spawned + app.onError(onError); + /** * The rest of initialize ends here. After this, system will * collecting all of Controllers files. diff --git a/tsconfig.json b/tsconfig.json index 7855d59..dfd18c8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "@tsconfig/node-lts/tsconfig.json", "compilerOptions": { - "lib": ["ES2023", "DOM"], + "lib": ["ES2023", "DOM", "DOM.Iterable"], "strict": true, "jsx": "react-jsx", "jsxImportSource": "hono/jsx",