Skip to content

Commit

Permalink
chore!(feat: app/controller): add validation decorator and inject on …
Browse files Browse the repository at this point in the history
…the base app (#1)

* fix(app/function): false import

* feat(app/decorator): new validator decorator and implement it

* fix(app/types): fix Input decorator

It's fucked up using Partial...

* refactor(app/controller): extend Todo target

* refactor(app/controller): extend Todo target

* refactor(app/decorator/validator): improve typing for zod validator

* refactor(app/controller): move the typing
  • Loading branch information
ikr4-m authored Feb 27, 2024
1 parent b432722 commit 6e34a88
Show file tree
Hide file tree
Showing 11 changed files with 105 additions and 11 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
21 changes: 20 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 25 additions & 0 deletions src/App/Decorator/Validator.ts
Original file line number Diff line number Diff line change
@@ -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<V> = { [VK in keyof V]: any };

/**
* Validate data for route.
*
* @param method - Target of request
* @param data - Data to validate
*/
export default function validator<T extends {}>(method: keyof ValidationTargets, data: ZodData<T>): 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
);
};
}
2 changes: 1 addition & 1 deletion src/App/Function/InjectController.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down
11 changes: 11 additions & 0 deletions src/App/Function/OnError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { Context, Env } from "hono";

export default function onError(err: Error, c: Context<Env, any, any>): 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);
}
1 change: 1 addition & 0 deletions src/App/Types/ControllerConstant.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export const MetadataConstant = "agniRouting";
export const MetadataValidatorConstant = "agniValidator";
8 changes: 7 additions & 1 deletion src/App/Types/ControllerTypes.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -10,3 +10,9 @@ export type AgniSupportedMethod = "delete" | "get" | "patch" | "post" | "put";
export type DefaultHonoApp = Hono<Env, BlankSchema, string>;
export type DefaultHonoContext = Context<Env, string, BlankInput>;
export type DefaultHonoFunctionContext = H<Env, string, BlankInput, Response>;

export type AgniInput<V> = Input & {
in: { [K in keyof ValidationTargets]: V };
out: { [K in keyof ValidationTargets]: V };
};
export type HonoInputContext<V> = Context<Env, string, AgniInput<V>>;
21 changes: 16 additions & 5 deletions src/Controller/Controller.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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<AgniSupportedMethod, any>)[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);
}
}
Expand Down
17 changes: 16 additions & 1 deletion src/Controller/HelloController.ts
Original file line number Diff line number Diff line change
@@ -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<HelloValidator>("json", {
message: z.string()
})
public helloApi(c: HonoInputContext<HelloValidator>): Response {
const { message } = c.req.valid("json");
return c.json(`Message: ${message}`);
}
}
4 changes: 4 additions & 0 deletions src/Initialize.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
Expand All @@ -12,6 +13,9 @@ export async function initialize(app: DefaultHonoApp): Promise<void> {
// 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.
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down

0 comments on commit 6e34a88

Please sign in to comment.