Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
7199db1
Create monorepo (#37)
bennettrwu Oct 23, 2025
8b57b9c
feat: create base-schema lib for defining shared api schema
bennettrwu Oct 23, 2025
559be16
feat(base-schema): define shared api error reply schema
bennettrwu Oct 23, 2025
c490820
feat(session-service-schema): define healthcheck api schema
bennettrwu Oct 23, 2025
0d44991
feat(base-schema,session-service-service): add interface so schema li…
bennettrwu Oct 23, 2025
11df1ba
feat(session-manager-schema): create toy calculator schema to demo usage
bennettrwu Oct 23, 2025
8c21302
build(session-manager): update watched dependencies
bennettrwu Oct 23, 2025
35666f1
feat(base-fastify-server): create lib that defines base configuration…
bennettrwu Oct 23, 2025
9d973f4
feat(base-fastify-server): create logger factory
bennettrwu Oct 23, 2025
5927b17
feat(base-fastify-server): create fastify server factory that creates…
bennettrwu Oct 23, 2025
84fe234
feat(base-fastify-server): create depdency container and provide to f…
bennettrwu Oct 23, 2025
bdc82fb
feat(base-fastify-server): define set of httperrors defined by shared…
bennettrwu Oct 23, 2025
69bbde5
feat(base-fastify-server): add typebox schema validator
bennettrwu Oct 23, 2025
bf263f6
test(base-fastify-server): add unit tests for typebox schema validator
bennettrwu Oct 23, 2025
d4d2f96
feat(base-fastify-server): add custom json parser
bennettrwu Oct 23, 2025
d39026d
test(base-fastify-server): add unit tests for custom json parser
bennettrwu Oct 23, 2025
388dccd
feat(base-fastify-server): create custom error handler
bennettrwu Oct 23, 2025
c3b66d0
test(base-fastify-server): create unit tests for custom error handler
bennettrwu Oct 23, 2025
ef4abbe
feat(base-fastify-server): create handler for not found event
bennettrwu Oct 23, 2025
0269db5
test(base-fastify-server): create unit tests for custom not found han…
bennettrwu Oct 23, 2025
38f899c
feat(base-fastify-server): add on request handler to scope logger cor…
bennettrwu Oct 23, 2025
8d2bef4
test(base-fastify-server): create unit tests for scope logger on requ…
bennettrwu Oct 23, 2025
8a94a76
feat(base-fastify-server): export public artifacts
bennettrwu Oct 23, 2025
29ee026
test(base-fastify-server): write integration test for dependency inje…
bennettrwu Oct 23, 2025
098b3c3
test(base-fastify-server): write integration test for server error ha…
bennettrwu Oct 23, 2025
56b86e5
test(base-fastify-server): write integration test for server request …
bennettrwu Oct 23, 2025
fbb895f
build(session-manager): update dockerfile with new dependency
bennettrwu Oct 23, 2025
be8cffb
feat(session-manager): define app config loader and provider
bennettrwu Oct 23, 2025
4318154
docs(session-manager): add template.env to document config schema
bennettrwu Oct 23, 2025
9599abd
feat(session-manager): setup fastify server and entrypoint
bennettrwu Oct 23, 2025
56ff44e
feat(session-manager): load swagger ui plugin is in development mode
bennettrwu Oct 23, 2025
637f58b
feat(session-manager): register app dependencies with container
bennettrwu Oct 23, 2025
4f49326
feat(session-manager): define healthcheck endpoint
bennettrwu Oct 23, 2025
5a75e06
test(session-manager): write unit tests for healthcheck controller
bennettrwu Oct 23, 2025
98b28d6
test(session-manager): write integration test for healthcheck endpoints
bennettrwu Oct 23, 2025
a628e7e
test(session-manager): create integration tests for api-docs endpoint
bennettrwu Oct 23, 2025
5b03667
feat(session-manager): implement toy calculator route to demo boilerp…
bennettrwu Oct 23, 2025
d33d06d
test(session-manager): write unit tests for calculator controller and…
bennettrwu Oct 23, 2025
6548dde
test(session-manager): write integration tests for calculator endpoints
bennettrwu Oct 23, 2025
cd46216
build: enable pretty print for logging in development mode
bennettrwu Oct 23, 2025
42e1a82
build(session-manager): update docker container with new dependencies
bennettrwu Oct 23, 2025
d69f501
Merge branch 'multi-tenancy' into fastify-boilerplate
bennettrwu Oct 23, 2025
4fab9a5
build(npm): update package-lock
bennettrwu Oct 23, 2025
0093ae8
ci(base-fastify-server): add coverage reporting
bennettrwu Oct 23, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/node-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ jobs:
working-directory: "."
run: npm run test

- name: Code Coverage Report - Base Fastify Server
uses: ./.github/actions/coverage-report
with:
package_name: "Base Fastify Server"
filename: "./libs/base-fastify-server/coverage/cobertura-coverage.xml"

- name: Code Coverage Report - Session Manager
uses: ./.github/actions/coverage-report
with:
Expand Down
23 changes: 23 additions & 0 deletions apps/session-manager/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ COPY package*.json .

ARG SRC="session-manager"
COPY apps/${SRC}/package*.json apps/${SRC}/
ARG LIB="base-fastify-server"
COPY libs/${LIB}/package*.json libs/${LIB}/
ARG LIB="base-schema"
COPY libs/${LIB}/package*.json libs/${LIB}/
ARG LIB="session-manager-schema"
COPY libs/${LIB}/package*.json libs/${LIB}/

Expand All @@ -23,6 +27,10 @@ COPY tsconfig.base.json .

ARG SRC="session-manager"
COPY apps/${SRC} apps/${SRC}
ARG LIB="base-fastify-server"
COPY libs/${LIB} libs/${LIB}
ARG LIB="base-schema"
COPY libs/${LIB} libs/${LIB}
ARG LIB="session-manager-schema"
COPY libs/${LIB} libs/${LIB}

Expand All @@ -39,6 +47,10 @@ COPY package*.json .

ARG SRC="session-manager"
COPY apps/${SRC}/package*.json apps/${SRC}/
ARG LIB="base-fastify-server"
COPY libs/${LIB}/package*.json libs/${LIB}/
ARG LIB="base-schema"
COPY libs/${LIB}/package*.json libs/${LIB}/
ARG LIB="session-manager-schema"
COPY libs/${LIB}/package*.json libs/${LIB}/

Expand All @@ -47,9 +59,20 @@ RUN npm ci --omit=dev
# Copy the build artifacts from build-env container
ARG SRC="session-manager"
COPY --from=build-env /app/apps/${SRC}/dist/src/ /app/apps/${SRC}/dist/src/
ARG LIB="base-fastify-server"
COPY --from=build-env /app/libs/${LIB}/dist/src/ /app/libs/${LIB}/dist/src/
ARG LIB="base-schema"
COPY --from=build-env /app/libs/${LIB}/dist/src/ /app/libs/${LIB}/dist/src/
ARG LIB="session-manager-schema"
COPY --from=build-env /app/libs/${LIB}/dist/src/ /app/libs/${LIB}/dist/src/

WORKDIR /app/apps/session-manager

# Ensure server listens on a defined interface and port inside container
# This docker can map this to a real port on the host
ENV HOST=0.0.0.0
ENV PORT=80

HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --start-interval=1s --retries=3 CMD curl -f http://localhost:80/healthcheck || exit 1

CMD ["npm", "start"]
12 changes: 10 additions & 2 deletions apps/session-manager/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,21 @@
"scripts": {
"build": "tsc --build",
"start": "node ./dist/src/index.js",
"start:dev": "node ./dist/src/index.js --dev",
"dev": "nodemon --exec 'npm run build && npm run start:dev' --watch src --watch ../../libs/session-manager-schema/src src/index.ts",
"start:dev": "node ./dist/src/index.js --dev | pino-pretty",
"dev": "nodemon --exec 'npm run build && npm run start:dev' --watch src --watch ../../libs/base-fastify-server/src --watch ../../libs/base-schema/src --watch ../../libs/session-manager-schema/src src/index.ts",
"format": "prettier --check ./src ./tests",
"format:fix": "prettier --write ./src ./tests",
"lint": "eslint ./src ./tests",
"lint:fix": "eslint ./src ./tests --fix",
"test:dev": "vitest --ui --coverage",
"test": "vitest run"
},
"dependencies": {
"@fastify/awilix": "8.0.0",
"@fastify/swagger": "9.5.2",
"@fastify/swagger-ui": "5.2.3",
"awilix": "12.0.5",
"env-schema": "6.1.0",
"typebox": "1.0.41"
}
}
52 changes: 52 additions & 0 deletions apps/session-manager/src/app_config/app_config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import envSchema from 'env-schema';
import { Type } from 'typebox';
import type { Static } from 'typebox';

import { LogLevel } from '@scribear/base-fastify-server';

const CONFIG_SCHEMA = Type.Object({
LOG_LEVEL: Type.Enum(LogLevel),
PORT: Type.Integer({ minimum: 0, maximum: 65_535 }),
HOST: Type.String(),
});

/**
* Class that loads and provides application configuration
*/
class AppConfig {
private _isDevelopment: boolean;
private _logLevel: LogLevel;
private _port: number;
private _host: string;

get isDevelopment() {
return this._isDevelopment;
}

get logLevel() {
return this._logLevel;
}

get port() {
return this._port;
}

get host() {
return this._host;
}

constructor(path?: string) {
this._isDevelopment = process.argv.includes('--dev');

const env = envSchema<Static<typeof CONFIG_SCHEMA>>({
dotenv: path ? { path, quiet: true } : { quiet: true },
schema: CONFIG_SCHEMA,
});

this._logLevel = env.LOG_LEVEL;
this._port = env.PORT;
this._host = env.HOST;
}
}

export default AppConfig;
32 changes: 30 additions & 2 deletions apps/session-manager/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,31 @@
import '@scribear/session-manager-schema';
import AppConfig from './app_config/app_config.js';
import createServer from './server/create_server.js';

console.log('Hello from session-manager!');
/**
* Main entrypoint for session manager server
*/
async function main() {
const config = new AppConfig();
const { logger, fastify } = await createServer(config);

// Handle uncaught exceptions and rejections
process.on('uncaughtException', (err) => {
logger.fatal({ msg: 'Uncaught exception', err });
throw err; // terminate on uncaught errors
});

process.on('unhandledRejection', (reason) => {
const err = Error('Unhandled rejection', { cause: reason });
logger.fatal({ msg: 'Unhandled rejection', err });
throw err; // terminate on uncaught rejection
});

try {
await fastify.listen({ port: config.port, host: config.host });
} catch (err) {
logger.fatal({ msg: 'Failed to start fastify webserver', err });
throw err; // terminate if failed to start
}
}

await main();
33 changes: 33 additions & 0 deletions apps/session-manager/src/server/create_server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { createBaseServer } from '@scribear/base-fastify-server';

import type AppConfig from '../app_config/app_config.js';
import registerDependencies from './dependency_injection/register_dependencies.js';
import calculatorRouter from './features/calculator/calculator.router.js';
import healthcheckRouter from './features/healthcheck/healthcheck.router.js';
import swagger from './plugins/swagger.js';

/**
* Initializes fastify server and registers dependencies
* @param config Application config
* @returns Initialized fastify server
*/
async function createServer(config: AppConfig) {
const { logger, dependencyContainer, fastify } = createBaseServer(
config.logLevel,
);

// Only include swagger docs if in development mode
if (config.isDevelopment) {
await fastify.register(swagger);
}

registerDependencies(dependencyContainer, config);

// Register routes
fastify.register(healthcheckRouter);
fastify.register(calculatorRouter);

return { logger, fastify };
}

export default createServer;
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Need to import so that declare module '@fastify/awilix' below works
import '@fastify/awilix';
import { type AwilixContainer, Lifetime, asClass, asValue } from 'awilix';

import type { BaseDependencies } from '@scribear/base-fastify-server';

import type AppConfig from '../../app_config/app_config.js';
import CalculatorController from '../features/calculator/calculator.controller.js';
import CalculatorService from '../features/calculator/calculator.service.js';
import HealthcheckController from '../features/healthcheck/healthcheck.controller.js';

/**
* Define types for entities in dependency container
*/
interface AppDependencies extends BaseDependencies {
config: AppConfig;

// Healthcheck
healthcheckController: HealthcheckController;

// Calculator
calculatorController: CalculatorController;
calculatorService: CalculatorService;
}

/**
* Ensure fastify awilix container is typed correctly
* @see https://github.com/fastify/fastify-awilix?tab=readme-ov-file#typescript-usage
*/
declare module '@fastify/awilix' {
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
interface Cradle extends AppDependencies {}

// eslint-disable-next-line @typescript-eslint/no-empty-object-type
interface RequestCradle extends AppDependencies {}
}

/**
* Register all controller, service, and repository classes into dependency container
* @param dependencyContainer Container to load dependencies into
* @param config AppConfig to be registered into dependency controller
*/
function registerDependencies(
dependencyContainer: AwilixContainer,
config: AppConfig,
) {
dependencyContainer.register({
// Config
config: asValue(config),

// Healthcheck
healthcheckController: asClass(HealthcheckController, {
lifetime: Lifetime.SCOPED,
}),

// Calculator
calculatorController: asClass(CalculatorController, {
lifetime: Lifetime.SCOPED,
}),
calculatorService: asClass(CalculatorService, {
lifetime: Lifetime.SCOPED,
}),
});
}

export default registerDependencies;
export type { AppDependencies };
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { FastifyReply, FastifyRequest } from 'fastify';

import type { AppDependencies } from './register_dependencies.js';

/**
* Creates a wrapper function around provided controller route handler
* Wrapper function resolves controller from request dependency container scope
* and passes request/reply to handler
* @param controller Name of controller to resolve
* @param method Name of method on controller to call
* @returns Wrapped controller method
*/
function resolveHandler<
C extends keyof AppDependencies,
M extends keyof AppDependencies[C],
>(controller: C, method: M): AppDependencies[C][M] {
const wrapper = async (req: FastifyRequest, res: FastifyReply) => {
const routeController = req.diScope.resolve(
controller,
) as AppDependencies[C];

// Throw exception if method is not a function
if (
!(method in routeController) ||
typeof routeController[method] !== 'function'
) {
throw new Error(
`Failed to resolve handler: Property '${String(method)}' on controller '${controller}' is not a function.`,
);
}

const handler = routeController[method].bind(routeController) as (
req: FastifyRequest,
res: FastifyReply,
) => unknown;

return await handler(req, res);
};

// Cast the type of the wrapper to be the same as the wrapped handler
return wrapper as AppDependencies[C][M];
}

export default resolveHandler;
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type {
BaseFastifyReply,
BaseFastifyRequest,
} from '@scribear/base-fastify-server';
import {
COMPUTE_BINOMIAL_SCHEMA,
type COMPUTE_MONOMIAL_SCHEMA,
} from '@scribear/session-manager-schema';

import type { AppDependencies } from '../../dependency_injection/register_dependencies.js';
import type CalculatorService from './calculator.service.js';

class CalculatorController {
private _calculatorService: CalculatorService;

constructor(calculatorService: AppDependencies['calculatorService']) {
this._calculatorService = calculatorService;
}

binomial(
req: BaseFastifyRequest<typeof COMPUTE_BINOMIAL_SCHEMA>,
res: BaseFastifyReply<typeof COMPUTE_BINOMIAL_SCHEMA>,
) {
const { a, b, op } = req.body;

const result = this._calculatorService.binomial(a, b, op);

res.code(200).send({ result, reqId: req.id });
}

monomial(
req: BaseFastifyRequest<typeof COMPUTE_MONOMIAL_SCHEMA>,
res: BaseFastifyReply<typeof COMPUTE_MONOMIAL_SCHEMA>,
) {
const { a, op } = req.body;

const result = this._calculatorService.monomial(a, op);

res.code(200).send({ result, reqId: req.id });
}
}

export default CalculatorController;
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { BaseFastifyInstance } from '@scribear/base-fastify-server';
import {
COMPUTE_BINOMIAL_ROUTE,
COMPUTE_BINOMIAL_SCHEMA,
COMPUTE_MONOMIAL_ROUTE,
COMPUTE_MONOMIAL_SCHEMA,
} from '@scribear/session-manager-schema';

import resolveHandler from '../../dependency_injection/resolve_handler.js';

/**
* Registers calculator demo routes
* @param fastify Fastify app instance
*/
function calculatorRouter(fastify: BaseFastifyInstance) {
fastify.route({
...COMPUTE_BINOMIAL_ROUTE,
schema: COMPUTE_BINOMIAL_SCHEMA,
handler: resolveHandler('calculatorController', 'binomial'),
});

fastify.route({
...COMPUTE_MONOMIAL_ROUTE,
schema: COMPUTE_MONOMIAL_SCHEMA,
handler: resolveHandler('calculatorController', 'monomial'),
});
}

export default calculatorRouter;
Loading