Skip to content

Commit

Permalink
chore: statically analize code to determine error statuses for Swagger
Browse files Browse the repository at this point in the history
  • Loading branch information
brunotot committed Apr 18, 2024
1 parent 806a40b commit c22b622
Show file tree
Hide file tree
Showing 35 changed files with 360 additions and 213 deletions.
18 changes: 8 additions & 10 deletions packages/backend/src/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import morgan from "morgan";
import swaggerUi from "swagger-ui-express";
import { $BackendAppConfig, startupLog, stream } from "./config";
import { $SwaggerManager } from "./config/swagger";
import { getInjectionClasses } from "./decorators/Injectable";
import { getInjectionClasses } from "./decorators/@Injectable";
import { RoutesMetaService } from "./meta/RoutesMetaService";
import { withAugmentedResponse } from "./middleware/withAugmentedResponse";
import { withCredentials } from "./middleware/withCredentials";
//import { ErrorMiddleware } from "@middlewares/error.middleware";

Expand Down Expand Up @@ -53,8 +54,7 @@ export class App {
}

private async databaseConnect() {
const { dbHost, dbPort, dbName, ...restOptions } =
$BackendAppConfig.databaseConnectionParams;
const { dbHost, dbPort, dbName, ...restOptions } = $BackendAppConfig.databaseConnectionParams;
const mongoUri = `mongodb://${dbHost}:${dbPort}`;
if ($BackendAppConfig.env.NODE_ENV !== "production") set("debug", true);
await connect(mongoUri, {
Expand All @@ -70,25 +70,27 @@ export class App {
cors({
origin: $BackendAppConfig.env.ORIGIN,
credentials: $BackendAppConfig.env.CREDENTIALS === "true",
})
}),
);
this.app.use(hpp());
this.app.use(helmet());
this.app.use(compression());
this.app.use(express.json());
this.app.use(express.urlencoded({ extended: true }));
this.app.use(cookieParser());
this.app.use(withAugmentedResponse());
//Should be specified at endpoint level.
//this.app.use(verifyJWT());
}

private initializeRoutes() {
getInjectionClasses().forEach((clazz) => {
getInjectionClasses().forEach(clazz => {
const router = Router();
const { basePath, routes } = RoutesMetaService.from(clazz).value;
routes.forEach(({ method, path = "", middlewares, handler }) => {
const fullPath = `${basePath}${path}`;
const pipeline = middlewares ? [...middlewares, handler] : [handler];
// @ts-expect-error Unknown
router[method](fullPath, ...pipeline);
});
this.app.use("/", router);
Expand All @@ -97,11 +99,7 @@ export class App {

private initializeSwagger() {
const swaggerSpec = $SwaggerManager.buildSpec();
this.app.use(
`/${this.swaggerPath}`,
swaggerUi.serve,
swaggerUi.setup(swaggerSpec)
);
this.app.use(`/${this.swaggerPath}`, swaggerUi.serve, swaggerUi.setup(swaggerSpec));
this.app.get(`/${this.swaggerPath}.json`, (_req, res) => {
res.setHeader("Content-Type", "application/json");
res.send(swaggerSpec);
Expand Down
26 changes: 8 additions & 18 deletions packages/backend/src/config/ioc/bottlejs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Class } from "@org/shared";
import Bottle from "bottlejs";
import { getInjectionClasses } from "../../decorators/Injectable";
import { getInjectionClasses } from "../../decorators/@Injectable";
import { InjectionMetaService } from "../../meta/InjectionMetaService";

const bottle = new Bottle();
Expand All @@ -13,19 +13,12 @@ export function inject<T>(name: string): T {
export function iocStartup() {
const injectionClasses = getInjectionClasses();

const dependencySchema: Record<string, string[]> = injectionClasses.reduce(
(acc, Class) => {
const { name, dependencies = [] } =
InjectionMetaService.from(Class).value;
return { ...acc, [name]: dependencies };
},
{}
);
const dependencySchema: Record<string, string[]> = injectionClasses.reduce((acc, Class) => {
const { name, dependencies = [] } = InjectionMetaService.from(Class).value;
return { ...acc, [name]: dependencies };
}, {});

function sortInjectionClasses(
classes: Class[],
dependencySchema: Record<string, string[]>
) {
function sortInjectionClasses(classes: Class[], dependencySchema: Record<string, string[]>) {
return [...classes].sort((classA, classB) => {
const { name: nameA } = InjectionMetaService.from(classA).value;
const { name: nameB } = InjectionMetaService.from(classB).value;
Expand All @@ -37,12 +30,9 @@ export function iocStartup() {
});
}

const sortedInjectionClasses = sortInjectionClasses(
injectionClasses,
dependencySchema
);
const sortedInjectionClasses = sortInjectionClasses(injectionClasses, dependencySchema);

sortedInjectionClasses.forEach((Class) => {
sortedInjectionClasses.forEach(Class => {
const name = InjectionMetaService.from(Class).value.name;
bottle.service(name, Class, ...dependencySchema[name]);
});
Expand Down
2 changes: 1 addition & 1 deletion packages/backend/src/config/swagger/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export type SwaggerRequestMapping =

export type SwaggerResponse = Partial<
Record<
HttpStatusNumeric,
HttpStatusNumeric | "ERROR",
{
description?: string;
content?: SwaggerRequestContent;
Expand Down
58 changes: 17 additions & 41 deletions packages/backend/src/controllers/AuthController.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { generateSchema } from "@anatine/zod-openapi";
import { TODO } from "@org/shared";
import bcrypt from "bcrypt";
import { Request, Response } from "express";
Expand All @@ -9,41 +8,19 @@ import { $BackendAppConfig } from "../config/BackendAppConfig";
import { Controller, PostMapping, Use } from "../decorators";
import UserDomain from "../domain/UserDomain";
import { withValidatedBody } from "../middleware";
import { buildSwaggerBody } from "../swagger/SwaggerRequestBody";

const LoginForm = z.object({
username: z.string(),
password: z.string(),
});
type LoginForm = z.infer<typeof LoginForm>;

/**
*
* Recursively iterate over keys of generated and its children values if object and convert all keys which are "type" from array of strings to just a string on zeroth index in array
*/
function _generateSchema(schema: TODO): TODO {
const generated = generateSchema(schema);
const iterate = (obj: TODO) => {
for (const key in obj) {
if (key === "type") {
obj[key] = obj[key][0];
} else if (typeof obj[key] === "object") {
iterate(obj[key]);
}
}
};
iterate(generated);
return generated;
}
const LoginResponse = z.object({
accessToken: z.string(),
});

function buildRequestBody(schema: TODO) {
return {
content: {
"application/json": {
schema: _generateSchema(schema),
},
},
};
}
type LoginResponse = z.infer<typeof LoginResponse>;

@Controller("/auth", {
description: "Authentication",
Expand All @@ -53,28 +30,27 @@ export class AuthController {
@PostMapping("/login", {
description: "Login user",
summary: "Login user",
requestBody: buildRequestBody(LoginForm),
requestBody: buildSwaggerBody(LoginForm),
responses: {
[HttpStatus.OK]: {
description: "Access token",
},
[HttpStatus.UNAUTHORIZED]: {
description: "Unauthorized",
},
[HttpStatus.BAD_REQUEST]: {
description: "Bad request",
content: buildSwaggerBody(LoginResponse).content,
},
},
})
async login(req: Request, res: Response) {
async login(req: Request, res: Response<TODO>) {
const cookies = req.cookies;

const { username, password } = req.body;
if (!username || !password)
return res.status(400).json({ message: "Username and password are required." });
if (!username || !password) {
res.sendError(422, "Username and password are required.");
}

const foundUser = await UserDomain.findOne({ username: username }).exec();
if (!foundUser) return res.sendStatus(401); //Unauthorized
if (!foundUser) {
res.sendError(401);
} //Unauthorized

// evaluate password
const match = await bcrypt.compare(password, foundUser.password);
if (match) {
Expand Down Expand Up @@ -137,9 +113,9 @@ export class AuthController {
});

// Send authorization roles and access token to user
res.json({ accessToken });
return res.json({ accessToken: accessToken });
} else {
res.sendStatus(401);
res.sendError(401);
}
}

Expand Down
15 changes: 10 additions & 5 deletions packages/backend/src/controllers/UserController.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Role } from "@org/shared";
import { Request, Response } from "express";
import HttpStatus from "http-status";
import { Autowired } from "../decorators/Autowired";
import { Controller } from "../decorators/Controller";
import { GetMapping } from "../decorators/GetMapping";
import { PostMapping } from "../decorators/PostMapping";
import { Use } from "../decorators/Use";
import { Autowired } from "../decorators/@Autowired";
import { Controller } from "../decorators/@Controller";
import { Use } from "../decorators/@Use";
import { GetMapping } from "../decorators/route/mapping/@GetMapping";
import { PostMapping } from "../decorators/route/mapping/@PostMapping";
import { UserService } from "../infrastructure/service/UserService";
import { withJwt } from "../middleware/withJwt";
import { withUserRoles } from "../middleware/withUserRoles";
Expand All @@ -23,6 +23,11 @@ export class UserController {
responses: {
[HttpStatus.OK]: {
description: "List of users",
content: {
"": {
schema: {},
},
},
},
},
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Class } from "@org/shared";
import { $SwaggerManager, SwaggerTag } from "../config";
import { RoutesMetaService } from "../meta/RoutesMetaService";
import { Injectable } from "./Injectable";
import { Injectable } from "./@Injectable";

export function Controller<This extends Class>(
basePath: string,
swaggerTag: Omit<SwaggerTag, "name"> = {}
swaggerTag: Omit<SwaggerTag, "name"> = {},
) {
return Injectable<This>((context, constructor) => {
const swaggerTagName = String(context.name!);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,9 @@ export function getInjectionClasses() {
return injectionClasses;
}

export type ClassDecoratorSupplier = (
context: DecoratorContext,
constructor: Class
) => void;
export type ClassDecoratorSupplier = (context: DecoratorContext, constructor: Class) => void;

export function Injectable<This extends Class>(
supplier?: ClassDecoratorSupplier
) {
export function Injectable<This extends Class>(supplier?: ClassDecoratorSupplier) {
return createClassDecorator<This>(({ clazz: constructor, meta }) => {
const context = meta.context;
const constructorName: string = constructor.name;
Expand All @@ -32,7 +27,6 @@ function normalizeTargetName(targetName: string) {
const targetNameSanitized = targetName.endsWith(commonSuffix)
? targetName.slice(0, targetNameLength - commonSuffix.length)
: targetName;
const uncapitalize = (str: string) =>
str.charAt(0).toLowerCase() + str.slice(1);
const uncapitalize = (str: string) => str.charAt(0).toLowerCase() + str.slice(1);
return uncapitalize(targetNameSanitized);
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import { MethodDecoratorDef, createMethodDecorator } from "@tsvdec/decorators";
import {
RouteHandler,
RouteMiddlewareHandler,
RoutesMetaService,
} from "../meta/RoutesMetaService";
import { RouteHandler, RouteMiddlewareHandler, RoutesMetaService } from "../meta/RoutesMetaService";

export function Use<This, Fn extends RouteHandler>(
...handlers: RouteMiddlewareHandler[]
): MethodDecoratorDef<This, Fn> {
return createMethodDecorator<This, Fn>(({ meta }) => {
const routeName = String(meta.context.name);
const routeService = RoutesMetaService.from(meta.context);
routeService.updateRoute(routeName, (r) => ({
routeService.updateRoute(routeName, r => ({
...r,
middlewares: [...(r.middlewares ?? []), ...handlers],
}));
Expand Down
14 changes: 6 additions & 8 deletions packages/backend/src/decorators/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
export * from "./Autowired";
export * from "./Controller";
export * from "./GetMapping";
export * from "./Injectable";
export * from "./PostMapping";
export * from "./Transactional";
export * from "./Use";
export * from "./route/Route";
export * from "./@Autowired";
export * from "./@Controller";
export * from "./@Injectable";
export * from "./@Transactional";
export * from "./@Use";
export * from "./route";
Loading

0 comments on commit c22b622

Please sign in to comment.