;
- name: N;
+ hono: OpenAPIHono;
+ modules?: BaseBuildModuleReturn[];
+ name: M;
plugin: P;
+ routes: Routes;
}
-export function createModuleApi<
- E extends Env,
- S extends Schema = Schema,
- N extends string = string,
- P extends string = string,
+export interface BuildModuleReturn<
+ P extends string,
+ M extends string,
+ Routes extends Route[] = Route[],
+ Modules extends BaseBuildModuleReturn
[] = BaseBuildModuleReturn
[],
+> extends BaseBuildModuleReturn
{
+ modules?: Modules;
+}
+
+export function buildModule<
+ const P extends string,
+ const M extends string,
+ const Routes extends Route[],
+ Modules extends BaseBuildModuleReturn
[],
>({
- name,
- plugin,
routes,
+ plugin,
+ name,
+ modules,
}: {
- name: N;
+ modules?: Modules;
+ name: M;
plugin: P;
- routes: OpenAPIHono;
-}): ModuleApi {
- const current = routes;
-
- return {
- app: current,
- plugin,
- name,
- };
+ routes: Routes;
+}): BuildModuleReturn {
+ const hono = new OpenAPIHono();
+
+ if (routes) {
+ routes.forEach(({ handler, route }) => {
+ hono.openapi(route, handler);
+ });
+ }
+
+ if (modules) {
+ modules.forEach(module => {
+ hono.route(`/${module.name}`, module.hono);
+ });
+ }
+
+ return { routes, plugin, hono, name, modules };
}
diff --git a/packages/vitnode/src/api/lib/plugin.ts b/packages/vitnode/src/api/lib/plugin.ts
deleted file mode 100644
index 330beb2c8..000000000
--- a/packages/vitnode/src/api/lib/plugin.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { OpenAPIHono } from '@hono/zod-openapi';
-import { Env, Schema } from 'hono';
-
-import { ModuleApi } from './module';
-
-export interface PluginAPI {
- app: OpenAPIHono;
- name: Plugin;
-}
-
-export function createPluginApi<
- T extends Schema,
- E extends Env = Env,
- Plugin extends string = string,
->({
- name,
- modules,
-}: {
- modules: ModuleApi[];
- name: Plugin;
-}): PluginAPI {
- const root = new OpenAPIHono();
- modules.forEach(handler => {
- root.route(`/${handler.name}`, handler.app);
- });
-
- return {
- name,
- app: root,
- };
-}
diff --git a/packages/vitnode/src/api/lib/route.ts b/packages/vitnode/src/api/lib/route.ts
index dcb8ad1c9..e2fe5f56b 100644
--- a/packages/vitnode/src/api/lib/route.ts
+++ b/packages/vitnode/src/api/lib/route.ts
@@ -1,5 +1,8 @@
-import { createRoute as createRouteHono, RouteConfig } from '@hono/zod-openapi';
-import { MiddlewareHandler } from 'hono';
+import {
+ createRoute as createRouteHono,
+ RouteConfig,
+ RouteHandler,
+} from '@hono/zod-openapi';
import { sessionMiddleware } from '../middlewares/session';
@@ -8,36 +11,55 @@ type RoutingPath =
? `${Head}/:${Param}${RoutingPath}`
: P;
-export function createApiRoute<
+type ValidHandler = (
+ c: Parameters>[0],
+) => ReturnType>;
+
+export const buildRoute = <
P extends string,
R extends Omit & {
+ isAuthorization?: boolean;
path: P;
},
+ H extends ValidHandler,
>({
- isAuth,
- plugin,
- ...routeConfig
-}: R & {
- isAuth?: boolean;
- plugin: string;
-}): R & {
- getRoutingPath: () => RoutingPath;
-} {
- const middlewareFromConfig: MiddlewareHandler[] = routeConfig.middleware
- ? Array.isArray(routeConfig.middleware)
- ? routeConfig.middleware
- : [routeConfig.middleware]
+ route,
+ handler,
+}: {
+ handler: H;
+ route: R;
+}): {
+ handler: H;
+ route: R & {
+ getRoutingPath: () => RoutingPath;
+ };
+} => {
+ const { isAuthorization, middleware, ...restOfRoute } = route;
+ const tags: string[] = ['test123 from createRoute', ...(route.tags ?? [])];
+ const middlewareArray = middleware
+ ? Array.isArray(middleware)
+ ? middleware
+ : [middleware]
: [];
- const tags: string[] = [
- plugin.charAt(0).toUpperCase() + plugin.slice(1),
- ...(routeConfig.tags ?? []),
- ];
- return createRouteHono({
- middleware: isAuth ? [sessionMiddleware(), ...middlewareFromConfig] : [],
- tags,
- ...routeConfig,
- }) as unknown as R & {
- getRoutingPath: () => RoutingPath;
+ return {
+ route: createRouteHono({
+ middleware: isAuthorization
+ ? [sessionMiddleware(), ...middlewareArray]
+ : middlewareArray,
+ tags,
+ ...restOfRoute,
+ }) as R & {
+ getRoutingPath: () => RoutingPath;
+ },
+ handler,
};
+};
+
+export interface Route<
+ R extends RouteConfig = RouteConfig,
+ H extends RouteHandler = RouteHandler,
+> {
+ handler: H;
+ route: R;
}
diff --git a/packages/vitnode/src/api/modules/admin/admin.module.ts b/packages/vitnode/src/api/modules/admin/admin.module.ts
index 5e129cb68..8f46e09ef 100644
--- a/packages/vitnode/src/api/modules/admin/admin.module.ts
+++ b/packages/vitnode/src/api/modules/admin/admin.module.ts
@@ -1,12 +1,9 @@
-import { createModuleApi } from '@/api/lib/module';
-import { OpenAPIHono } from '@hono/zod-openapi';
+import { buildModule } from '@/api/lib/module';
import { sessionAdminRoute } from './routes/session.route';
-export const adminModule = createModuleApi({
+export const adminModule = buildModule({
name: 'admin',
plugin: 'core',
- routes: new OpenAPIHono().route('/session', sessionAdminRoute),
+ routes: [sessionAdminRoute],
});
-
-export type AdminTypes = typeof adminModule;
diff --git a/packages/vitnode/src/api/modules/admin/routes/session.route.ts b/packages/vitnode/src/api/modules/admin/routes/session.route.ts
index 05fe6caef..7b87fb05d 100644
--- a/packages/vitnode/src/api/modules/admin/routes/session.route.ts
+++ b/packages/vitnode/src/api/modules/admin/routes/session.route.ts
@@ -1,45 +1,49 @@
-import { createApiRoute } from '@/api/lib/route';
+import { buildRoute } from '@/api/lib/route';
import { SessionAdminModel } from '@/api/models/session-admin';
-import { OpenAPIHono } from '@hono/zod-openapi';
import { z } from 'zod';
-const route = createApiRoute({
- method: 'get',
- description: 'Verify admin session',
- plugin: 'core',
- path: '/',
- responses: {
- 200: {
- content: {
- 'application/json': {
- schema: z.object({
- user: z.object({
- id: z.string(),
- email: z.string(),
- name: z.string(),
- name_code: z.string(),
- joined_at: z.date(),
- newsletter: z.boolean(),
- avatar_color: z.string(),
- email_verified: z.boolean(),
- role_id: z.string(),
- birthday: z.date().nullable(),
+export const sessionAdminRoute = buildRoute({
+ route: {
+ method: 'get',
+ description: 'Verify admin session',
+ plugin: 'core',
+ pluginConfig: {
+ id: 'core',
+ name: 'Core',
+ },
+ path: '/session',
+ responses: {
+ 200: {
+ content: {
+ 'application/json': {
+ schema: z.object({
+ user: z.object({
+ id: z.string(),
+ email: z.string(),
+ name: z.string(),
+ name_code: z.string(),
+ joined_at: z.date(),
+ newsletter: z.boolean(),
+ avatar_color: z.string(),
+ email_verified: z.boolean(),
+ role_id: z.string(),
+ birthday: z.date().nullable(),
+ }),
}),
- }),
+ },
},
+ description: 'User',
+ },
+ 403: {
+ description: 'Access Denied',
},
- description: 'User',
- },
- 403: {
- description: 'Access Denied',
},
},
-});
-
-export const sessionAdminRoute = new OpenAPIHono().openapi(route, async c => {
- const user = await new SessionAdminModel(c).verifySession();
+ handler: async c => {
+ const user = await new SessionAdminModel(c).verifySession();
- return c.json({
- user,
- });
+ return c.json({
+ user,
+ });
+ },
});
diff --git a/packages/vitnode/src/api/modules/middleware/middleware.module.ts b/packages/vitnode/src/api/modules/middleware/middleware.module.ts
index cdab70139..1d12c1bbd 100644
--- a/packages/vitnode/src/api/modules/middleware/middleware.module.ts
+++ b/packages/vitnode/src/api/modules/middleware/middleware.module.ts
@@ -1,40 +1,9 @@
-import { createModuleApi } from '@/api/lib/module';
-import { createApiRoute } from '@/api/lib/route';
-import { EmailModel } from '@/api/models/email';
-import { OpenAPIHono, z } from '@hono/zod-openapi';
+import { buildModule } from '@/api/lib/module';
-import { middlewareRoute } from './route';
+import { routeMiddleware } from './route';
-export const middlewareModule = createModuleApi({
- name: 'middleware',
+export const middlewareModule = buildModule({
plugin: 'core',
- routes: new OpenAPIHono().route('/', middlewareRoute).openapi(
- createApiRoute({
- method: 'post',
- plugin: 'core',
- path: '/test',
- responses: {
- 200: {
- content: {
- 'application/json': {
- schema: z.string(),
- },
- },
- description: 'test',
- },
- },
- }),
- async c => {
- const email = new EmailModel(c);
- await email.send({
- to: 'ithereplay@gmail.com',
- subject: 'test',
- html: 'test',
- });
-
- return c.json('Hello, world!');
- },
- ),
+ name: 'middleware',
+ routes: [routeMiddleware],
});
-
-export type MiddlewareTypes = typeof middlewareModule;
diff --git a/packages/vitnode/src/api/modules/middleware/route.ts b/packages/vitnode/src/api/modules/middleware/route.ts
index 6598ef4db..a67467a12 100644
--- a/packages/vitnode/src/api/modules/middleware/route.ts
+++ b/packages/vitnode/src/api/modules/middleware/route.ts
@@ -1,34 +1,34 @@
-import { createApiRoute } from '@/api/lib/route';
+import { buildRoute } from '@/api/lib/route';
import { EmailModel } from '@/api/models/email';
-import { OpenAPIHono } from '@hono/zod-openapi';
import { z } from 'zod';
-const route = createApiRoute({
- method: 'get',
- plugin: 'core',
- description: 'Middleware route with user authentication',
- path: '/',
- responses: {
- 200: {
- content: {
- 'application/json': {
- schema: z.object({
- sso: z.array(z.object({ id: z.string(), name: z.string() })),
- isEmail: z.boolean(),
- }),
+export const routeMiddleware = buildRoute({
+ route: {
+ isAuth: true,
+ path: '/',
+ method: 'get',
+ description: 'Middleware route with user authentication',
+ responses: {
+ 200: {
+ content: {
+ 'application/json': {
+ schema: z.object({
+ sso: z.array(z.object({ id: z.string(), name: z.string() })),
+ isEmail: z.boolean(),
+ }),
+ },
},
+ description: 'Middleware route',
},
- description: 'Middleware route',
},
},
-});
-
-export const middlewareRoute = new OpenAPIHono().openapi(route, c => {
- const sso = c.get('core').authorization.ssoPlugins;
- const email = new EmailModel(c);
+ handler: c => {
+ const sso = c.get('core').authorization.ssoPlugins;
+ const email = new EmailModel(c);
- return c.json({
- isEmail: email.isAvailable(),
- sso: sso.map(s => ({ id: s.id, name: s.name })),
- });
+ return c.json({
+ isEmail: email.isAvailable(),
+ sso: sso.map(s => ({ id: s.id, name: s.name })),
+ });
+ },
});
diff --git a/packages/vitnode/src/api/modules/users/routes/session.route.ts b/packages/vitnode/src/api/modules/users/routes/session.route.ts
index 344f818e8..051a5716d 100644
--- a/packages/vitnode/src/api/modules/users/routes/session.route.ts
+++ b/packages/vitnode/src/api/modules/users/routes/session.route.ts
@@ -1,52 +1,51 @@
-import { createApiRoute } from '@/api/lib/route';
+import { buildRoute } from '@/api/lib/route';
import { SessionModel } from '@/api/models/session';
import { SessionAdminModel } from '@/api/models/session-admin';
-import { OpenAPIHono } from '@hono/zod-openapi';
import { z } from 'zod';
-const route = createApiRoute({
- method: 'get',
- description: 'Verify session',
- plugin: 'core',
- path: '/',
- responses: {
- 200: {
- content: {
- 'application/json': {
- schema: z.object({
- user: z
- .object({
- id: z.string(),
- email: z.string(),
- name: z.string(),
- name_code: z.string(),
- joined_at: z.date(),
- newsletter: z.boolean(),
- avatar_color: z.string(),
- email_verified: z.boolean(),
- role_id: z.string(),
- birthday: z.date().nullable(),
- isAdmin: z.boolean(),
- })
- .nullable(),
- }),
+export const sessionRoute = buildRoute({
+ route: {
+ method: 'get',
+ description: 'Verify session',
+ path: '/session',
+ responses: {
+ 200: {
+ content: {
+ 'application/json': {
+ schema: z.object({
+ user: z
+ .object({
+ id: z.string(),
+ email: z.string(),
+ name: z.string(),
+ name_code: z.string(),
+ joined_at: z.date(),
+ newsletter: z.boolean(),
+ avatar_color: z.string(),
+ email_verified: z.boolean(),
+ role_id: z.string(),
+ birthday: z.date().nullable(),
+ isAdmin: z.boolean(),
+ })
+ .nullable(),
+ }),
+ },
},
+ description: 'User',
},
- description: 'User',
},
},
-});
-
-export const sessionRoute = new OpenAPIHono().openapi(route, async c => {
- const user = await new SessionModel(c).verifySession();
- const admin = new SessionAdminModel(c);
+ handler: async c => {
+ const user = await new SessionModel(c).verifySession();
+ const admin = new SessionAdminModel(c);
- return c.json({
- user: user
- ? {
- ...user,
- isAdmin: await admin.checkIfUserIsAdmin(user.id),
- }
- : null,
- });
+ return c.json({
+ user: user
+ ? {
+ ...user,
+ isAdmin: await admin.checkIfUserIsAdmin(user.id),
+ }
+ : null,
+ });
+ },
});
diff --git a/packages/vitnode/src/api/modules/users/routes/sign-in.route.ts b/packages/vitnode/src/api/modules/users/routes/sign-in.route.ts
index ff352b682..3cafe6474 100644
--- a/packages/vitnode/src/api/modules/users/routes/sign-in.route.ts
+++ b/packages/vitnode/src/api/modules/users/routes/sign-in.route.ts
@@ -1,65 +1,64 @@
-import { createApiRoute } from '@/api/lib/route';
+import { buildRoute } from '@/api/lib/route';
import { SessionModel } from '@/api/models/session';
import { SessionAdminModel } from '@/api/models/session-admin';
import { UserModel } from '@/api/models/user';
-import { OpenAPIHono } from '@hono/zod-openapi';
import { z } from 'zod';
-const route = createApiRoute({
- method: 'post',
- description: 'Sign in with email and password',
- plugin: 'core',
- path: '/',
- request: {
- body: {
- required: true,
- content: {
- 'application/json': {
- schema: z.object({
- email: z.string().email().toLowerCase().openapi({
- example: 'test@test.com',
+export const signInRoute = buildRoute({
+ route: {
+ method: 'post',
+ description: 'Sign in with email and password',
+ path: '/sign_in',
+ request: {
+ body: {
+ required: true,
+ content: {
+ 'application/json': {
+ schema: z.object({
+ email: z.string().email().toLowerCase().openapi({
+ example: 'test@test.com',
+ }),
+ password: z.string().openapi({
+ example: 'Test123!',
+ }),
+ isAdmin: z.boolean().optional().openapi({
+ example: false,
+ }),
}),
- password: z.string().openapi({
- example: 'Test123!',
- }),
- isAdmin: z.boolean().optional().openapi({
- example: false,
- }),
- }),
+ },
},
},
},
- },
- responses: {
- 200: {
- content: {
- 'application/json': {
- schema: z.object({
- id: z.string(),
- token: z.string(),
- }),
+ responses: {
+ 403: {
+ description: 'Access Denied',
+ },
+ 201: {
+ content: {
+ 'application/json': {
+ schema: z.object({
+ id: z.string(),
+ token: z.string(),
+ }),
+ },
},
+ description: 'User signed in',
},
- description: 'User signed in',
- },
- 403: {
- description: 'Access Denied',
},
},
-});
+ handler: async c => {
+ const { password, isAdmin, email } = c.req.valid('json');
+ const data = await new UserModel().signInWithPassword({ password, email });
-export const signInRoute = new OpenAPIHono().openapi(route, async c => {
- const { password, isAdmin, email } = c.req.valid('json');
- const data = await new UserModel().signInWithPassword({ password, email });
+ if (isAdmin) {
+ const { token } = await new SessionAdminModel(c).createSessionByUserId(
+ data.id,
+ );
- if (isAdmin) {
- const { token } = await new SessionAdminModel(c).createSessionByUserId(
- data.id,
- );
+ return c.json({ id: data.id, token }, 201);
+ }
+ const { token } = await new SessionModel(c).createSessionByUserId(data.id);
- return c.json({ id: data.id, token });
- }
- const { token } = await new SessionModel(c).createSessionByUserId(data.id);
-
- return c.json({ id: data.id, token });
+ return c.json({ id: data.id, token }, 201);
+ },
});
diff --git a/packages/vitnode/src/api/modules/users/routes/sign-out.route.ts b/packages/vitnode/src/api/modules/users/routes/sign-out.route.ts
index 7dfbdba53..a089491c8 100644
--- a/packages/vitnode/src/api/modules/users/routes/sign-out.route.ts
+++ b/packages/vitnode/src/api/modules/users/routes/sign-out.route.ts
@@ -1,44 +1,44 @@
-import { createApiRoute } from '@/api/lib/route';
+import { buildRoute } from '@/api/lib/route';
import { SessionModel } from '@/api/models/session';
import { SessionAdminModel } from '@/api/models/session-admin';
-import { OpenAPIHono, z } from '@hono/zod-openapi';
+import { z } from '@hono/zod-openapi';
-const route = createApiRoute({
- method: 'delete',
- description: 'Sign out the current admin',
- plugin: 'core',
- path: '/',
- request: {
- body: {
- content: {
- 'application/json': {
- schema: z.object({
- isAdmin: z.boolean().optional().openapi({
- example: false,
+export const signOutRoute = buildRoute({
+ route: {
+ method: 'delete',
+ description: 'Sign out the current admin',
+ path: '/sign_out',
+ request: {
+ body: {
+ content: {
+ 'application/json': {
+ schema: z.object({
+ isAdmin: z.boolean().optional().openapi({
+ example: false,
+ }),
}),
- }),
+ },
},
},
},
- },
- responses: {
- 200: {
- description: 'User signed out',
- },
- 403: {
- description: 'Access Denied',
+ responses: {
+ 200: {
+ description: 'User signed out',
+ },
+ 403: {
+ description: 'Access Denied',
+ },
},
},
-});
+ handler: async c => {
+ const { isAdmin } = c.req.valid('json');
+ if (isAdmin) {
+ await new SessionAdminModel(c).deleteSession();
-export const signOutRoute = new OpenAPIHono().openapi(route, async c => {
- const { isAdmin } = c.req.valid('json');
- if (isAdmin) {
- await new SessionAdminModel(c).deleteSession();
+ return c.json({});
+ }
+ await new SessionModel(c).deleteSession();
return c.json({});
- }
- await new SessionModel(c).deleteSession();
-
- return c.json({});
+ },
});
diff --git a/packages/vitnode/src/api/modules/users/routes/sign-up.route.ts b/packages/vitnode/src/api/modules/users/routes/sign-up.route.ts
index e5f0adbe1..0569a2992 100644
--- a/packages/vitnode/src/api/modules/users/routes/sign-up.route.ts
+++ b/packages/vitnode/src/api/modules/users/routes/sign-up.route.ts
@@ -1,63 +1,62 @@
-import { createApiRoute } from '@/api/lib/route';
+import { buildRoute } from '@/api/lib/route';
import { PasswordModel } from '@/api/models/password';
import { UserModel } from '@/api/models/user';
-import { OpenAPIHono } from '@hono/zod-openapi';
import { z } from 'zod';
const nameRegex = /^(?!.* {2})[\p{L}\p{N}._@ -]*$/u;
-const route = createApiRoute({
- method: 'post',
- description: 'Create a new user',
- plugin: 'core',
- path: '/',
- request: {
- body: {
- required: true,
- content: {
- 'application/json': {
- schema: z.object({
- email: z.string().email().toLowerCase().openapi({
- example: 'test@test.com',
- }),
- name: z
- .string()
- .openapi({ example: 'test' })
- .min(3)
- .refine(val => nameRegex.test(val), {
- message: 'Invalid name',
+export const signUpRoute = buildRoute({
+ route: {
+ method: 'post',
+ description: 'Create a new user',
+ path: '/sign_up',
+ request: {
+ body: {
+ required: true,
+ content: {
+ 'application/json': {
+ schema: z.object({
+ email: z.string().email().toLowerCase().openapi({
+ example: 'test@test.com',
+ }),
+ name: z
+ .string()
+ .openapi({ example: 'test' })
+ .min(3)
+ .refine(val => nameRegex.test(val), {
+ message: 'Invalid name',
+ }),
+ password: z.string().min(8).openapi({
+ example: 'Test123!',
}),
- password: z.string().min(8).openapi({
- example: 'Test123!',
+ newsletter: z.boolean().default(false).optional(),
}),
- newsletter: z.boolean().default(false).optional(),
- }),
+ },
},
},
},
- },
- responses: {
- 200: {
- content: {
- 'application/json': {
- schema: z.object({
- id: z.string(),
- }),
+ responses: {
+ 200: {
+ content: {
+ 'application/json': {
+ schema: z.object({
+ id: z.string(),
+ }),
+ },
},
+ description: 'User created',
},
- description: 'User created',
},
},
-});
-
-export const signUpRoute = new OpenAPIHono().openapi(route, async c => {
- const hashedPassword = await new PasswordModel().encryptPassword(
- c.req.valid('json').password,
- );
- const data = await new UserModel().signUp(
- { ...c.req.valid('json'), hashedPassword },
- c.req,
- );
+ handler: async c => {
+ const hashedPassword = await new PasswordModel().encryptPassword(
+ c.req.valid('json').password,
+ );
+ const data = await new UserModel().signUp(
+ { ...c.req.valid('json'), hashedPassword },
+ c.req,
+ );
- return c.json({ id: data.id });
+ return c.json({ id: data.id });
+ },
});
diff --git a/packages/vitnode/src/api/modules/users/routes/test.route.ts b/packages/vitnode/src/api/modules/users/routes/test.route.ts
new file mode 100644
index 000000000..e438ab11b
--- /dev/null
+++ b/packages/vitnode/src/api/modules/users/routes/test.route.ts
@@ -0,0 +1,31 @@
+import { buildRoute } from '@/api/lib/route';
+import { z } from 'zod';
+
+export const testRoute = buildRoute({
+ route: {
+ method: 'get',
+ description: 'Test route',
+ path: '/test',
+ responses: {
+ 200: {
+ content: {
+ 'text/plain': {
+ schema: z.string(),
+ },
+ },
+ description: 'User',
+ },
+ 201: {
+ content: {
+ 'text/plain': {
+ schema: z.string(),
+ },
+ },
+ description: 'User',
+ },
+ },
+ },
+ handler: c => {
+ return c.text('test');
+ },
+});
diff --git a/packages/vitnode/src/api/modules/users/sso/routes/callback.route.ts b/packages/vitnode/src/api/modules/users/sso/routes/callback.route.ts
index d9d5a2b56..cf9a3b01a 100644
--- a/packages/vitnode/src/api/modules/users/sso/routes/callback.route.ts
+++ b/packages/vitnode/src/api/modules/users/sso/routes/callback.route.ts
@@ -1,43 +1,44 @@
-import { createApiRoute } from '@/api/lib/route';
+import { buildRoute } from '@/api/lib/route';
import { SessionModel } from '@/api/models/session';
import { SSOModel } from '@/api/models/sso';
-import { OpenAPIHono } from '@hono/zod-openapi';
import { z } from 'zod';
-const route = createApiRoute({
- method: 'get',
- description: 'SSO Callback',
- plugin: 'core',
- path: '/{providerId}/callback',
- request: {
- params: z.object({
- providerId: z.string(),
- }),
- query: z.object({
- code: z.string(),
- state: z.string(),
- }),
- },
- responses: {
- 200: {
- content: {
- 'application/json': {
- schema: z.object({
- id: z.string(),
- token: z.string(),
- }),
+export const callbackRoute = buildRoute({
+ route: {
+ method: 'get',
+ description: 'SSO Callback',
+ path: '/{providerId}/callback',
+ request: {
+ params: z.object({
+ providerId: z.string(),
+ }),
+ query: z.object({
+ code: z.string(),
+ state: z.string(),
+ }),
+ },
+ responses: {
+ 200: {
+ content: {
+ 'application/json': {
+ schema: z.object({
+ id: z.string(),
+ token: z.string(),
+ }),
+ },
},
+ description: 'URL',
},
- description: 'URL',
},
},
-});
+ handler: async c => {
+ const { providerId } = c.req.valid('param');
+ const { code, state } = c.req.valid('query');
+ const sso = await new SSOModel(c).callback({ providerId, code, state });
+ const { token } = await new SessionModel(c).createSessionByUserId(
+ sso.userId,
+ );
-export const callbackRoute = new OpenAPIHono().openapi(route, async c => {
- const { providerId } = c.req.valid('param');
- const { code, state } = c.req.valid('query');
- const sso = await new SSOModel(c).callback({ providerId, code, state });
- const { token } = await new SessionModel(c).createSessionByUserId(sso.userId);
-
- return c.json({ id: sso.userId, token });
+ return c.json({ id: sso.userId, token });
+ },
});
diff --git a/packages/vitnode/src/api/modules/users/sso/routes/create-url.route.ts b/packages/vitnode/src/api/modules/users/sso/routes/create-url.route.ts
index d2502040f..9ef6805b1 100644
--- a/packages/vitnode/src/api/modules/users/sso/routes/create-url.route.ts
+++ b/packages/vitnode/src/api/modules/users/sso/routes/create-url.route.ts
@@ -1,35 +1,34 @@
-import { createApiRoute } from '@/api/lib/route';
+import { buildRoute } from '@/api/lib/route';
import { SSOModel } from '@/api/models/sso';
-import { OpenAPIHono } from '@hono/zod-openapi';
import { z } from 'zod';
-const route = createApiRoute({
- method: 'post',
- description: 'Generate SSO URL',
- plugin: 'core',
- path: '/{providerId}',
- request: {
- params: z.object({
- providerId: z.string(),
- }),
- },
- responses: {
- 200: {
- content: {
- 'application/json': {
- schema: z.object({ url: z.string() }),
+export const createUrlRoute = buildRoute({
+ route: {
+ method: 'post',
+ description: 'Generate SSO URL',
+ path: '/{providerId}',
+ request: {
+ params: z.object({
+ providerId: z.string(),
+ }),
+ },
+ responses: {
+ 200: {
+ content: {
+ 'application/json': {
+ schema: z.object({ url: z.string() }),
+ },
},
+ description: 'URL',
},
- description: 'URL',
},
},
-});
+ handler: async c => {
+ const { providerId } = c.req.valid('param');
+ const url = await new SSOModel(c).getUrl(providerId);
-export const createUrlRoute = new OpenAPIHono().openapi(route, async c => {
- const { providerId } = c.req.valid('param');
- const url = await new SSOModel(c).getUrl(providerId);
-
- return c.json({
- url,
- });
+ return c.json({
+ url,
+ });
+ },
});
diff --git a/packages/vitnode/src/api/modules/users/sso/sso.module.ts b/packages/vitnode/src/api/modules/users/sso/sso.module.ts
index 862e174d6..f80c2f59a 100644
--- a/packages/vitnode/src/api/modules/users/sso/sso.module.ts
+++ b/packages/vitnode/src/api/modules/users/sso/sso.module.ts
@@ -1,13 +1,10 @@
-import { createModuleApi } from '@/api/lib/module';
-import { OpenAPIHono } from '@hono/zod-openapi';
+import { buildModule } from '@/api/lib/module';
import { callbackRoute } from './routes/callback.route';
import { createUrlRoute } from './routes/create-url.route';
-export const ssoUserModule = createModuleApi({
+export const ssoUserModule = buildModule({
name: 'sso',
plugin: 'core',
- routes: new OpenAPIHono()
- .route('/', createUrlRoute)
- .route('/', callbackRoute),
+ routes: [callbackRoute, createUrlRoute],
});
diff --git a/packages/vitnode/src/api/modules/users/users.module.ts b/packages/vitnode/src/api/modules/users/users.module.ts
index ab7831528..f4a718915 100644
--- a/packages/vitnode/src/api/modules/users/users.module.ts
+++ b/packages/vitnode/src/api/modules/users/users.module.ts
@@ -1,21 +1,15 @@
-import { createModuleApi } from '@/api/lib/module';
-import { OpenAPIHono } from '@hono/zod-openapi';
+import { buildModule } from '@/api/lib/module';
import { sessionRoute } from './routes/session.route';
import { signInRoute } from './routes/sign-in.route';
import { signOutRoute } from './routes/sign-out.route';
import { signUpRoute } from './routes/sign-up.route';
+import { testRoute } from './routes/test.route';
import { ssoUserModule } from './sso/sso.module';
-export const usersModule = createModuleApi({
- name: 'users',
+export const usersModule = buildModule({
plugin: 'core',
- routes: new OpenAPIHono()
- .route('/sign_up', signUpRoute)
- .route('/sign_in', signInRoute)
- .route('/session', sessionRoute)
- .route('/sign_out', signOutRoute)
- .route('/sso', ssoUserModule.app),
+ name: 'users',
+ routes: [sessionRoute, signInRoute, signOutRoute, signUpRoute, testRoute],
+ modules: [ssoUserModule],
});
-
-export type UsersTypes = typeof usersModule;
diff --git a/packages/vitnode/src/api/plugin.ts b/packages/vitnode/src/api/plugin.ts
index f41b2244a..7e4fc30b8 100644
--- a/packages/vitnode/src/api/plugin.ts
+++ b/packages/vitnode/src/api/plugin.ts
@@ -1,9 +1,9 @@
-import { createPluginApi } from './lib/plugin';
+import { buildPlugin } from '../lib/plugin';
import { adminModule } from './modules/admin/admin.module';
import { middlewareModule } from './modules/middleware/middleware.module';
import { usersModule } from './modules/users/users.module';
-export default createPluginApi({
+export const newBuildPluginCore = buildPlugin({
name: 'core',
- modules: [usersModule, middlewareModule, adminModule],
+ modules: [middlewareModule, usersModule, adminModule],
});
diff --git a/packages/vitnode/src/components/table/data-table.tsx b/packages/vitnode/src/components/table/data-table.tsx
index 80de6a214..49c93f1eb 100644
--- a/packages/vitnode/src/components/table/data-table.tsx
+++ b/packages/vitnode/src/components/table/data-table.tsx
@@ -24,42 +24,46 @@ export function DataTable({
data: T[];
}) {
return (
-
-
-
- {columns.map(column => (
- {column.label}
- ))}
-
-
+
+
+
+
+
+ {columns.map(column => (
+ {column.label}
+ ))}
+
+
-
- {data.length ? (
- data.map(row => (
-
- {columns.map(column => {
- const content =
- column.cell?.({
- allData: data,
- row,
- }) ?? String(row[column.id]);
+
+ {data.length ? (
+ data.map(row => (
+
+ {columns.map(column => {
+ const content =
+ column.cell?.({
+ allData: data,
+ row,
+ }) ?? String(row[column.id]);
- return (
-
- {content}
-
- );
- })}
-
- ))
- ) : (
-
-
- Not Found
-
-
- )}
-
-
+ return (
+
+ {content}
+
+ );
+ })}
+
+ ))
+ ) : (
+
+
+ Not Found
+
+
+ )}
+
+
+
+
);
}
diff --git a/packages/vitnode/src/components/ui/table.tsx b/packages/vitnode/src/components/ui/table.tsx
index 83f366f2b..b6691a2ec 100644
--- a/packages/vitnode/src/components/ui/table.tsx
+++ b/packages/vitnode/src/components/ui/table.tsx
@@ -68,7 +68,7 @@ function TableHead({ className, ...props }: React.ComponentProps<'th'>) {
return (
[role=checkbox]]:translate-y-[2px]',
+ 'text-muted-foreground h-10 whitespace-nowrap px-2 text-left align-middle font-medium [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
className,
)}
data-slot="table-head"
diff --git a/packages/vitnode/src/database/schema/admins.ts b/packages/vitnode/src/database/schema/admins.ts
index 259946bdb..f40a399ed 100644
--- a/packages/vitnode/src/database/schema/admins.ts
+++ b/packages/vitnode/src/database/schema/admins.ts
@@ -29,7 +29,7 @@ export const core_admin_permissions = pgTable(
index('core_admin_permissions_role_id_idx').on(t.role_id),
index('core_admin_permissions_user_id_idx').on(t.user_id),
],
-);
+).enableRLS();
export const core_admin_permissions_relations = relations(
core_admin_permissions,
@@ -69,7 +69,7 @@ export const core_admin_sessions = pgTable(
index('core_admin_sessions_token_idx').on(t.token),
index('core_admin_sessions_user_id_idx').on(t.user_id),
],
-);
+).enableRLS();
export const core_admin_sessions_relations = relations(
core_admin_sessions,
diff --git a/packages/vitnode/src/database/schema/config.ts b/packages/vitnode/src/database/schema/config.ts
index 5457aa77d..c795acac8 100644
--- a/packages/vitnode/src/database/schema/config.ts
+++ b/packages/vitnode/src/database/schema/config.ts
@@ -30,4 +30,4 @@ export const core_config = pgTable('core_config', t => ({
.timestamp()
.notNull()
.$onUpdate(() => new Date()),
-}));
+})).enableRLS();
diff --git a/packages/vitnode/src/database/schema/languages.ts b/packages/vitnode/src/database/schema/languages.ts
index 339798b3a..e8b2b3907 100644
--- a/packages/vitnode/src/database/schema/languages.ts
+++ b/packages/vitnode/src/database/schema/languages.ts
@@ -23,7 +23,7 @@ export const core_languages = pgTable(
index('core_languages_code_idx').on(t.code),
index('core_languages_name_idx').on(t.name),
],
-);
+).enableRLS();
export const core_languages_words = pgTable(
'core_languages_words',
@@ -42,7 +42,7 @@ export const core_languages_words = pgTable(
variable: t.varchar({ length: 255 }).notNull(),
}),
t => [index('core_languages_words_lang_code_idx').on(t.language_code)],
-);
+).enableRLS();
export const core_languages_words_relations = relations(
core_languages_words,
diff --git a/packages/vitnode/src/database/schema/moderators.ts b/packages/vitnode/src/database/schema/moderators.ts
index 64271c324..690d6f782 100644
--- a/packages/vitnode/src/database/schema/moderators.ts
+++ b/packages/vitnode/src/database/schema/moderators.ts
@@ -25,7 +25,7 @@ export const core_moderators_permissions = pgTable(
index('core_moderators_permissions_role_id_idx').on(t.role_id),
index('core_moderators_permissions_user_id_idx').on(t.user_id),
],
-);
+).enableRLS();
export const core_moderators_permissions_relations = relations(
core_moderators_permissions,
diff --git a/packages/vitnode/src/database/schema/roles.ts b/packages/vitnode/src/database/schema/roles.ts
index 06939e904..a7be33623 100644
--- a/packages/vitnode/src/database/schema/roles.ts
+++ b/packages/vitnode/src/database/schema/roles.ts
@@ -15,4 +15,4 @@ export const core_roles = pgTable('core_roles', t => ({
files_allow_upload: t.boolean().notNull().default(true),
files_total_max_storage: t.integer().notNull().default(500000),
files_max_storage_for_submit: t.integer().notNull().default(5000),
-}));
+})).enableRLS();
diff --git a/packages/vitnode/src/database/schema/sessions.ts b/packages/vitnode/src/database/schema/sessions.ts
index 75dc9876a..c1bd1c660 100644
--- a/packages/vitnode/src/database/schema/sessions.ts
+++ b/packages/vitnode/src/database/schema/sessions.ts
@@ -23,7 +23,7 @@ export const core_sessions = pgTable(
.notNull(),
}),
t => [index('core_sessions_user_id_idx').on(t.user_id)],
-);
+).enableRLS();
export const core_sessions_relations = relations(core_sessions, ({ one }) => ({
user: one(core_users, {
@@ -45,7 +45,7 @@ export const core_sessions_known_devices = pgTable(
last_seen: t.timestamp().notNull().defaultNow(),
}),
t => [index('core_sessions_known_devices_ip_address_idx').on(t.ip_address)],
-);
+).enableRLS();
export const core_sessions_known_devices_relations = relations(
core_sessions_known_devices,
diff --git a/packages/vitnode/src/database/schema/users.ts b/packages/vitnode/src/database/schema/users.ts
index 5e91dc31d..87822373b 100644
--- a/packages/vitnode/src/database/schema/users.ts
+++ b/packages/vitnode/src/database/schema/users.ts
@@ -35,7 +35,7 @@ export const core_users = pgTable(
index('core_users_name_idx').on(t.name),
index('core_users_email_idx').on(t.email),
],
-);
+).enableRLS();
export const core_users_relations = relations(core_users, ({ one, many }) => ({
group: one(core_roles, {
@@ -75,7 +75,7 @@ export const core_users_sso = pgTable(
.$onUpdate(() => new Date()),
}),
t => [index('core_users_sso_user_id_idx').on(t.user_id)],
-);
+).enableRLS();
export const core_users_sso_relations = relations(
core_users_sso,
@@ -101,7 +101,7 @@ export const core_users_confirm_emails = pgTable(
created_at: t.timestamp().notNull().defaultNow(),
expires: t.timestamp().notNull(),
}),
-);
+).enableRLS();
export const core_users_confirm_emails_relations = relations(
core_users_confirm_emails,
@@ -129,7 +129,7 @@ export const core_users_forgot_password = pgTable(
created_at: t.timestamp().notNull().defaultNow(),
expires_at: t.timestamp().notNull(),
}),
-);
+).enableRLS();
export const core_users_forgot_password_relations = relations(
core_users_forgot_password,
diff --git a/packages/vitnode/src/lib/api/get-middleware-api.ts b/packages/vitnode/src/lib/api/get-middleware-api.ts
index 7b430da7c..821db5568 100644
--- a/packages/vitnode/src/lib/api/get-middleware-api.ts
+++ b/packages/vitnode/src/lib/api/get-middleware-api.ts
@@ -1,17 +1,12 @@
-import { MiddlewareTypes } from '@/api/modules/middleware/middleware.module';
-
-import { fetcher } from '../fetcher';
+import { middlewareModule } from '@/api/modules/middleware/middleware.module';
+import { fetcher } from '@/lib/fetcher';
export const getMiddlewareApi = async () => {
- const client = await fetcher({
- plugin: 'core',
+ const res = await fetcher(middlewareModule, {
+ path: '/',
+ method: 'get',
module: 'middleware',
- options: {
- cache: 'force-cache',
- },
});
- const res = await client.index.$get();
-
return await res.json();
};
diff --git a/packages/vitnode/src/lib/api/get-session-admin-api.ts b/packages/vitnode/src/lib/api/get-session-admin-api.ts
index efe3f554e..4496571aa 100644
--- a/packages/vitnode/src/lib/api/get-session-admin-api.ts
+++ b/packages/vitnode/src/lib/api/get-session-admin-api.ts
@@ -1,18 +1,15 @@
-import { AdminTypes } from '@/api/modules/admin/admin.module';
+import { adminModule } from '@/api/modules/admin/admin.module';
+import { fetcher } from '@/lib/fetcher';
-import { fetcher } from '../fetcher';
import { redirect } from '../navigation';
export const getSessionAdminApi = async () => {
- const client = await fetcher({
- plugin: 'core',
+ const res = await fetcher(adminModule, {
+ path: '/session',
+ method: 'get',
module: 'admin',
- options: {
- cache: 'force-cache',
- },
});
- const res = await client.session.$get();
if (res.status !== 200) {
await redirect('/admin');
diff --git a/packages/vitnode/src/lib/api/get-session-api.ts b/packages/vitnode/src/lib/api/get-session-api.ts
index 3d7a19a8c..27b04b182 100644
--- a/packages/vitnode/src/lib/api/get-session-api.ts
+++ b/packages/vitnode/src/lib/api/get-session-api.ts
@@ -1,18 +1,13 @@
-import { UsersTypes } from '@/api/modules/users/users.module';
-
-import { fetcher } from '../fetcher';
+import { usersModule } from '@/api/modules/users/users.module';
+import { fetcher } from '@/lib/fetcher';
export const getSessionApi = async () => {
- const client = await fetcher({
- plugin: 'core',
+ const res = await fetcher(usersModule, {
+ path: '/session',
+ method: 'get',
module: 'users',
- options: {
- cache: 'force-cache',
- },
});
- const res = await client.session.$get();
-
return await res.json();
};
diff --git a/packages/vitnode/src/lib/fetcher-client.ts b/packages/vitnode/src/lib/fetcher-client.ts
deleted file mode 100644
index 487b9aa51..000000000
--- a/packages/vitnode/src/lib/fetcher-client.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { ModuleApi } from '@/api/lib/module';
-import { Env, Schema } from 'hono';
-import { hc } from 'hono/client';
-import { UnionToIntersection } from 'hono/utils/types';
-
-import { CONFIG } from './config';
-import { Client } from './fetcher';
-
-export function fetcherClient<
- T extends ModuleApi,
->({
- plugin,
- module,
- options,
-}: {
- module: T['name'];
- options?: Omit;
- plugin: T['plugin'];
-}): UnionToIntersection> {
- const url = new URL(`/api/${plugin}/${module}`, CONFIG.backend.origin);
-
- const client = hc(url.href, {
- fetch: async (input, requestInit) => {
- return fetch(input, {
- ...requestInit,
- ...options,
- });
- },
- });
-
- return client as unknown as UnionToIntersection>;
-}
diff --git a/packages/vitnode/src/lib/fetcher.ts b/packages/vitnode/src/lib/fetcher.ts
deleted file mode 100644
index e5ef3a8c9..000000000
--- a/packages/vitnode/src/lib/fetcher.ts
+++ /dev/null
@@ -1,126 +0,0 @@
-import { ModuleApi } from '@/api/lib/module';
-import { ClientRequest, ClientResponse, hc } from 'hono/client';
-import { HonoBase } from 'hono/hono-base';
-import { Env, ResponseFormat, Schema } from 'hono/types';
-import { StatusCode } from 'hono/utils/http-status';
-import { UnionToIntersection } from 'hono/utils/types';
-import { cookies, headers } from 'next/headers';
-
-import { CONFIG } from './config';
-import { cookieFromStringToObject } from './cookie-from-string-to-object';
-
-type PathToChain<
- Path extends string,
- E extends Schema,
- Original extends string = Path,
-> = Path extends `/${infer P}`
- ? PathToChain
- : Path extends `${infer P}/${infer R}`
- ? {
- [K in P]: PathToChain;
- }
- : Record<
- Path extends '' ? 'index' : Path,
- ClientRequest ? E[Original] : never>
- >;
-
-export type Client =
- T extends HonoBase
- ? S extends Record
- ? K extends string
- ? PathToChain
- : never
- : never
- : never;
-
-export async function fetcher<
- T extends ModuleApi,
->({
- plugin,
- module,
- options,
-}: {
- module: T['name'];
- options?: Omit;
- plugin: T['plugin'];
-}): Promise>> {
- const url = new URL(`/api/${plugin}/${module}`, CONFIG.backend.origin);
- const [nextInternalHeaders, cookie] = await Promise.all([
- headers(),
- cookies(),
- ]);
-
- const client = hc(url.href, {
- fetch: async (input, requestInit) => {
- const headers = new Headers({
- 'Content-Type': 'application/json',
- Cookie: cookie.toString(),
- ['user-agent']: nextInternalHeaders.get('user-agent') ?? 'node',
- ['x-forwarded-for']:
- nextInternalHeaders.get('x-forwarded-for') ?? '0.0.0.0',
- // 'x-vitnode-user-language': cookie.get('NEXT_LOCALE')?.value ?? 'en',
- ...options?.headers,
- });
-
- return await fetch(input, {
- ...requestInit,
- ...options,
- headers,
- });
- },
- });
-
- return client as unknown as UnionToIntersection>;
-}
-
-type SchemaOf> =
- T extends ModuleApi ? S : never;
-
-type MethodsForEndpoint<
- E extends Env,
- T extends ModuleApi,
- P extends keyof SchemaOf,
-> = {
- [K in Extract[P], `$${string}`>]: K extends `$${infer U}`
- ? Lowercase
- : never;
-}[Extract[P], `$${string}`>];
-
-export type FetcherInput<
- T extends ModuleApi,
- P extends keyof SchemaOf,
- M extends MethodsForEndpoint,
- E extends Env = Env,
-> = {
- [K in Extract[P], `$${string}`>]: Lowercase<
- K extends `$${infer U}` ? U : never
- > extends M
- ? SchemaOf[P][K] extends { input: infer I }
- ? I
- : never
- : never;
-}[Extract[P], `$${string}`>];
-
-export async function handleSetCookiesFetcher<
- T,
- U extends number = StatusCode,
- F extends ResponseFormat = string,
->(res: ClientResponse) {
- await Promise.all(
- cookieFromStringToObject(res.headers.getSetCookie()).map(async cookie => {
- const key = Object.keys(cookie)[0];
- const value = Object.values(cookie)[0];
-
- if (typeof value !== 'string' || typeof key !== 'string') return;
-
- (await cookies()).set(key, value, {
- domain: cookie.Domain,
- path: cookie.Path,
- expires: new Date(cookie.Expires),
- secure: cookie.Secure,
- httpOnly: cookie.HttpOnly,
- sameSite: cookie.SameSite,
- });
- }),
- );
-}
diff --git a/packages/vitnode/src/lib/fetcher/index.ts b/packages/vitnode/src/lib/fetcher/index.ts
new file mode 100644
index 000000000..361a808a4
--- /dev/null
+++ b/packages/vitnode/src/lib/fetcher/index.ts
@@ -0,0 +1,134 @@
+import { BaseBuildModuleReturn, BuildModuleReturn } from '@/api/lib/module';
+import { Route } from '@/api/lib/route';
+import { cookies, headers } from 'next/headers';
+
+import { CONFIG } from '../config';
+import { cookieFromStringToObject } from '../cookie-from-string-to-object';
+import {
+ FetcherParams,
+ GetModulePaths,
+ GetValidPathsForModule,
+ InferResponseType,
+} from '../fetcher/types';
+
+const handleSetCookiesFetcher = async (res: Response) => {
+ await Promise.all(
+ cookieFromStringToObject(res.headers.getSetCookie()).map(async cookie => {
+ const key = Object.keys(cookie)[0];
+ const value = Object.values(cookie)[0];
+
+ if (typeof value !== 'string' || typeof key !== 'string') return;
+
+ (await cookies()).set(key, value, {
+ domain: cookie.Domain,
+ path: cookie.Path,
+ expires: new Date(cookie.Expires),
+ secure: cookie.Secure,
+ httpOnly: cookie.HttpOnly,
+ sameSite: cookie.SameSite,
+ });
+ }),
+ );
+};
+
+const buildSearchParams = (query: Record) => {
+ const searchParams = new URLSearchParams();
+
+ for (const [k, v] of Object.entries(query)) {
+ if (v === undefined) {
+ continue;
+ }
+
+ if (Array.isArray(v)) {
+ for (const v2 of v) {
+ searchParams.append(k, v2);
+ }
+ } else {
+ searchParams.set(k, v);
+ }
+ }
+
+ return searchParams;
+};
+
+export async function fetcher<
+ M extends string,
+ Routes extends Route[],
+ Modules extends BaseBuildModuleReturn[],
+ ModuleName extends GetModulePaths,
+ SelectedPath extends GetValidPathsForModule,
+>(
+ { plugin }: BuildModuleReturn,
+ {
+ path,
+ method,
+ module,
+ args,
+ allowSaveCookies = false,
+ }: FetcherParams & {
+ allowSaveCookies?: boolean;
+ options?: Omit;
+ },
+): Promise> {
+ let currentPath: string = path;
+
+ // Replace path parameters
+ if (args && 'params' in args && args.params) {
+ for (const [key, value] of Object.entries(args.params)) {
+ currentPath = currentPath.replaceAll(`{${key}}`, String(value));
+ }
+ }
+
+ // Ensure path starts with a slash
+ const formattedPath = currentPath.startsWith('/')
+ ? currentPath
+ : `/${currentPath}`;
+
+ // Construct the base URL
+ const url = new URL(
+ `/api/${plugin}/${module}${formattedPath}`,
+ CONFIG.backend.origin,
+ );
+
+ // Add query parameters if they exist
+ if (args && 'query' in args && args.query) {
+ const searchParams = buildSearchParams(
+ args.query as Record,
+ );
+ url.search = searchParams.toString();
+ }
+
+ const [nextInternalHeaders, cookie] = await Promise.all([
+ headers(),
+ cookies(),
+ ]);
+
+ const response = await fetch(url, {
+ method: method.toUpperCase(),
+ headers: new Headers({
+ 'Content-Type': 'application/json',
+ Cookie: cookie.toString(),
+ ['user-agent']: nextInternalHeaders.get('user-agent') ?? 'node',
+ ['x-forwarded-for']:
+ nextInternalHeaders.get('x-forwarded-for') ?? '0.0.0.0',
+ }),
+ body: args && 'body' in args ? JSON.stringify(args.body) : undefined,
+ });
+
+ if (
+ response.status >= 200 &&
+ response.status < 300 &&
+ allowSaveCookies &&
+ method !== 'get'
+ ) {
+ await handleSetCookiesFetcher(response);
+ }
+
+ return response as InferResponseType<
+ M,
+ Routes,
+ Modules,
+ ModuleName,
+ SelectedPath
+ >;
+}
diff --git a/packages/vitnode/src/lib/fetcher/types.ts b/packages/vitnode/src/lib/fetcher/types.ts
new file mode 100644
index 000000000..7bd6d6bbf
--- /dev/null
+++ b/packages/vitnode/src/lib/fetcher/types.ts
@@ -0,0 +1,241 @@
+import { BaseBuildModuleReturn } from '@/api/lib/module';
+import { Route } from '@/api/lib/route';
+import { RouteConfig } from '@hono/zod-openapi';
+import { ResponseFormat } from 'hono/types';
+import { StatusCode, SuccessStatusCode } from 'hono/utils/http-status';
+import { z } from 'zod';
+
+interface ClientResponse<
+ T,
+ U extends number = StatusCode,
+ F extends ResponseFormat = ResponseFormat,
+> extends globalThis.Response {
+ arrayBuffer: () => Promise;
+ blob: () => Promise;
+ readonly body: null | ReadableStream;
+ readonly bodyUsed: boolean;
+ clone: () => Response;
+ formData: () => Promise;
+ headers: Headers;
+ json: () => F extends 'text/html' | 'text/plain'
+ ? Promise
+ : F extends 'application/json'
+ ? Promise
+ : Promise;
+ ok: U extends SuccessStatusCode
+ ? true
+ : U extends Exclude
+ ? false
+ : boolean;
+ redirect: (url: string, status: number) => Response;
+ status: U;
+ statusText: string;
+ text: () => F extends 'text/html' | 'text/plain'
+ ? T extends string
+ ? Promise
+ : Promise
+ : Promise;
+ url: string;
+}
+
+interface RouteShape {
+ readonly route: {
+ readonly method: string;
+ readonly path: string;
+ };
+}
+
+interface ModuleSpec {
+ readonly modules?: readonly ModuleSpec[];
+ readonly name: string;
+ readonly routes: readonly RouteShape[];
+}
+
+type SplitPath = S extends `${infer First}/${infer Rest}`
+ ? [First, ...SplitPath]
+ : S extends ''
+ ? []
+ : [S];
+
+type FindModuleNested<
+ M extends { modules?: readonly ModuleSpec[] },
+ Path extends string[],
+> = Path extends [infer First extends string, ...infer Rest extends string[]]
+ ? Extract[number] extends infer SubModule
+ ? SubModule extends ModuleSpec & { name: First }
+ ? Rest['length'] extends 0
+ ? SubModule
+ : FindModuleNested
+ : never
+ : never
+ : M;
+
+export type GetModulePaths<
+ MainModule extends string,
+ Modules extends readonly ModuleSpec[],
+> =
+ | `${MainModule}/${Modules[number]['name']}/${Extract<
+ Modules[number]['modules'],
+ readonly ModuleSpec[]
+ >[number]['name']}`
+ | `${MainModule}/${Modules[number]['name']}`
+ | MainModule;
+
+type GetTargetModule<
+ ModulePath extends string,
+ MainModuleName extends string,
+ MainRoutes extends readonly RouteShape[],
+ SubModules extends readonly ModuleSpec[],
+> = ModulePath extends MainModuleName
+ ? { modules: SubModules; name: MainModuleName; routes: MainRoutes }
+ : ModulePath extends `${MainModuleName}/${infer Rest}`
+ ? SplitPath extends infer PathArray extends string[]
+ ? PathArray['length'] extends 0
+ ? never
+ : FindModuleNested<{ modules: SubModules }, PathArray>
+ : never
+ : never;
+
+type ExtractPaths =
+ M['routes'][number]['route']['path'];
+
+type ExtractMethodForPath<
+ M extends { routes: readonly RouteShape[] },
+ P extends string,
+> = Extract['route']['method'];
+
+type ExtractZodType = T extends z.ZodTypeAny ? z.infer : never;
+
+type InferInputType<
+ RouteCfg extends RouteConfig,
+ Part extends 'body' | 'params' | 'query',
+> = Part extends 'body'
+ ? RouteCfg extends {
+ request: {
+ body: { content: { 'application/json': { schema: infer S } } };
+ };
+ }
+ ? ExtractZodType
+ : RouteCfg extends { request: { body: { schema?: infer S } } }
+ ? ExtractZodType
+ : undefined
+ : Part extends 'query'
+ ? RouteCfg extends { request: { query: infer S } }
+ ? ExtractZodType
+ : undefined
+ : Part extends 'params'
+ ? RouteCfg extends { request: { params: infer S } }
+ ? ExtractZodType
+ : undefined
+ : never;
+
+type FindRouteConfig<
+ M extends { routes: readonly Route[] },
+ P extends string,
+ Method extends string,
+> = Extract<
+ M['routes'][number],
+ { route: { method: Method; path: P } }
+>['route'];
+
+type BuildArgsType = {
+ [K in 'body' | 'params' | 'query' as InferInputType<
+ RouteCfg,
+ K
+ > extends undefined
+ ? never
+ : K]: InferInputType;
+};
+
+export type GetValidPathsForModule<
+ ModulePath extends string,
+ MainModuleName extends string,
+ MainRoutes extends readonly RouteShape[],
+ SubModules extends readonly ModuleSpec[],
+> = ExtractPaths<
+ GetTargetModule
+>;
+
+type GetValidMethodForPath<
+ ModulePath extends string,
+ Path extends string,
+ MainModuleName extends string,
+ MainRoutes extends readonly RouteShape[],
+ SubModules extends readonly ModuleSpec[],
+> = Lowercase<
+ Extract<
+ ExtractMethodForPath<
+ GetTargetModule,
+ Path
+ >,
+ string
+ >
+>;
+
+interface BaseFetcherParams<
+ M extends string,
+ Routes extends Route[],
+ Modules extends BaseBuildModuleReturn[],
+ ModuleName extends GetModulePaths,
+ SelectedPath extends GetValidPathsForModule,
+> {
+ method: GetValidMethodForPath;
+ module: ModuleName;
+ path: SelectedPath;
+}
+
+export type FetcherParams<
+ M extends string,
+ Routes extends Route[],
+ Modules extends BaseBuildModuleReturn[],
+ ModuleName extends GetModulePaths,
+ SelectedPath extends GetValidPathsForModule,
+ RouteConfig extends FindRouteConfig<
+ GetTargetModule,
+ SelectedPath,
+ GetValidMethodForPath
+ > = FindRouteConfig<
+ GetTargetModule,
+ SelectedPath,
+ GetValidMethodForPath
+ >,
+ ArgsType extends BuildArgsType = BuildArgsType,
+> = BaseFetcherParams &
+ (keyof ArgsType extends never ? { args?: undefined } : { args: ArgsType });
+
+type InferStatusCode = K extends `${infer N extends number}`
+ ? N
+ : K extends number
+ ? K
+ : never;
+
+export type InferResponseType<
+ M extends string,
+ Routes extends Route[],
+ Modules extends BaseBuildModuleReturn[],
+ ModuleName extends GetModulePaths,
+ SelectedPath extends GetValidPathsForModule,
+ RouteConfig extends FindRouteConfig<
+ GetTargetModule,
+ SelectedPath,
+ GetValidMethodForPath
+ > = FindRouteConfig<
+ GetTargetModule,
+ SelectedPath,
+ GetValidMethodForPath
+ >,
+> = RouteConfig extends { responses: infer S }
+ ? {
+ [K in keyof S]: S[K] extends infer Response
+ ? Response extends { content: infer C }
+ ? {
+ [Fmt in keyof C]: ClientResponse<
+ C[Fmt] extends { schema: infer S } ? ExtractZodType : never,
+ InferStatusCode,
+ Fmt extends string ? Fmt : string
+ >;
+ }[keyof C]
+ : ClientResponse |