Skip to content

Commit

Permalink
Merge pull request #7 from brunotot/refactor-setup-and-runtime-fixes
Browse files Browse the repository at this point in the history
chore: refactor setup and runtime fixes, major rework
  • Loading branch information
brunotot authored Aug 25, 2024
2 parents 8c4db7c + ed68b8e commit 1df9236
Show file tree
Hide file tree
Showing 92 changed files with 1,328 additions and 1,215 deletions.
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/packages/backend/src/main.ts",
"preLaunchTask": "npm: backend:build",
//"preLaunchTask": "npm: backend:build",
"outFiles": ["${workspaceFolder}/packages/backend/dist/**/*.js"],
"envFile": "${workspaceFolder}/packages/backend/.env.development.local",
"sourceMaps": true,
Expand Down
5 changes: 0 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,11 +307,6 @@ Our goal is to provide an easy setup and deployment process, allowing developers
<td align="right">^4.18.2</td>
<td>The web framework used for building the backend API</td>
</tr>
<tr>
<td>express-rate-limit</td>
<td align="right">^7.2.0</td>
<td>Provides rate limiting to protect against brute force attacks</td>
</tr>
<tr>
<td>flatted</td>
<td align="right">^3.3.1</td>
Expand Down
5 changes: 0 additions & 5 deletions md/7-dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,6 @@
<td align="right">^4.18.2</td>
<td>The web framework used for building the backend API</td>
</tr>
<tr>
<td>express-rate-limit</td>
<td align="right">^7.2.0</td>
<td>Provides rate limiting to protect against brute force attacks</td>
</tr>
<tr>
<td>flatted</td>
<td align="right">^3.3.1</td>
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@
"axios": "^1.6.8",
"express-session": "^1.18.0",
"glob": "^11.0.0",
"helmet": "^7.1.0",
"helmet-csp": "^4.0.0",
"keycloak-connect": "^25.0.2",
"react-use": "^17.5.0"
}
Expand Down
14 changes: 14 additions & 0 deletions packages/backend/assets/css/swagger.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,17 @@ table.responses-table
.swagger-ui .highlight-code > .microlight {
min-height: auto !important;
}

.role-badge {
display: inline-block;
padding: 0.2em 0.5em;
font-size: 0.875rem; /* slightly smaller font size */
font-weight: 600; /* makes the text bold */
color: #fff; /* text color */
background-color: #007bff; /* background color, similar to Bootstrap primary */
border-radius: 0.25rem; /* rounded corners */
text-transform: uppercase; /* makes the text uppercase */
vertical-align: middle; /* aligns the badge nicely with text */
white-space: nowrap; /* prevents text from wrapping */
margin-left: 0.5rem;
}
60 changes: 60 additions & 0 deletions packages/backend/assets/js/swagger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Function to be triggered when the element with class 'swagger-container' appears
let swaggerContainerAdded = false;

function onSwaggerContainerAdded() {
if (swaggerContainerAdded) return;
swaggerContainerAdded = true;
const summaries = document.querySelectorAll(".opblock-summary-description");

summaries.forEach(summary => {
const text = summary.textContent;
const data = extractBadges(text);
summary.textContent = data.stringWithoutBadges;
data.badges.forEach(role => {
const roleValue = role.title;
const span = document.createElement("span");
span.classList.add("role-badge");
span.textContent = roleValue;
//summary.textContent = summary.textContent.replace(role[0], "");
summary.appendChild(span);
});
});
}

// Create a MutationObserver to observe changes in the DOM
const observer = new MutationObserver((mutationsList, observer) => {
for (const mutation of mutationsList) {
if (mutation.type === "childList") {
// Check if the added nodes contain the element with class 'swagger-container'
mutation.addedNodes.forEach(node => {
if (node.classList && node.classList.contains("swagger-container")) {
onSwaggerContainerAdded();
observer.disconnect(); // Stop observing if you only need to detect the element once
}
});
}
}
});

// Start observing the document's body for changes
observer.observe(document.body, { childList: true, subtree: true });

function extractBadges(input) {
const badgeRegex = /\[([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]+)\]/g;
let match;
const badges = [];
let stringWithoutBadges = input;

// Extract all badges
while ((match = badgeRegex.exec(input)) !== null) {
badges.push({ name: match[1], title: match[2] });
}

// Remove all badge instances from the string
stringWithoutBadges = stringWithoutBadges.replace(badgeRegex, "").trim();

return {
stringWithoutBadges,
badges,
};
}
7 changes: 3 additions & 4 deletions packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
"type": "module",
"scripts": {
"test": "pnpm run build --silent && export PACKAGE_JSON_VERSION=$(grep -o '\"version\": *\"[^\"]*\"' package.json | awk -F'\"' '{print $4}') && vitest --run",
"build": "rm -rf dist && tsc && tsc-alias",
"start": "export PACKAGE_JSON_VERSION=$(grep -o '\"version\": *\"[^\"]*\"' package.json | awk -F'\"' '{print $4}') && node --no-warnings --loader ts-node/esm --experimental-specifier-resolution=node ./dist/main.js",
"build": "rm -rf dist && tsc --build && tsc-alias",
"start": "bash scripts/start.sh",
"dev": "nodemon",
"clean": "rm -rf ./dist"
},
Expand All @@ -28,7 +28,6 @@
"cross-dirname": "^0.1.0",
"dotenv": "^16.4.5",
"express": "^4.18.2",
"express-rate-limit": "^7.2.0",
"flatted": "^3.3.1",
"glob": "^11.0.0",
"helmet": "^7.1.0",
Expand Down Expand Up @@ -62,7 +61,7 @@
"babel-jest": "^29.7.0",
"jest": "^29.7.0",
"mongodb-memory-server": "^9.2.0",
"nodemon": "^3.1.0",
"nodemon": "^3.1.4",
"supertest": "^7.0.0",
"ts-jest": "^29.1.2",
"ts-node": "^10.9.2",
Expand Down
21 changes: 8 additions & 13 deletions packages/backend/scripts/start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,20 @@ export PACKAGE_JSON_VERSION=$(grep -o '\"version\": *\"[^\"]*\"' package.json |

rm -rf dist

start "" " $(color $CYAN)1.$(color) Compiling shared package"
#start "" " $(color $CYAN)1.$(color) Compiling shared package"
cd "${PWD_BACKEND}/../shared" && rm -rf dist && tsc
stop "$(color $GREEN)$(color)"
#stop "$(color $GREEN)✓$(color)"

start "" " $(color $CYAN)2.$(color) Compiling backend package"
#start "" " $(color $CYAN)2.$(color) Compiling backend package"
cd "${PWD_BACKEND}" && rm -rf dist && tsc
stop "$(color $GREEN)$(color)"
#stop "$(color $GREEN)✓$(color)"

start "" " $(color $CYAN)3.$(color) Converting path aliases to relative paths"
#start "" " $(color $CYAN)3.$(color) Converting path aliases to relative paths"
tsc-alias -p "${PWD_BACKEND}/tsconfig.json"
stop "$(color $GREEN)$(color)"
#stop "$(color $GREEN)✓$(color)"

echo -e "\n$(color $GREEN)$(color $CYAN)4.$(color) Starting...\n"
#echo -e "\n$(color $GREEN)✓ $(color $CYAN)4.$(color) Starting...\n"
export PACKAGE_JSON_VERSION=$(grep -o '\"version\": *\"[^\"]*\"' package.json | awk -F'\"' '{print $4}')
node --no-warnings --loader ts-node/esm --experimental-specifier-resolution=node "${PWD_BACKEND}/dist/main.js"

#
#
#
#export PACKAGE_JSON_VERSION=$(grep -o '\"version\": *\"[^\"]*\"' package.json | awk -F'\"' '{print $4}')
#node --no-warnings --loader ts-node/esm --experimental-specifier-resolution=node ./dist/main.js

echo -e "\n"
141 changes: 64 additions & 77 deletions packages/backend/src/App.ts
Original file line number Diff line number Diff line change
@@ -1,84 +1,101 @@
import express from "express";
import swaggerUi from "swagger-ui-express";
import { MongoClient, type MongoClientOptions } from "mongodb";
import { generateOpenApi } from "@ts-rest/open-api";
import express, { type ErrorRequestHandler } from "express";
import { initServer, createExpressEndpoints } from "@ts-rest/express";
import { CONTRACTS, type ErrorResponse, suppressConsole } from "@org/shared";
import { MongoClient } from "mongodb";

import { GLOBAL_MIDDLEWARES } from "@org/backend/infrastructure/middleware/global";
import { CONTRACTS, operationMapper, suppressConsole } from "@org/shared";
import { Environment } from "@org/backend/config/singletons/Environment";
import { Logger } from "@org/backend/config/singletons/Logger";
import { RouterCollection } from "./config/singletons/RouterCollection";
import { ServiceRegistry } from "./config/singletons/ServiceRegistry";
import MODULES from "./modules";
import keycloak from "./keycloak";
import { log, logTable } from "@org/backend/config/singletons/Logger";
import { RouterCollection } from "@org/backend/config/singletons/RouterCollection";
import { ServiceRegistry } from "@org/backend/config/singletons/ServiceRegistry";
import { env, SERVER_URL } from "@org/backend/config/singletons/Environment";
import { applySwaggerMiddleware, SWAGGER_PATH } from "@org/backend/swagger";
import MODULES, { type NoArgsClass } from "@org/backend/modules";

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

private environment = Environment.getInstance();
private logger = Logger.getInstance();
mongoClient: MongoClient;
public get mongoClient(): MongoClient {
return this.#mongoClient;
}

#mongoClient: MongoClient;

private get memoryUsage() {
return `${Math.round((process.memoryUsage().heapUsed / 1024 / 1024) * 100) / 100} MB`;
}

constructor() {
this.expressApp = express();
this.env = this.environment.vars.NODE_ENV;
this.port = this.environment.vars.PORT;
this.swaggerPath = "api-docs";
const domain =
this.env === "production"
? `https://${process.env.RAILWAY_PUBLIC_DOMAIN}`
: "http://localhost";
this.url = `${domain}:${this.port}`;
this.#initializeGlobalMiddlewares();
this.#initializeRoutes();
this.#initializeSwagger();
this.keycloakUrl = env.KEYCLOAK_URL;
this.port = env.PORT;
this.url = SERVER_URL;
log.info("Applying Swagger middleware");
applySwaggerMiddleware(this.expressApp);
}

public async prepare(): Promise<void> {
ServiceRegistry.getInstance().iocStartup(MODULES);
public async init(mocks: Record<string, NoArgsClass> = {}): Promise<void> {
log.info("Initializing IoC container");
this.#initializeIoc(mocks);
log.info(`Initializing global middleware (${GLOBAL_MIDDLEWARES.length})`);
this.#initializeGlobalMiddlewares();
log.info("Initializing routes");
this.#initializeRoutes();
log.info("Initializing global error handler");
this.#initializeErrorHandlerMiddleware();
log.info("Connecting to database");
await this.#initializeDatabase();
log.info("App successfully initialized!");
}

public async start(): Promise<void> {
public async startListening(): Promise<void> {
return new Promise(resolve => {
log.info("Server connecting...");
this.expressApp.listen(this.port, () => {
this.logger.table({
title: `[Express] MERN Sample App v${this.environment.vars.PACKAGE_JSON_VERSION}`,
logTable({
title: `[Express] ${env.APP_NAME} v${env.PACKAGE_JSON_VERSION}`,
data: {
"🟢 NodeJS": process.version,
"🏠 Env": this.env,
"📝 Swagger": `/${this.swaggerPath}`,
"🏠 Env": env.NODE_ENV,
"📝 Swagger": SWAGGER_PATH,
"🆔 PID": `${process.pid}`,
"🧠 Memory": `${Math.round((process.memoryUsage().heapUsed / 1024 / 1024) * 100) / 100} MB`,
"🧠 Memory": this.memoryUsage,
"📅 Started": new Date().toLocaleString(),
"🔑 Keycloak": this.keycloakUrl ?? "-",
},
});
this.logger.logger.info(`🚀 App listening on port ${this.port}`);
log.info(`🚀 App listening on port ${this.port}`);
resolve();
});
});
}

async #initializeDatabase() {
try {
const { MONGO_URL } = Environment.getInstance().vars;
const MONGO_OPTIONS: MongoClientOptions = {};
this.mongoClient = new MongoClient(MONGO_URL, MONGO_OPTIONS);
await this.mongoClient.connect();
} catch (error) {
console.log(error);
}
this.#mongoClient = new MongoClient(env.MONGO_URL, {});
await this.#mongoClient.connect();
}

#initializeErrorHandlerMiddleware() {
const errorHandler: ErrorRequestHandler = (err: ErrorResponse, req, res, next) => {
if (res.headersSent) return next(err);
log.warn(`Headers sent before reaching main error handler`, err);
res.status(err.content.status).json(err.content);
};
this.expressApp.use(errorHandler);
}

#initializeIoc(mocks: Record<string, NoArgsClass>) {
const modules: Record<string, NoArgsClass> = {};
Object.entries(MODULES).forEach(([key, value]) => (modules[key.toLowerCase()] = value));
Object.entries(mocks).forEach(([key, value]) => (modules[key.toLowerCase()] = value));
ServiceRegistry.getInstance().iocStartup(modules);
}

#initializeGlobalMiddlewares() {
this.expressApp.use(keycloak.middleware());
GLOBAL_MIDDLEWARES.forEach(middleware => {
this.expressApp.use(middleware);
GLOBAL_MIDDLEWARES.forEach(middlewareFactory => {
this.expressApp.use(middlewareFactory());
});
}

Expand All @@ -87,34 +104,4 @@ export class App {
const router = s.router(CONTRACTS, RouterCollection.getInstance().getRouters());
suppressConsole(() => createExpressEndpoints(CONTRACTS, router, this.expressApp));
}

#initializeSwagger() {
const apiDoc: Parameters<typeof generateOpenApi>[1] = {
info: {
title: "REST API",
license: {
name: "MIT",
url: "https://spdx.org/licenses/MIT.html",
},
termsOfService: "http://swagger.io/terms/",
contact: {
email: "",
name: "",
url: "",
},
version: Environment.getInstance().vars.PACKAGE_JSON_VERSION,
description: "This is a dynamically generated Swagger API documentation",
},
};

const openApiDocument = generateOpenApi(CONTRACTS, apiDoc, { operationMapper });

this.expressApp.use(
"/api-docs",
swaggerUi.serve,
swaggerUi.setup(openApiDocument, {
customCssUrl: "/css/swagger.css",
}),
);
}
}
Loading

0 comments on commit 1df9236

Please sign in to comment.