Compare commits

..

2 Commits
1.6.0 ... 1.7.0

Author SHA1 Message Date
5081819281 chore(release): 1.7.0
Some checks failed
Build release images / build-container (push) Failing after 8s
2026-02-20 20:19:31 +01:00
240bd9cba1 refactor: Bun by default 2026-02-20 20:16:37 +01:00
14 changed files with 3411 additions and 137 deletions

4
.gitignore vendored
View File

@@ -126,8 +126,12 @@ dist
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# Old package manager lockfiles (Bun migration - keep bun.lock)
yarn.lock
package-lock.json
pnpm-lock.yaml
build
*.sqlite

View File

@@ -2,10 +2,17 @@
All notable changes to this project will be documented in this file. Dates are displayed in UTC.
#### [1.7.0](https://git.odit.services/lfk/backend/compare/1.6.0...1.7.0)
- refactor: Bun by default [`240bd9c`](https://git.odit.services/lfk/backend/commit/240bd9cba10636bfc100ea2732508d805639f105)
#### [1.6.0](https://git.odit.services/lfk/backend/compare/1.5.2...1.6.0)
> 20 February 2026
- feat(data): Added nats jetstream dependency [`bbf6ea6`](https://git.odit.services/lfk/backend/commit/bbf6ea6c0fdffa11dacdf4b9afb6160ce54e197d)
- chore(deps): Bump typescript and get rid of now legacy imports [`2da8247`](https://git.odit.services/lfk/backend/commit/2da8247978c5142eec194651a7520fa53396d762)
- chore(release): 1.6.0 [`53fb038`](https://git.odit.services/lfk/backend/commit/53fb0389cd1da2b71b82102e82fc3d30f0be3820)
- feat(nats): Implement caching for card, runner, and station entries with improved key management [`b0c6759`](https://git.odit.services/lfk/backend/commit/b0c67598132deffce697f19c83bd4826420abe76)
- feat(auth): Implement caching for scanauth [`526738e`](https://git.odit.services/lfk/backend/commit/526738e48722fffe4493102fad69f65b40fc3b49)
- refactor(scan): Implement KV-backed scan station submissions and response model [`d3e0206`](https://git.odit.services/lfk/backend/commit/d3e0206a3ccbff0e69024426bb2bf266cde30eeb)

View File

@@ -1,27 +1,23 @@
# Typescript Build
FROM registry.odit.services/hub/library/node:23.10.0-alpine3.21 AS build
ARG NPM_REGISTRY_URL=https://registry.npmjs.org
FROM registry.odit.services/hub/oven/bun:1.3.9-alpine AS build
WORKDIR /app
COPY package.json ./
COPY pnpm-workspace.yaml ./
COPY pnpm-lock.yaml ./
RUN npm config set registry $NPM_REGISTRY_URL && npm i -g pnpm@10.7
RUN mkdir /pnpm && pnpm config set store-dir /pnpm && pnpm i
COPY package.json bun.lockb* ./
RUN bun install --frozen-lockfile
COPY tsconfig.json ormconfig.js ./
COPY tsconfig.json ormconfig.js bunfig.toml ./
COPY src ./src
RUN pnpm run build \
RUN bun run build \
&& rm -rf /app/node_modules \
&& pnpm i --production --prefer-offline
&& bun install --production --frozen-lockfile
# final image
FROM registry.odit.services/hub/library/node:23.10.0-alpine3.21 AS final
FROM registry.odit.services/hub/oven/bun:1.3.9-alpine AS final
WORKDIR /app
COPY --from=build /app/package.json /app/package.json
COPY --from=build /app/pnpm-lock.yaml /app/pnpm-lock.yaml
COPY --from=build /app/pnpm-workspace.yaml /app/pnpm-workspace.yaml
COPY --from=build /app/bun.lockb* /app/
COPY --from=build /app/ormconfig.js /app/ormconfig.js
COPY --from=build /app/bunfig.toml /app/bunfig.toml
COPY --from=build /app/dist /app/dist
COPY --from=build /app/node_modules /app/node_modules
ENTRYPOINT ["node", "/app/dist/app.js"]
ENTRYPOINT ["bun", "/app/dist/app.js"]

123
README.md
View File

@@ -2,66 +2,119 @@
Backend Server
## Quickstart 🐳
> Use this to run the backend with a postgresql db in docker
## Prerequisites
1. Clone the repo or copy the docker-compose
2. Run in toe folder that contains the docker-compose file: `docker-compose up -d`
This project uses **Bun** as the runtime and package manager. Install Bun first:
```bash
# macOS/Linux
curl -fsSL https://bun.sh/install | bash
# Windows
powershell -c "irm bun.sh/install.ps1 | iex"
```
Or visit [bun.sh](https://bun.sh) for other installation methods.
## Quickstart 🐳
> Use this to run the backend with a PostgreSQL db in Docker
1. Clone the repo or copy the docker-compose
2. Run in the folder that contains the docker-compose file: `docker-compose up -d`
3. Visit http://127.0.0.1:4010/api/docs to check if the server is running
4. You can now use the default admin user (`demo:demo`)
## Dev Setup 🛠
> Local dev setup utilizing sqlite3 as the database.
> Local dev setup utilizing SQLite3 as the database and NATS for caching.
1. Rename the .env.example file to .env (you can adjust app port and other settings, if needed)
2. Install Dependencies
1. Rename the `.env.example` file to `.env` (you can adjust app port and other settings if needed)
2. Start NATS (required for KV cache):
```bash
pnpm i
docker-compose up -d nats
```
3. Start the server
3. Install dependencies:
```bash
pnpm dev
bun install
```
4. Start the server:
```bash
bun run dev
```
**Note**: Bun cannot run TypeScript source files directly due to circular TypeORM dependencies. The `dev` script automatically builds and runs the compiled output. For hot-reload during development, you may need to rebuild manually after code changes.
### Run Tests
```bash
# Run tests once (server has to run)
pnpm test
# Run tests once (server has to be running)
bun test
# Run test in watch mode (reruns on change)
pnpm test:watch
bun run test:watch
# Run test in ci mode (automaticly starts the dev server)
pnpm test:ci
# Run test in CI mode (automatically starts the dev server)
bun run test:ci
```
### Run Benchmarks
```bash
# Start the server first
bun run dev
# In another terminal:
bun run benchmark
```
### Generate Docs
```bash
pnpm docs
bun run docs
```
### Other Commands
```bash
# Build for production
bun run build
# Start production server
bun start
# Seed database with test data
bun run seed
# Export OpenAPI spec
bun run openapi:export
# Generate license report
bun run licenses:export
# Generate changelog
bun run changelog:export
```
## ENV Vars
> You can provide them via .env file or docker env vars.
> You can use the `test:ci:generate_env` package script to generate a example env (uses bs data as test server and ignores the errors).
> You can use the `test:ci:generate_env` package script to generate an example env (uses placeholder data for test server and ignores the errors).
| Name | Type | Default | Description |
| ---------------------- | ------------------ | -------------------- | -------------------------------------------------------------------------------------------------------------- |
| APP_PORT | Number | 4010 | The port the backend server listens on. Is optional. |
| DB_TYPE | String | N/A | The type of the db u want to use. It has to be supported by typeorm. Possible: `sqlite`, `mysql`, `postgresql` |
| DB_HOST | String | N/A | The db's host's ip-address/fqdn or file path for sqlite |
| DB_PORT | String | N/A | The db's port |
| DB_USER | String | N/A | The user for accessing the db |
| DB_PASSWORD | String | N/A | The user's password for accessing the db |
| DB_NAME | String | N/A | The db's name |
| NODE_ENV | String | dev | The apps env - influences debug info. Also when the env is set to "test", mailing errors get ignored. |
| POSTALCODE_COUNTRYCODE | String/CountryCode | N/A | The countrycode used to validate address's postal codes |
| PHONE_COUNTRYCODE | String/CountryCode | null (international) | The countrycode used to validate phone numers |
| SEED_TEST_DATA | Boolean | False | If you want the app to seed some example data set this to true |
| MAILER_URL | String(Url) | N/A | The mailer's base url (no trailing slash) |
| MAILER_KEY | String | N/A | The mailer's api key. |
| SELFSERVICE_URL | String(Url) | N/A | The link to selfservice (no trailing slash) |
| IMPRINT_URL | String(Url) | /imprint | The link to a imprint page for the system (Defaults to the frontend's imprint) |
| PRIVACY_URL | String(Url) | /privacy | The link to a privacy page for the system (Defaults to the frontend's privacy page) |
| Name | Type | Default | Description |
| ------------------------- | ------------------ | -------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| APP_PORT | Number | 4010 | The port the backend server listens on. Is optional. |
| DB_TYPE | String | N/A | The type of the db you want to use. Supported by TypeORM. Possible: `sqlite`, `mysql`, `postgresql` |
| DB_HOST | String | N/A | The db's host IP address/FQDN or file path for sqlite |
| DB_PORT | String | N/A | The db's port |
| DB_USER | String | N/A | The user for accessing the db |
| DB_PASSWORD | String | N/A | The user's password for accessing the db |
| DB_NAME | String | N/A | The db's name |
| NODE_ENV | String | dev | The app's env - influences debug info. When set to "test", mailing errors get ignored. |
| POSTALCODE_COUNTRYCODE | String/CountryCode | N/A | The country code used to validate address postal codes |
| PHONE_COUNTRYCODE | String/CountryCode | null (international) | The country code used to validate phone numbers |
| SEED_TEST_DATA | Boolean | false | If you want the app to seed example data, set this to true |
| STATION_TOKEN_SECRET | String | N/A | Secret key for HMAC-SHA256 station token generation (min 32 chars). **Required.** |
| NATS_URL | String(URL) | nats://localhost:4222 | NATS server connection URL for KV cache |
| NATS_PREWARM | Boolean | false | Preload all runner state into NATS cache at startup (eliminates DB reads on first scan) |
| MAILER_URL | String(URL) | N/A | The mailer's base URL (no trailing slash) |
| MAILER_KEY | String | N/A | The mailer's API key |
| SELFSERVICE_URL | String(URL) | N/A | The link to selfservice (no trailing slash) |
| IMPRINT_URL | String(URL) | /imprint | The link to an imprint page for the system (defaults to the frontend's imprint) |
| PRIVACY_URL | String(URL) | /privacy | The link to a privacy page for the system (defaults to the frontend's privacy page) |
## Recommended Editor

3099
bun.lock Normal file

File diff suppressed because it is too large Load Diff

6
bunfig.toml Normal file
View File

@@ -0,0 +1,6 @@
# Bun configuration
# See: https://bun.sh/docs/runtime/bunfig
[runtime]
# Enable Node.js compatibility mode
bun = true

View File

@@ -1280,35 +1280,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# nodemon
**Author**: [object Object]
**Repo**: [object Object]
**License**: MIT
**Description**: Simple monitor script for use during development of a node.js app.
## License Text
MIT License
Copyright (c) 2010 - present, Remy Sharp, https://remysharp.com <remy@remysharp.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# release-it
**Author**: [object Object]
**Repo**: [object Object]
@@ -1398,35 +1369,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# ts-node
**Author**: [object Object]
**Repo**: [object Object]
**License**: MIT
**Description**: TypeScript execution environment and REPL for node.js, with source map support
## License Text
The MIT License (MIT)
Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
# typedoc
**Author**: undefined
**Repo**: [object Object]

View File

@@ -1,7 +1,8 @@
const dotenv = require('dotenv');
dotenv.config();
//
const SOURCE_PATH = process.env.NODE_ENV === 'production' ? 'dist' : 'src';
// Bun workflow: always compile first, then run from dist/
const SOURCE_PATH = 'dist';
module.exports = {
type: process.env.DB_TYPE,
host: process.env.DB_HOST,
@@ -9,8 +10,7 @@ module.exports = {
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
// entities: ["src/**/entities/*.ts"],
entities: [ `${SOURCE_PATH}/**/entities/*{.ts,.js}` ],
seeds: [ `${SOURCE_PATH}/**/seeds/*{.ts,.js}` ]
// seeds: ['src/seeds/*.ts'],
// Always load compiled .js files from dist/ (TypeORM entities have circular deps)
entities: [ `${SOURCE_PATH}/**/entities/*.js` ],
seeds: [ `${SOURCE_PATH}/**/seeds/*.js` ]
};

View File

@@ -1,6 +1,6 @@
{
"name": "@odit/lfk-backend",
"version": "1.6.0",
"version": "1.7.0",
"main": "src/app.ts",
"repository": "https://git.odit.services/lfk/backend",
"author": {
@@ -63,27 +63,26 @@
"auto-changelog": "2.4.0",
"cp-cli": "2.0.0",
"jest": "26.6.3",
"nodemon": "2.0.7",
"release-it": "14.2.2",
"rimraf": "3.0.2",
"start-server-and-test": "1.11.7",
"ts-jest": "26.5.0",
"ts-node": "10.9.2",
"typedoc": "0.20.19",
"typescript": "5.9.3"
},
"scripts": {
"dev": "nodemon src/app.ts",
"dev": "bun scripts/dev_watch.ts",
"start": "bun dist/app.js",
"build": "rimraf ./dist && tsc && cp-cli ./src/static ./dist/static",
"docs": "typedoc --out docs src",
"test": "jest",
"test:watch": "jest --watchAll",
"test:ci:generate_env": "ts-node scripts/create_testenv.ts",
"test:ci:generate_env": "bun scripts/create_testenv.ts",
"test:ci:run": "start-server-and-test dev http://localhost:4010/api/docs/openapi.json test",
"test:ci": "npm run test:ci:generate_env && npm run test:ci:run",
"benchmark": "ts-node scripts/benchmark_scan_intake.ts",
"seed": "ts-node ./node_modules/typeorm/cli.js schema:sync && ts-node ./node_modules/typeorm-seeding/dist/cli.js seed",
"openapi:export": "ts-node scripts/openapi_export.ts",
"test:ci": "bun run test:ci:generate_env && bun run test:ci:run",
"benchmark": "bun scripts/benchmark_scan_intake.ts",
"seed": "bun ./node_modules/typeorm/cli.js schema:sync && bun ./node_modules/typeorm-seeding/dist/cli.js seed",
"openapi:export": "bun scripts/openapi_export.ts",
"licenses:export": "license-exporter --markdown",
"changelog:export": "auto-changelog --commit-limit false -p -u --hide-credit",
"release": "release-it --only-version"
@@ -103,13 +102,7 @@
"publish": false
},
"hooks": {
"after:bump": "npm run changelog:export && npm run licenses:export && git add CHANGELOG.md && git add licenses.md"
"after:bump": "bun run changelog:export && bun run licenses:export && git add CHANGELOG.md && git add licenses.md"
}
},
"nodemonConfig": {
"ignore": [
"src/tests/*",
"docs/*"
]
}
}

View File

@@ -2,11 +2,11 @@
* Scan Intake Benchmark Script
*
* Measures TrackScan creation performance before and after each optimisation phase.
* Run against a live dev server: npm run dev
* Run against a live dev server: bun run dev
*
* Usage:
* npx ts-node scripts/benchmark_scan_intake.ts
* npx ts-node scripts/benchmark_scan_intake.ts --base http://localhost:4010
* bun run benchmark
* bun scripts/benchmark_scan_intake.ts --base http://localhost:4010
*
* What it measures:
* 1. Single sequential scans — baseline latency per request (p50/p95/p99/max)

138
scripts/dev_watch.ts Normal file
View File

@@ -0,0 +1,138 @@
#!/usr/bin/env bun
/**
* Development watch script for Bun
*
* Watches src/ for changes, rebuilds on change, and restarts the server.
* This is necessary because we must compile TypeScript first due to circular
* TypeORM entity dependencies that Bun's TS loader cannot handle directly.
*/
import { watch } from "fs";
import { spawn } from "child_process";
import consola from "consola";
let serverProcess: ReturnType<typeof spawn> | null = null;
let isRebuilding = false;
let pendingRestart = false;
let debounceTimer: NodeJS.Timeout | null = null;
let watcherReady = false;
function killServer() {
return new Promise<void>((resolve) => {
if (serverProcess) {
consola.info("Stopping server...");
serverProcess.kill();
serverProcess = null;
// Wait for port to be fully released (longer on Windows)
setTimeout(resolve, 2000);
} else {
resolve();
}
});
}
function startServer() {
consola.info("Starting server...");
serverProcess = spawn("bun", ["dist/app.js"], {
stdio: "inherit",
shell: true,
});
serverProcess.on("error", (err) => {
consola.error("Server process error:", err);
});
serverProcess.on("exit", (code) => {
if (code !== null && code !== 0 && code !== 143) {
consola.error(`Server exited with code ${code}`);
}
});
// Enable watcher after initial server start
if (!watcherReady) {
setTimeout(() => {
watcherReady = true;
consola.success("👀 Watching for file changes...");
}, 1000);
}
}
async function rebuild() {
if (isRebuilding) {
pendingRestart = true;
return;
}
isRebuilding = true;
pendingRestart = false;
consola.info("Rebuilding...");
await killServer();
const buildProcess = spawn("bun", ["run", "build"], {
stdio: "inherit",
shell: true,
});
buildProcess.on("exit", (code) => {
isRebuilding = false;
if (code === 0) {
consola.success("Build complete!");
startServer();
if (pendingRestart) {
consola.info("Change detected during build, rebuilding again...");
setTimeout(() => rebuild(), 100);
}
} else {
consola.error(`Build failed with code ${code}`);
}
});
}
// Initial build and start
consola.info("🔄 Development mode - watching for changes...");
rebuild();
// Watch src/ for changes (including subdirectories)
const watcher = watch(
"./src",
{ recursive: true },
(eventType, filename) => {
if (!watcherReady) return; // Ignore changes during initial build
if (filename && filename.endsWith(".ts")) {
// Ignore test files and declaration files
if (filename.endsWith(".spec.ts") || filename.endsWith(".d.ts")) {
return;
}
// Debounce: wait 500ms for multiple rapid changes
if (debounceTimer) {
clearTimeout(debounceTimer);
}
debounceTimer = setTimeout(() => {
consola.info(`File changed: ${filename}`);
rebuild();
}, 500);
}
}
);
// Cleanup on exit
process.on("SIGINT", () => {
consola.info("\nShutting down...");
if (debounceTimer) clearTimeout(debounceTimer);
killServer();
watcher.close();
process.exit(0);
});
process.on("SIGTERM", () => {
if (debounceTimer) clearTimeout(debounceTimer);
killServer();
watcher.close();
process.exit(0);
});

View File

@@ -7,7 +7,8 @@ import authchecker from "./middlewares/authchecker";
import { ErrorHandler } from './middlewares/ErrorHandler';
import UserChecker from './middlewares/UserChecker';
const CONTROLLERS_FILE_EXTENSION = process.env.NODE_ENV === 'production' ? 'js' : 'ts';
// Always use .js when running from dist/ (Bun workflow: always build first)
const CONTROLLERS_FILE_EXTENSION = 'js';
const app = createExpressServer({
authorizationChecker: authchecker,
currentUserChecker: UserChecker,

View File

@@ -1,8 +1,8 @@
import consola from 'consola';
import { config as configDotenv } from 'dotenv';
import { CountryCode } from 'libphonenumber-js';
import ValidatorJS from 'validator';
import consola from 'consola';
import { config as configDotenv } from 'dotenv';
import { CountryCode } from 'libphonenumber-js';
import ValidatorJS from 'validator';
configDotenv();
export const config = {
internal_port: parseInt(process.env.APP_PORT) || 4010,

View File

@@ -0,0 +1,35 @@
/**
* Entity barrel file for Bun compatibility.
* Imports all entities in the correct order to resolve circular dependencies.
*/
// Base/parent entities first
export * from './Participant';
export * from './Donation';
export * from './Scan';
// Child entities that depend on the above
export * from './Runner';
export * from './DistanceDonation';
export * from './FixedDonation';
export * from './TrackScan';
// Entities with cross-references
export * from './RunnerCard';
export * from './RunnerGroup';
export * from './RunnerOrganization';
export * from './RunnerTeam';
export * from './ScanStation';
export * from './Track';
// Independent entities
export * from './Address';
export * from './ConfigFlags';
export * from './Donor';
export * from './GroupContact';
export * from './Permission';
export * from './Principal';
export * from './StatsClient';
export * from './User';
export * from './UserAction';
export * from './UserGroup';