Skip to content

Commit d44504a

Browse files
committed
MagicRouter inprogress
1 parent 938430e commit d44504a

14 files changed

+303
-163
lines changed

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"@types/node": "^18.11.18",
3131
"@types/nodemailer": "^6.4.8",
3232
"@types/passport": "^1.0.11",
33+
"@types/swagger-ui-express": "^4.1.6",
3334
"@types/validator": "^13.7.17",
3435
"@typescript-eslint/eslint-plugin": "^5.62.0",
3536
"@typescript-eslint/parser": "^7.11.0",
@@ -82,6 +83,7 @@
8283
"multer-s3": "^3.0.1",
8384
"nanoid": "^3.3.7",
8485
"nodemailer": "^6.9.13",
86+
"openapi3-ts": "^4.3.3",
8587
"passport": "^0.7.0",
8688
"passport-jwt": "^4.0.1",
8789
"pino": "^9.1.0",

pnpm-lock.yaml

+13
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/main.ts

+8-7
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import { z } from 'zod';
2+
import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi';
3+
extendZodWithOpenApi(z);
4+
15
import { createBullBoard } from '@bull-board/api';
26
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
37
import { ExpressAdapter } from '@bull-board/express';
@@ -20,18 +24,13 @@ import { useSocketIo } from './lib/realtime.server';
2024
import path from 'path';
2125

2226
import swaggerUi from 'swagger-ui-express';
27+
28+
import { convertDocumentationToYaml } from './openapi/swagger-doc-generator';
2329
import YAML from 'yaml';
24-
import fs from 'node:fs/promises';
2530

2631
const boostrapServer = async () => {
2732
await connectDatabase();
2833

29-
const file = await fs.readFile(
30-
path.join(process.cwd(), 'openapi-docs.yml'),
31-
'utf8',
32-
);
33-
const swaggerDocument = YAML.parse(file);
34-
3534
const app = express();
3635

3736
app.set('trust proxy', true);
@@ -86,6 +85,8 @@ const boostrapServer = async () => {
8685
}
8786

8887
app.use('/api', apiRoutes);
88+
89+
const swaggerDocument = YAML.parse(convertDocumentationToYaml());
8990
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));
9091

9192
const serverAdapter = new ExpressAdapter();

src/middlewares/can-access.middleware.ts

+3-6
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export const canAccess =
1717
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1818
req: Request<any, any, any, any>,
1919
res: Response,
20-
next: NextFunction,
20+
next?: NextFunction,
2121
) => {
2222
try {
2323
const requestUser = req?.user as JwtPayload;
@@ -29,10 +29,7 @@ export const canAccess =
2929
StatusCodes.UNAUTHORIZED,
3030
);
3131
}
32-
const currentUser = await getUserById(
33-
{ id: requestUser.sub },
34-
requestUser.role,
35-
);
32+
const currentUser = await getUserById({ id: requestUser.sub });
3633

3734
if (!currentUser) {
3835
return errorResponse(res, 'Login again', StatusCodes.UNAUTHORIZED);
@@ -102,5 +99,5 @@ export const canAccess =
10299
);
103100
}
104101

105-
next();
102+
next?.();
106103
};

src/middlewares/validate-zod-schema.middleware.ts

+5-10
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,16 @@ import { StatusCodes } from 'http-status-codes';
33
import { ZodError, ZodSchema } from 'zod';
44
import { errorResponse } from '../utils/api.utils';
55
import { sanitizeRecord } from '../utils/common.utils';
6-
7-
export type ValidateZodSchemaType = {
8-
params?: ZodSchema;
9-
query?: ZodSchema;
10-
body?: ZodSchema;
11-
};
6+
import { RequestZodSchemaType } from '../types';
127

138
export const validateZodSchema =
14-
(payload: ValidateZodSchemaType) =>
9+
(payload: RequestZodSchemaType) =>
1510
// eslint-disable-next-line @typescript-eslint/no-explicit-any
16-
(req: Request<any, any, any, any>, res: Response, next: NextFunction) => {
11+
(req: Request<any, any, any, any>, res: Response, next?: NextFunction) => {
1712
let error: ZodError | null = null;
1813

1914
Object.entries(payload).forEach((prop) => {
20-
const [key, value] = prop as [keyof ValidateZodSchemaType, ZodSchema];
15+
const [key, value] = prop as [keyof RequestZodSchemaType, ZodSchema];
2116

2217
const parsed = value.safeParse(req[key]);
2318

@@ -40,6 +35,6 @@ export const validateZodSchema =
4035
error,
4136
);
4237
} else {
43-
next();
38+
next?.();
4439
}
4540
};

src/openapi/magic-router.ts

+141
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { NextFunction, Request, Response, Router } from 'express';
2+
import { ZodTypeAny } from 'zod';
3+
import { validateZodSchema } from '../middlewares/validate-zod-schema.middleware';
4+
import { RequestZodSchemaType } from '../types';
5+
import {
6+
parseRouteString,
7+
routeToClassName,
8+
camelCaseToTitleCase,
9+
} from './openapi.utils';
10+
import { registry } from './swagger-instance';
11+
12+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
13+
export type MaybePromise = any | Promise<any> | void;
14+
15+
export class MagicRouter {
16+
private router: Router;
17+
private rootRoute: string;
18+
19+
constructor(rootRoute: string) {
20+
this.router = Router();
21+
this.rootRoute = rootRoute;
22+
}
23+
24+
private getPath(path: string) {
25+
return this.rootRoute + parseRouteString(path);
26+
}
27+
28+
public get(
29+
path: string,
30+
requestType: RequestZodSchemaType = {},
31+
responseModel: ZodTypeAny,
32+
...middlewares: Array<
33+
(
34+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
35+
req: Request<any, any, any, any, any>,
36+
res: Response,
37+
next?: NextFunction,
38+
) => MaybePromise
39+
>
40+
): void {
41+
const bodySchema = requestType.body
42+
? registry.register(routeToClassName(this.rootRoute), requestType.body)
43+
: null;
44+
45+
registry.registerPath({
46+
method: 'get',
47+
path: this.getPath(path),
48+
description: camelCaseToTitleCase(
49+
middlewares[middlewares.length - 1].name,
50+
),
51+
summary: camelCaseToTitleCase(middlewares[middlewares.length - 1].name),
52+
request: {
53+
params: requestType.params,
54+
query: requestType.query,
55+
...(bodySchema
56+
? {
57+
body: {
58+
content: {
59+
'applicat ion/json': {
60+
schema: bodySchema,
61+
},
62+
},
63+
},
64+
}
65+
: {}),
66+
},
67+
responses: {
68+
200: {
69+
description: '',
70+
content: {
71+
'application/json': {
72+
schema: responseModel,
73+
},
74+
},
75+
},
76+
},
77+
});
78+
79+
if (Object.keys(requestType).length) {
80+
this.router.get(path, validateZodSchema(requestType), ...middlewares);
81+
} else {
82+
this.router.get(path, ...middlewares);
83+
}
84+
}
85+
86+
public post(
87+
path: string,
88+
requestType: RequestZodSchemaType = {},
89+
responseModel: ZodTypeAny,
90+
...middlewares: Array<
91+
(
92+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
93+
req: Request<any, any, any, any, any>,
94+
res: Response,
95+
next?: NextFunction,
96+
) => MaybePromise
97+
>
98+
): void {
99+
this.router.post(path, ...middlewares);
100+
}
101+
public put(
102+
path: string,
103+
...middlewares: Array<
104+
(req: Request, res: Response, next: NextFunction) => void
105+
>
106+
): void {
107+
this.router.put(path, ...middlewares);
108+
}
109+
public delete(
110+
path: string,
111+
...middlewares: Array<
112+
(req: Request, res: Response, next: NextFunction) => void
113+
>
114+
): void {
115+
this.router.delete(path, ...middlewares);
116+
}
117+
public patch(
118+
path: string,
119+
...middlewares: Array<
120+
(
121+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
122+
req: Request<any, any, any, any>,
123+
res: Response,
124+
next: NextFunction,
125+
) => void
126+
>
127+
): void {
128+
this.router.patch(path, ...middlewares);
129+
}
130+
131+
public use(...args: Parameters<Router['use']>): void {
132+
this.router.use(...args);
133+
}
134+
135+
// Method to get the router instance
136+
public getRouter(): Router {
137+
return this.router;
138+
}
139+
}
140+
141+
export default MagicRouter;

src/openapi/openapi.utils.ts

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export const parseRouteString = (route: string): string => {
2+
return route.replace(/\/:(\w+)/g, '/{$1}');
3+
};
4+
5+
export const routeToClassName = (route: string): string => {
6+
const cleanedRoute = route.replace(/^\/|\/$/g, '');
7+
8+
const className =
9+
cleanedRoute.charAt(0).toUpperCase() + cleanedRoute.slice(1).toLowerCase();
10+
11+
return className.endsWith('s') ? className.slice(0, -1) : className;
12+
};
13+
14+
export const camelCaseToTitleCase = (input: string): string => {
15+
return input
16+
.replace(/([A-Z])/g, ' $1')
17+
.replace(/^./, (str) => str.toUpperCase())
18+
.trim();
19+
};

src/openapi/swagger-doc-generator.ts

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { OpenApiGeneratorV3 } from '@asteasolutions/zod-to-openapi';
2+
import fs from 'node:fs/promises';
3+
import * as yaml from 'yaml';
4+
5+
import type { OpenAPIObject } from 'openapi3-ts/oas30';
6+
import { registry } from './swagger-instance';
7+
8+
export const getOpenApiDocumentation = (): OpenAPIObject => {
9+
const generator = new OpenApiGeneratorV3(registry.definitions);
10+
11+
return generator.generateDocument({
12+
openapi: '3.0.0',
13+
info: {
14+
version: '1.0.0',
15+
title: 'My API',
16+
description: 'This is the API',
17+
},
18+
servers: [{ url: 'v1' }],
19+
});
20+
};
21+
22+
export const convertDocumentationToYaml = (): string => {
23+
const docs = getOpenApiDocumentation();
24+
25+
const fileContent = yaml.stringify(docs);
26+
27+
return fileContent;
28+
};
29+
30+
export const writeDocumentationToDisk = async (): Promise<void> => {
31+
const fileContent = convertDocumentationToYaml();
32+
33+
await fs.writeFile(`${__dirname}/openapi-docs.yml`, fileContent, {
34+
encoding: 'utf-8',
35+
});
36+
};

src/openapi/swagger-instance.ts

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi';
2+
3+
export const registry = new OpenAPIRegistry();
4+
5+
export const bearerAuth = registry.registerComponent(
6+
'securitySchemes',
7+
'bearerAuth',
8+
{
9+
type: 'http',
10+
scheme: 'bearer',
11+
bearerFormat: 'JWT',
12+
},
13+
);

src/types.ts

+11
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
1+
import { AnyZodObject, ZodEffects, ZodSchema } from 'zod';
12
import { IUser } from './user/user.model';
23

34
export type UserType = IUser & { _id?: string };
45

6+
type ZodObjectWithEffect =
7+
| AnyZodObject
8+
| ZodEffects<ZodObjectWithEffect, unknown, unknown>;
9+
510
export interface GoogleCallbackQuery {
611
code: string;
712
error?: string;
813
}
14+
15+
export type RequestZodSchemaType = {
16+
params?: ZodObjectWithEffect;
17+
query?: ZodObjectWithEffect;
18+
body?: ZodSchema;
19+
};

0 commit comments

Comments
 (0)