Skip to content

Commit

Permalink
chore: backend typescript aliases and fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
brunotot committed Apr 20, 2024
1 parent 18ee814 commit 166825b
Show file tree
Hide file tree
Showing 81 changed files with 4,763 additions and 3,614 deletions.
4 changes: 3 additions & 1 deletion packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,18 @@
"license": "MIT",
"dependencies": {
"@anatine/zod-openapi": "^2.2.5",
"@org/shared": "^0.0.1",
"@org/shared": "file:../shared/src",
"@tsvdec/core": "^2.0.11",
"@tsvdec/decorators": "^1.0.7",
"@zodyac/zod-mongoose": "^1.1.2",
"bcrypt": "^5.1.1",
"bottlejs": "^2.0.1",
"express": "^4.18.2",
"express-rate-limit": "^7.2.0",
"http-status": "^1.7.4",
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.2.0",
"openapi3-ts": "^4.3.1",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.0",
"zod": "^3.22.4"
Expand Down
13 changes: 13 additions & 0 deletions packages/backend/src/@types/express.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { HttpStatusNumeric } from "../config";

declare global {
namespace Express {
export interface Response {
sendError: (
httpStatus: HttpStatusNumeric,
details?: string,
metadata?: Record<string, unknown>,
) => never;
}
}
}
1 change: 1 addition & 0 deletions packages/backend/src/@types/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./express";
113 changes: 34 additions & 79 deletions packages/backend/src/App.ts
Original file line number Diff line number Diff line change
@@ -1,112 +1,67 @@
import compression from "compression";
import cookieParser from "cookie-parser";
import cors from "cors";
import express, { Router } from "express";
import helmet from "helmet";
import hpp from "hpp";
import { connect, set } from "mongoose";
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 { RoutesMetaService } from "./meta/RoutesMetaService";
import { withAugmentedResponse } from "./middleware/withAugmentedResponse";
import { withCredentials } from "./middleware/withCredentials";
//import { ErrorMiddleware } from "@middlewares/error.middleware";
import { $BackendAppConfig, mongoConnect, registerSwagger, startupLog } from "@config";
import { registerRoutes } from "@decorators";
import { GLOBAL_MIDDLEWARES } from "@infrastructure";
import express from "express";

export class App {
public app: express.Application;
public env: string;
public port: string;
public swaggerPath: string;
public url: string;
public readonly app: express.Application;
public readonly env: string;
public readonly port: string;
public readonly swaggerPath: string;
public readonly url: string;

constructor() {
this.#initializeUncaughtExceptionHandler();

this.app = express();
this.env = $BackendAppConfig.env.NODE_ENV;
this.port = $BackendAppConfig.env.PORT;
this.swaggerPath = "api-docs";
this.url = `http://localhost:${this.port}`;
this.url = $BackendAppConfig.url;

this.databaseConnect();
this.initializeMiddlewares();
this.initializeRoutes();
this.initializeSwagger();
//this.initializeErrorHandling();
this.#initializeDatabase();
this.#initializeMiddlewares();
this.#initializeRoutes();
this.#initializeSwagger();
}

public listen() {
this.app.listen(this.port, () => {
startupLog({
title: "Express app started!",
title: `[Express] MERN Sample App v${$BackendAppConfig.env.PACKAGE_JSON_VERSION}`,
data: {
"🏠 Env": this.env,
"🟢 NodeJS": process.version,
"📦 Database": $BackendAppConfig.databaseConnectionParams.dbName,
"🚀 App": this.url,
"📝 Swagger": `${this.url}/${this.swaggerPath}`,
"🆔 PID": `${process.pid}`,
"🧠 Memory": `${Math.round((process.memoryUsage().heapUsed / 1024 / 1024) * 100) / 100} MB`,
"🏠 Env": this.env,
"📅 Started": new Date().toLocaleString(),
},
});
});
}

public getServer() {
return this.app;
async #initializeDatabase() {
await mongoConnect();
}

private async databaseConnect() {
const { dbHost, dbPort, dbName, ...restOptions } = $BackendAppConfig.databaseConnectionParams;
const mongoUri = `mongodb://${dbHost}:${dbPort}`;
if ($BackendAppConfig.env.NODE_ENV !== "production") set("debug", true);
await connect(mongoUri, {
dbName,
...restOptions,
});
#initializeMiddlewares() {
GLOBAL_MIDDLEWARES.forEach(middleware => this.app.use(middleware));
}

private initializeMiddlewares() {
this.app.use(morgan($BackendAppConfig.env.LOG_FORMAT, { stream }));
this.app.use(withCredentials());
this.app.use(
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());
#initializeRoutes() {
registerRoutes(this.app);
}

private initializeRoutes() {
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);
});
#initializeSwagger() {
registerSwagger(this.app, this.swaggerPath);
}

private initializeSwagger() {
const swaggerSpec = $SwaggerManager.buildSpec();
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);
#initializeUncaughtExceptionHandler() {
process.on("uncaughtException", err => {
console.error("Uncaught Exception:", err);
});
}

/*private initializeErrorHandling() {
this.app.use(ErrorMiddleware);
}*/
}
9 changes: 9 additions & 0 deletions packages/backend/src/config/BackendAppConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ export class BackendAppConfig {
public databaseConnectionParams: DatabaseConnectionParams;
public userRoles = VAR_USER_ROLES;

public get url() {
const domain =
this.env.NODE_ENV === "production"
? `https://${process.env.RAILWAY_PUBLIC_DOMAIN}`
: "http://localhost";

return `${domain}:${this.env.PORT}`;
}

constructor() {
// NOOP
}
Expand Down
6 changes: 6 additions & 0 deletions packages/backend/src/config/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import "./logger";

// ADD

export * from "./zod";

// BackendAppConfig must be imported first in order to be used in other files.
export * from "./BackendAppConfig";
export * from "./swagger";
Expand Down
12 changes: 6 additions & 6 deletions packages/backend/src/config/ioc/bottlejs.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Class } from "@org/shared";
import Bottle from "bottlejs";
import { getInjectionClasses } from "../../decorators/@Injectable";
import { InjectionMetaService } from "../../meta/InjectionMetaService";
import { getInjectionClasses } from "../../decorators/ioc/@Injectable";
import { InjectionDecoratorManager } from "../../decorators/managers/InjectionDecoratorManager";

const bottle = new Bottle();
const container = bottle.container;
Expand All @@ -14,14 +14,14 @@ export function iocStartup() {
const injectionClasses = getInjectionClasses();

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

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;
const { name: nameA } = InjectionDecoratorManager.from(classA).value;
const { name: nameB } = InjectionDecoratorManager.from(classB).value;
if (dependencySchema[nameA].length === 0) return -1;
if (dependencySchema[nameB].length === 0) return 1;
if (dependencySchema[nameA].includes(nameB)) return 1;
Expand All @@ -33,7 +33,7 @@ export function iocStartup() {
const sortedInjectionClasses = sortInjectionClasses(injectionClasses, dependencySchema);

sortedInjectionClasses.forEach(Class => {
const name = InjectionMetaService.from(Class).value.name;
const name = InjectionDecoratorManager.from(Class).value.name;
bottle.service(name, Class, ...dependencySchema[name]);
});
}
2 changes: 1 addition & 1 deletion packages/backend/src/config/ioc/scanner.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { iocStartup } from "./bottlejs";

// Import all directories that need to be registered in the IoC container.
import "../../controllers";
import "../../infrastructure";
import "../../web/controllers";

iocStartup();
7 changes: 4 additions & 3 deletions packages/backend/src/config/logger/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,13 @@ function startupLog({ title, data, kvSeparator = " : ", padding = 2 }: StartupLo

const spacer = " ".repeat(padding);
const hrY = kvSeparator;
const maxKeyLength = Math.max(...Object.keys(data).map(key => key.length));

const keyValueLengths = Object.entries(data).map(
([key, value]) => key.length + hrY.length + value.length,
const keyValueLengths = Object.values(data).map(
value => maxKeyLength + hrY.length + value.length,
);

const containerWidth = Math.max(title.length, ...keyValueLengths) + padding * 2;
const maxKeyLength = Math.max(...Object.keys(data).map(key => key.length));

const hrX = `${"─".repeat(containerWidth)}`;

Expand Down
36 changes: 19 additions & 17 deletions packages/backend/src/config/swagger/swagger.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import { Class } from "@org/shared";
import swaggerJsdoc from "swagger-jsdoc";
//import PackageJson from "./../../../package.json";
import express from "express";
import swaggerUi from "swagger-ui-express";
import { RouteDecoratorManager } from "../../decorators/managers";
import { $BackendAppConfig } from "../BackendAppConfig";
import { RoutesMetaService } from "./../../meta";
import {
SwaggerDefinition,
SwaggerPath,
SwaggerRequestMapping,
SwaggerTag,
} from "./types";
import { SwaggerDefinition, SwaggerPath, SwaggerRequestMapping, SwaggerTag } from "./types";

const DEFAULT_SWAGGER_DEFINITION: SwaggerDefinition = {
openapi: "3.0.0",
Expand Down Expand Up @@ -63,29 +60,25 @@ export class SwaggerManager {
}

registerTag(tagData: SwaggerTag & { constructor: Class }) {
if (this.tags.some((t) => t.name === tagData.name)) return;
if (this.tags.some(t => t.name === tagData.name)) return;
const { constructor, ...tag } = tagData;
this.controllerClasses.push(constructor);
this.tags.push(tag);
}

registerPath(
path: string,
requestMapping: SwaggerRequestMapping,
data: SwaggerPath
) {
#registerPath(path: string, requestMapping: SwaggerRequestMapping, data: SwaggerPath) {
if (!this.definition.paths[path]) this.definition.paths[path] = {};
this.definition.paths[path][requestMapping] = data;
}

#registerPaths() {
$SwaggerManager.controllerClasses.forEach((controllerClass) => {
const meta = RoutesMetaService.from(controllerClass).value;
meta.routes.forEach((route) => {
this.controllerClasses.forEach(controllerClass => {
const meta = RouteDecoratorManager.from(controllerClass).value;
meta.routes.forEach(route => {
const fullPath = `${meta.basePath}${route.path}`;
const swagger = route.swagger ?? {};
swagger.tags = [String(controllerClass.name)];
$SwaggerManager.registerPath(fullPath, route.method, swagger);
this.#registerPath(fullPath, route.method, swagger);
});
});
}
Expand All @@ -97,3 +90,12 @@ export class SwaggerManager {
}

export const $SwaggerManager = new SwaggerManager();

export function registerSwagger(app: express.Application, path: string) {
const swaggerSpec = $SwaggerManager.buildSpec();
app.use(`/${path}`, swaggerUi.serve, swaggerUi.setup(swaggerSpec));
app.get(`/${path}.json`, (_req, res) => {
res.setHeader("Content-Type", "application/json");
res.send(swaggerSpec);
});
}
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 | "ERROR",
HttpStatusNumeric | "default",
{
description?: string;
content?: SwaggerRequestContent;
Expand Down
14 changes: 12 additions & 2 deletions packages/backend/src/config/vars/databaseConnectionParams.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { TODO } from "@org/shared";
import { ConnectOptions, Document } from "mongoose";
import { ConnectOptions, Document, connect, set } from "mongoose";
import { $BackendAppConfig } from "../BackendAppConfig";

export type InferMongoId<T> = "_id" extends keyof T ? T["_id"] : TODO;

export type Entity<T> = Document<InferMongoId<T>, TODO, T>;
export type Entity<T> = Document<InferMongoId<T>, TODO, T> & T;

export type DatabaseConnectionParams = {
dbHost: string;
Expand All @@ -18,4 +18,14 @@ export const VAR_DATABASE_CONNECTION_PARAMS: DatabaseConnectionParams = {
dbName: $BackendAppConfig.env.DB_DATABASE,
};

export async function mongoConnect() {
const { dbHost, dbPort, dbName, ...restOptions } = $BackendAppConfig.databaseConnectionParams;
const mongoUri = `mongodb://${dbHost}:${dbPort}`;
if ($BackendAppConfig.env.NODE_ENV !== "production") set("debug", true);
await connect(mongoUri, {
dbName,
...restOptions,
});
}

$BackendAppConfig.databaseConnectionParams = VAR_DATABASE_CONNECTION_PARAMS;
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { zodSchema } from "@zodyac/zod-mongoose";
import mongoose from "mongoose";
import { z } from "zod";

export function buildMongoEntity<const T extends z.AnyZodObject>(
export function convertZodToMongooseModel<const T extends z.AnyZodObject>(
name: string,
zodModel: T,
): mongoose.Model<z.infer<T>> {
Expand Down
Loading

0 comments on commit 166825b

Please sign in to comment.