Skip to content

Commit 6e34a88

Browse files
authored
chore!(feat: app/controller): add validation decorator and inject on 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
1 parent b432722 commit 6e34a88

File tree

11 files changed

+105
-11
lines changed

11 files changed

+105
-11
lines changed

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515
},
1616
"devDependencies": {
1717
"@hazmi35/eslint-config": "^13.3.1",
18+
"@hono/zod-validator": "^0.1.11",
1819
"@tsconfig/node-lts": "^20.1.1",
1920
"@types/node": "^20.11.20",
20-
"tsx": "^3.12.2"
21+
"tsx": "^3.12.2",
22+
"zod": "^3.22.4"
2123
}
2224
}

pnpm-lock.yaml

Lines changed: 20 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/App/Decorator/Validator.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import "reflect-metadata";
2+
import { zValidator } from "@hono/zod-validator";
3+
import type { ValidationTargets } from "hono";
4+
import { z } from "zod";
5+
import { MetadataValidatorConstant } from "App/Types/ControllerConstant.js";
6+
import type Controller from "Controller/Controller.js";
7+
8+
type ZodData<V> = { [VK in keyof V]: any };
9+
10+
/**
11+
* Validate data for route.
12+
*
13+
* @param method - Target of request
14+
* @param data - Data to validate
15+
*/
16+
export default function validator<T extends {}>(method: keyof ValidationTargets, data: ZodData<T>): Function {
17+
return function decorate(target: Controller, propKey: string, descriptor: PropertyDescriptor): void {
18+
const targetFunc = descriptor.value as Function;
19+
Reflect.defineMetadata(
20+
MetadataValidatorConstant,
21+
zValidator(method, z.object(data)),
22+
targetFunc
23+
);
24+
};
25+
}

src/App/Function/InjectController.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { join } from "node:path";
22
import { NoDefaultExportError, IsNotConstructorError } from "App/Error/AppError.js";
3-
import type { DefaultHonoApp } from "Controller/Controller.js";
3+
import type { DefaultHonoApp } from "App/Types/ControllerTypes.js";
44
import Controller from "Controller/Controller.js";
55
import traversalFileScan from "./TraversalFileScan.js";
66

src/App/Function/OnError.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { Context, Env } from "hono";
2+
3+
export default function onError(err: Error, c: Context<Env, any, any>): Response {
4+
const errMessage = err.message;
5+
6+
console.error(err);
7+
return c.json({
8+
message: "Something happened, but don't worry maybe it's you or our developer.",
9+
errMessage
10+
}, 500);
11+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export const MetadataConstant = "agniRouting";
2+
export const MetadataValidatorConstant = "agniValidator";

src/App/Types/ControllerTypes.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Hono, Context, Env } from "hono";
2-
import type { BlankSchema, H, BlankInput } from "hono/types";
2+
import type { BlankSchema, H, BlankInput, Input, ValidationTargets } from "hono/types";
33

44
export type AgniRoutingMetadata = {
55
path: string;
@@ -10,3 +10,9 @@ export type AgniSupportedMethod = "delete" | "get" | "patch" | "post" | "put";
1010
export type DefaultHonoApp = Hono<Env, BlankSchema, string>;
1111
export type DefaultHonoContext = Context<Env, string, BlankInput>;
1212
export type DefaultHonoFunctionContext = H<Env, string, BlankInput, Response>;
13+
14+
export type AgniInput<V> = Input & {
15+
in: { [K in keyof ValidationTargets]: V };
16+
out: { [K in keyof ValidationTargets]: V };
17+
};
18+
export type HonoInputContext<V> = Context<Env, string, AgniInput<V>>;

src/Controller/Controller.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import "reflect-metadata";
22
import type { Factory } from "hono/factory";
33
import { createFactory } from "hono/factory";
4-
import type { HandlerInterface } from "hono/types";
4+
import type { HandlerInterface, MiddlewareHandler } from "hono/types";
55
import { InsufficientControllerMethodError, NotSupportedMethodError } from "App/Error/AppError.js";
6-
import { MetadataConstant } from "App/Types/ControllerConstant.js";
6+
import { MetadataConstant, MetadataValidatorConstant } from "App/Types/ControllerConstant.js";
77
import type {
88
AgniRoutingMetadata,
99
AgniSupportedMethod,
@@ -33,16 +33,27 @@ export default class Controller {
3333
if (!Reflect.hasMetadata(MetadataConstant, func)) return;
3434

3535
const metadata = Reflect.getMetadata(MetadataConstant, func) as AgniRoutingMetadata;
36+
const metadataKeys = Reflect.getMetadataKeys(func);
3637
const honoApp = (this.app as Record<AgniSupportedMethod, any>)[metadata.method] as HandlerInterface | undefined;
3738
if (typeof honoApp !== "function") {
3839
throw new NotSupportedMethodError();
3940
}
4041

4142
/**
42-
* TODO [2024-02-25]: Add support for middleware, multi handler/middleware
43-
* and validator using Zod Validate
43+
* TODO [2024-02-27]: Add support for middleware, multi handler/middleware
4444
*/
45-
const handlers = this._honoFactory.createHandlers(func);
45+
const ctx: DefaultHonoFunctionContext[] = [];
46+
if (metadataKeys.includes(MetadataValidatorConstant)) {
47+
const middleware = Reflect.getMetadata(MetadataValidatorConstant, func) as MiddlewareHandler;
48+
ctx.push(this._honoFactory.createMiddleware(middleware));
49+
}
50+
51+
ctx.push(func);
52+
53+
// TODO [2024-03-01]: How to bypass this?
54+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
55+
// @ts-expect-error
56+
const handlers = this._honoFactory.createHandlers(...ctx);
4657
honoApp(metadata.path, ...handlers);
4758
}
4859
}

src/Controller/HelloController.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,25 @@
1+
import { z } from "zod";
12
import httpRoute from "App/Decorator/HttpRoute.js";
2-
import type { DefaultHonoContext } from "App/Types/ControllerTypes.js";
3+
import validator from "App/Decorator/Validator.js";
4+
import type { DefaultHonoContext, HonoInputContext } from "App/Types/ControllerTypes.js";
35
import Controller from "./Controller.js";
46

7+
type HelloValidator = {
8+
message: string;
9+
};
10+
511
export default class HelloController extends Controller {
612
@httpRoute("get", "/")
713
public hellow(c: DefaultHonoContext): Response {
814
return c.text("Say hello world!");
915
}
16+
17+
@httpRoute("post", "/api")
18+
@validator<HelloValidator>("json", {
19+
message: z.string()
20+
})
21+
public helloApi(c: HonoInputContext<HelloValidator>): Response {
22+
const { message } = c.req.valid("json");
23+
return c.json(`Message: ${message}`);
24+
}
1025
}

src/Initialize.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { logger } from "hono/logger";
22
import injectController from "App/Function/InjectController.js";
3+
import onError from "App/Function/OnError.js";
34
import type { DefaultHonoApp } from "App/Types/ControllerTypes.js";
45

56
export async function initialize(app: DefaultHonoApp): Promise<void> {
@@ -12,6 +13,9 @@ export async function initialize(app: DefaultHonoApp): Promise<void> {
1213
// Initialize Logger
1314
app.use(logger());
1415

16+
// When error is spawned
17+
app.onError(onError);
18+
1519
/**
1620
* The rest of initialize ends here. After this, system will
1721
* collecting all of Controllers files.

0 commit comments

Comments
 (0)