Skip to content

Commit

Permalink
chore: more overall improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
brunotot committed Sep 17, 2024
1 parent c81facb commit a9589d3
Show file tree
Hide file tree
Showing 46 changed files with 458 additions and 443 deletions.
18 changes: 17 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,22 @@
"ignorePatterns": ["node_modules/", "dist/", "docs/", "assets/"],
"rules": {
"@typescript-eslint/consistent-type-imports": "error",
"@typescript-eslint/ban-ts-comment": "off"
"@typescript-eslint/ban-ts-comment": "off",
"no-console": ["error", { "allow": ["warn", "error"] }],
"@typescript-eslint/member-ordering": [
"error",
{
"default": [
// Order as per your requirements:
"private-static-field",
"private-instance-field",
"public-static-field",
"public-instance-field",
"constructor",
"public-instance-method",
"private-instance-method"
]
}
]
}
}
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
"pnpm": ">=8.15.7"
},
"scripts": {
"prepare": "npm run build",
"test": "npm run app-node-express:test && npm run app-vite-react:test && npm run lib-commons:test && npm run lib-api-client:test",
"build": "npm run lib-commons:build && npm run lib-api-client:build && npm run app-node-express:build && npm run app-vite-react:build",
"clean": "rm -rf docs && npm run app-node-express:clean && npm run app-vite-react:clean && npm run lib-commons:clean && npm run lib-api-client:clean",
Expand Down
2 changes: 1 addition & 1 deletion packages/mern-sample-app/app-node-express/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,4 @@ Provides various decorators to enrich the controllers and services with addition

//"test": "vitest",
//"build": "rm -rf dist && npm run compile:ts",
//"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",
//"start": "export SERVER_VERSION=$(grep -o '\"version\": *\"[^\"]*\"' package.json | awk -F'\"' '{print $4}') && node --no-warnings --loader ts-node/esm --experimental-specifier-resolution=node ./dist/main.js",
2 changes: 1 addition & 1 deletion packages/mern-sample-app/app-node-express/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"main": "dist/main.js",
"type": "module",
"scripts": {
"test": "npm run build --silent && export PACKAGE_JSON_VERSION=$(grep -o '\"version\": *\"[^\"]*\"' package.json | awk -F'\"' '{print $4}') && vitest --run",
"test": "npm run build --silent && export SERVER_VERSION=$(grep -o '\"version\": *\"[^\"]*\"' package.json | awk -F'\"' '{print $4}') && vitest --run",
"build": "rm -rf dist && tsc --build && tsc-alias -p tsconfig.prod.json",
"start": "bash start.sh",
"dev": "nodemon",
Expand Down
36 changes: 20 additions & 16 deletions packages/mern-sample-app/app-node-express/src/ExpressApp.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import express from "express";
/* eslint-disable no-console */

import type { RouteMiddlewareFactory } from "@org/app-node-express/lib/@ts-rest";
import type { Class } from "@org/lib-commons";
import type { MongoClient } from "@org/app-node-express/lib/mongodb";

import express from "express";
import { initializeExpressRoutes, initializeSwagger } from "@org/app-node-express/lib/@ts-rest";
import { MongoDatabaseService } from "@org/app-node-express/lib/mongodb";
import { getTypedError } from "@org/lib-api-client";
Expand All @@ -18,9 +19,9 @@ export type ExpressAppConfig = Partial<{

export class ExpressApp {
public readonly expressApp: express.Application;
public readonly port: string;
public readonly port: number;
public readonly url: string;
public readonly keycloakUrl?: string;
public readonly keycloakUrl: string;
public readonly middleware: RouteMiddlewareFactory[];
public readonly modules: Record<string, Class>;

Expand All @@ -32,7 +33,7 @@ export class ExpressApp {
this.modules = config.modules ?? {};
this.expressApp = express();
this.keycloakUrl = env.KEYCLOAK_URL;
this.port = env.PORT;
this.port = env.SERVER_PORT;
this.url = env.SERVER_URL;
}

Expand All @@ -48,7 +49,10 @@ export class ExpressApp {
return `${Math.round((process.memoryUsage().heapUsed / 1024 / 1024) * 100) / 100} MB`;
}

public async init(mocks: Record<string, Class> = {}): Promise<void> {
public async init(
mocks: Record<string, Class> = {},
onReady?: (app: ExpressApp) => void,
): Promise<void> {
log.info("Initializing Swagger");
this.#initializeSwagger();
log.info("Initializing IoC container");
Expand All @@ -62,22 +66,23 @@ export class ExpressApp {
log.info("Connecting to database");
await this.#initializeDatabase();
log.info("App successfully initialized!");
onReady?.(this);
}

public async startListening(): Promise<void> {
return new Promise(resolve => {
log.info("Server connecting...");
this.expressApp.listen(this.port, () => {
this.#logTable({
title: `[Express] ${env.APP_NAME} v${env.PACKAGE_JSON_VERSION}`,
title: `[Express] ${env.SERVER_NAME} v${env.SERVER_VERSION}`,
data: {
"🟢 NodeJS": process.version,
"🏠 Env": env.NODE_ENV,
"📝 Swagger": env.SWAGGER_ENDPOINT,
"🏠 Env": env.SERVER_ENV,
"🔑 Keycloak": this.keycloakUrl,
"📝 Swagger": env.TS_REST_SWAGGER_ENDPOINT,
"🆔 PID": `${process.pid}`,
"🧠 Memory": this.memoryUsage,
"📅 Started": new Date().toLocaleString(),
"🔑 Keycloak": this.keycloakUrl ?? "-",
},
});
log.info(`🚀 App listening on port ${this.port}`);
Expand Down Expand Up @@ -113,11 +118,11 @@ export class ExpressApp {
#initializeSwagger() {
initializeSwagger({
app: this.expressApp,
oauth2RedirectUrl: `${this.url}${env.SWAGGER_ENDPOINT}${env.SWAGGER_OAUTH2_REDIRECT_ENDPOINT}`,
version: env.PACKAGE_JSON_VERSION,
endpoint: env.SWAGGER_ENDPOINT,
cssPath: env.SWAGGER_CSS_PATH,
jsPath: env.SWAGGER_JS_PATH,
oauth2RedirectUrl: `${this.url}${env.TS_REST_SWAGGER_ENDPOINT}${env.TS_REST_SWAGGER_OAUTH2_REDIRECT_ENDPOINT}`,
version: env.SERVER_VERSION,
endpoint: env.TS_REST_SWAGGER_ENDPOINT,
cssPath: env.TS_REST_SWAGGER_CSS_PATH,
jsPath: env.TS_REST_SWAGGER_JS_PATH,
});
}

Expand Down Expand Up @@ -173,15 +178,14 @@ export class ExpressApp {

const containerWidth = Math.max(title.length, ...keyValueLengths) + padding * 2;

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

const content = Object.entries(data).map(([key, value]) => {
const keyPadding = " ".repeat(maxKeyLength - key.length);
const text = `${key}${keyPadding}${hrY}${value}`;
const remainder = " ".repeat(containerWidth - text.length - spacer.length * 2);
return `│${spacer}${text}${remainder}${spacer}│`;
});

const hrX = `${"─".repeat(containerWidth)}`;
console.info(`┌${hrX}┐`);
console.info(`│${center(title, containerWidth)}│`);
console.info(`├${hrX}┤`);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { IocServiceDecoratorMetadataEntry } from "@org/app-node-express/lib/bottlejs";
import { iocRegistry } from "@org/app-node-express/lib/bottlejs";

export function autowired(_target: undefined, context: ClassFieldDecoratorContext) {
const componentName = String(context.name).toLowerCase();
IocServiceDecoratorMetadataEntry.for(context).addDependency(componentName);
export function autowired(name?: string) {
return function (_target: undefined, context: ClassFieldDecoratorContext) {
const fieldName = String(context.name);
const computedName = name?.toLowerCase() ?? fieldName.toLowerCase();
IocServiceDecoratorMetadataEntry.for(context).addDependency(computedName);

return function () {
return iocRegistry.inject(componentName);
return function () {
return iocRegistry.inject(computedName);
};
};
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Class } from "@org/lib-commons";
import type { NoArgsClass } from "@org/lib-commons";

// @ts-expect-error Stage 3 decorators polyfill.
Symbol.metadata ??= Symbol("Symbol.metadata");
Expand All @@ -10,7 +10,7 @@ declare global {
}
}

export type DecoratorMetadataInjectType = Class | DecoratorContext;
export type DecoratorMetadataInjectType = NoArgsClass | DecoratorContext;

export class DecoratorMetadata {
public static for(target: DecoratorMetadataInjectType) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,16 @@ export function contract<const Route extends AppRoute, This, Fn extends RouteHan

return function (target: Fn, context: ClassMethodDecoratorContext<This, Fn>) {
async function handler(data: unknown): Promise<unknown> {
const session = MongoDatabaseService.getInstance().client.startSession();
const databaseService = MongoDatabaseService.getInstance();
const session = databaseService.client.startSession();
try {
MongoDatabaseService.getInstance().startTransaction(session);
databaseService.startTransaction(session);
const container = iocRegistry.inject(context);
const result = await target.call(container, data as RouteInput<Route>);
await MongoDatabaseService.getInstance().commitTransaction(session);
await databaseService.commitTransaction(session);
return result;
} catch (error: unknown) {
await MongoDatabaseService.getInstance().rollbackTransaction(session);
await databaseService.rollbackTransaction(session);
const typedError = getTypedError(error);
return { status: typedError.content.status, body: typedError.content };
} finally {
Expand Down
12 changes: 12 additions & 0 deletions packages/mern-sample-app/app-node-express/src/decorators/inject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { IocServiceDecoratorMetadataEntry } from "@org/app-node-express/lib/bottlejs";
import { type NoArgsClass } from "@org/lib-commons";

export function inject<T extends NoArgsClass>(name?: string) {
return function (target: T, context: ClassDecoratorContext<T>) {
const className = String(context.name);
const computedName = name?.toLowerCase() ?? className.toLowerCase();
const iocServiceDecoratorMetadataEntry = IocServiceDecoratorMetadataEntry.for(context);
iocServiceDecoratorMetadataEntry.setName(computedName);
iocServiceDecoratorMetadataEntry.setClass(target);
};
}
85 changes: 43 additions & 42 deletions packages/mern-sample-app/app-node-express/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,53 +5,51 @@
import { z } from "@org/lib-commons";
import dotenv from "dotenv";
import path from "path";
initDotenv();

// prettier-ignore
const ENVIRONMENT_VARS = z.object({
PACKAGE_JSON_VERSION: z.string().default("?.?.?"),
NODE_ENV: z.string(),
SWAGGER_ENDPOINT: z
.string()
.default("/api-docs")
.transform(s => (s.startsWith("/") ? s : `/${s}`)),
APP_NAME: z.string().default("app-node-express"),
PORT: z.string(),
ORIGIN: z.string().default("*"),
CREDENTIALS: z
.string()
.default("true")
.transform(s => s === "true"),
MONGO_URL: z.string(),
MONGO_DATABASE: z.string(),
KEYCLOAK_URL: z.string().optional(),
KEYCLOAK_SSL_REQUIRED: z.string().optional(),
KEYCLOAK_REALM: z.string().optional(),
KEYCLOAK_ADMIN_CLI_ID: z.string().optional(),
KEYCLOAK_ADMIN_CLI_SECRET: z.string().optional(),
KEYCLOAK_CONFIDENTIAL_PORT: z.string().optional(),
ALLOWED_ORIGINS: z
.string()
.default("http://localhost:5173")
.transform(s => s.split(",")),
SWAGGER_CSS_PATH: z.string().default("/css/swagger.css"),
SWAGGER_JS_PATH: z.string().default("/js/swagger.js"),
SWAGGER_OAUTH2_REDIRECT_ENDPOINT: z.string().default("/oauth2-redirect.html"),
KEYCLOAK_AUTHORIZATION_ENDPOINT: z
.string()
.default("/realms/master/protocol/openid-connect/auth"),
// Express server
SERVER_DOMAIN: z.string(),
SERVER_SESSION_SECRET: z.string(),
SERVER_VERSION: z.string().default("?.?.?"),
SERVER_ENV: z.string().default("development"),
SERVER_NAME: z.string().default("app-node-express"),
SERVER_PORT: z.string().default("8081").transform(v => Number(v)),

// mongodb
DATABASE_URL: z.string(),
DATABASE_NAME: z.string().default("development"),

// keycloak
KEYCLOAK_URL: z.string(),
KEYCLOAK_ADMIN_CLI_SECRET: z.string(),
KEYCLOAK_ADMIN_CLI_ID: z.string().default("admin-cli"),
KEYCLOAK_REALM: z.string().default("master"),
KEYCLOAK_AUTHORIZATION_ENDPOINT: z.string().default("/realms/master/protocol/openid-connect/auth"),
KEYCLOAK_SSL_REQUIRED: z.string().default("none"),
KEYCLOAK_CONFIDENTIAL_PORT: z.string().default("0"),
KEYCLOAK_BEARER_ONLY: z.string().default("true").transform(v => v === "true"),

// @ts-rest
TS_REST_SWAGGER_ENDPOINT: z.string().default("/api-docs").transform(s => (s.startsWith("/") ? s : `/${s}`)),
TS_REST_SWAGGER_CSS_PATH: z.string().default("/css/swagger.css"),
TS_REST_SWAGGER_JS_PATH: z.string().default("/js/swagger.js"),
TS_REST_SWAGGER_OAUTH2_REDIRECT_ENDPOINT: z.string().default("/oauth2-redirect.html"),

// cors
CORS_ALLOWED_ORIGINS: z.string().transform(s => s.split(",")),
CORS_CREDENTIALS: z.string().default("true").transform(s => s === "true"),
CORS_ALLOWED_METHODS: z.string().default("GET,POST,PUT,DELETE,PATCH").transform(s => s.split(",")),
CORS_ALLOWED_HEADERS: z.string().default("*").transform(s => s.split(",")),
});

export const env = parseEnvironmentVars();

function initDotenv() {
dotenv.config({
path: path.join(process.cwd(), `.env.${process.env.NODE_ENV ?? "development"}.local`),
});
}

function parseEnvironmentVars() {
configLocalDotenv();
const typedEnvData = filterEnvBySchema();
const parsedResults = ENVIRONMENT_VARS.safeParse(typedEnvData);

if (!parsedResults.success) {
const currentIssue = parsedResults.error.issues[0];
const { path, ...rest } = currentIssue;
Expand All @@ -61,11 +59,8 @@ function parseEnvironmentVars() {
const errorMessage = `${errorTitle}\n${errorJson}`;
throw new Error(errorMessage);
}
const serverUrl: string =
process.env.NODE_ENV === "development"
? `http://localhost:${process.env.PORT}`
: `https://${process.env.RAILWAY_PUBLIC_DOMAIN}:${process.env.PORT}`;

const serverUrl = `${process.env.SERVER_DOMAIN}:${process.env.SERVER_PORT}`;
return { ...parsedResults.data, SERVER_URL: serverUrl } as const;
}

Expand All @@ -77,3 +72,9 @@ function filterEnvBySchema() {
return acc;
}, {});
}

function configLocalDotenv() {
dotenv.config({
path: path.join(process.cwd(), `.env.${process.env.SERVER_ENV ?? "development"}.local`),
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import { type RouteInput, type RouteOutput } from "@org/app-node-express/lib/@ts
import { autowired } from "@org/app-node-express/decorators/autowired";
import { contract } from "@org/app-node-express/decorators/contract";
import { type UserService } from "../service/UserService";
import { inject } from "@org/app-node-express/decorators/inject";

@inject()
export class UserController {
@autowired userService: UserService;
@autowired() userService: UserService;

@contract({ contract: contracts.User.findOneByUsername, roles: ["admin"] })
async findOneByUsername(
Expand All @@ -33,28 +35,32 @@ export class UserController {
async findAllPaginated(
payload: RouteInput<typeof contracts.User.findAllPaginated>,
): RouteOutput<typeof contracts.User.findAllPaginated> {
// eslint-disable-next-line no-console
console.log(payload);
return {
status: 200,
body: (await this.userService.search(payload.query.paginationOptions)) as TODO,
body: {} as TODO,
};
}

@contract({ contract: contracts.User.createOne })
async createOne(
payload: RouteInput<typeof contracts.User.createOne>,
): RouteOutput<typeof contracts.User.createOne> {
const user = await this.userService.create(payload.body);
// eslint-disable-next-line no-console
console.log(payload);
return {
status: 201,
body: user,
body: {} as TODO,
};
}

@contract({ contract: contracts.User.deleteByUsername })
async deleteByUsername(
payload: RouteInput<typeof contracts.User.deleteByUsername>,
): RouteOutput<typeof contracts.User.deleteByUsername> {
await this.userService.deleteByUsername(payload.body.username);
// eslint-disable-next-line no-console
console.log(payload);
return {
status: 201,
body: "OK",
Expand Down
Loading

0 comments on commit a9589d3

Please sign in to comment.