Compare commits

..

5 Commits
1.6.0 ... 1.7.1

Author SHA1 Message Date
d1c4744231 chore(release): 1.7.1
All checks were successful
Build release images / build-container (push) Successful in 2m26s
2026-02-20 20:32:25 +01:00
fe90414dd9 fix(ci): Switch to bun in ci 2026-02-20 20:31:23 +01:00
21ceb9fa26 perf(db): Added indexes 2026-02-20 20:31:02 +01:00
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
25 changed files with 3256 additions and 174 deletions

View File

@@ -10,12 +10,9 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up Node.js - uses: oven-sh/setup-bun@v2
uses: actions/setup-node@v4 - run: bun install --frozen-lockfile
with: - run: bun licenses:export
node-version: 19
- run: npm i -g pnpm@10.7 && pnpm i
- run: pnpm licenses:export
- name: Login to registry - name: Login to registry
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:

4
.gitignore vendored
View File

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

View File

@@ -2,10 +2,25 @@
All notable changes to this project will be documented in this file. Dates are displayed in UTC. All notable changes to this project will be documented in this file. Dates are displayed in UTC.
#### [1.7.1](https://git.odit.services/lfk/backend/compare/1.7.0...1.7.1)
- fix(ci): Switch to bun in ci [`fe90414`](https://git.odit.services/lfk/backend/commit/fe90414dd910baff8107197408575b6af0cc4cbf)
- perf(db): Added indexes [`21ceb9f`](https://git.odit.services/lfk/backend/commit/21ceb9fa265df2f2193a6c4fb58080ead9c72bf8)
#### [1.7.0](https://git.odit.services/lfk/backend/compare/1.6.0...1.7.0)
> 20 February 2026
- refactor: Bun by default [`240bd9c`](https://git.odit.services/lfk/backend/commit/240bd9cba10636bfc100ea2732508d805639f105)
- chore(release): 1.7.0 [`5081819`](https://git.odit.services/lfk/backend/commit/5081819281eacd6beb8d4876f0a9df71c901e84e)
#### [1.6.0](https://git.odit.services/lfk/backend/compare/1.5.2...1.6.0) #### [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) - 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(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(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) - 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) - 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 # Typescript Build
FROM registry.odit.services/hub/library/node:23.10.0-alpine3.21 AS build FROM registry.odit.services/hub/oven/bun:1.3.9-alpine AS build
ARG NPM_REGISTRY_URL=https://registry.npmjs.org
WORKDIR /app WORKDIR /app
COPY package.json ./ COPY package.json bun.lockb* ./
COPY pnpm-workspace.yaml ./ RUN bun install --frozen-lockfile
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 tsconfig.json ormconfig.js ./ COPY tsconfig.json ormconfig.js bunfig.toml ./
COPY src ./src COPY src ./src
RUN pnpm run build \ RUN bun run build \
&& rm -rf /app/node_modules \ && rm -rf /app/node_modules \
&& pnpm i --production --prefer-offline && bun install --production --frozen-lockfile
# final image # 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 WORKDIR /app
COPY --from=build /app/package.json /app/package.json COPY --from=build /app/package.json /app/package.json
COPY --from=build /app/pnpm-lock.yaml /app/pnpm-lock.yaml COPY --from=build /app/bun.lockb* /app/
COPY --from=build /app/pnpm-workspace.yaml /app/pnpm-workspace.yaml
COPY --from=build /app/ormconfig.js /app/ormconfig.js 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/dist /app/dist
COPY --from=build /app/node_modules /app/node_modules 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 Backend Server
## Quickstart 🐳 ## Prerequisites
> Use this to run the backend with a postgresql db in docker
1. Clone the repo or copy the docker-compose This project uses **Bun** as the runtime and package manager. Install Bun first:
2. Run in toe folder that contains the docker-compose file: `docker-compose up -d`
```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 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`) 4. You can now use the default admin user (`demo:demo`)
## Dev Setup 🛠 ## 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) 1. Rename the `.env.example` file to `.env` (you can adjust app port and other settings if needed)
2. Install Dependencies 2. Start NATS (required for KV cache):
```bash ```bash
pnpm i docker-compose up -d nats
``` ```
3. Start the server 3. Install dependencies:
```bash ```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 ### Run Tests
```bash ```bash
# Run tests once (server has to run) # Run tests once (server has to be running)
pnpm test bun test
# Run test in watch mode (reruns on change) # 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) # Run test in CI mode (automatically starts the dev server)
pnpm test:ci bun run test:ci
```
### Run Benchmarks
```bash
# Start the server first
bun run dev
# In another terminal:
bun run benchmark
``` ```
### Generate Docs ### Generate Docs
```bash ```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 ## ENV Vars
> You can provide them via .env file or docker 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 | | Name | Type | Default | Description |
| ---------------------- | ------------------ | -------------------- | -------------------------------------------------------------------------------------------------------------- | | ------------------------- | ------------------ | -------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| APP_PORT | Number | 4010 | The port the backend server listens on. Is optional. | | 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_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's ip-address/fqdn or file path for sqlite | | 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_PORT | String | N/A | The db's port |
| DB_USER | String | N/A | The user for accessing the db | | 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_PASSWORD | String | N/A | The user's password for accessing the db |
| DB_NAME | String | N/A | The db's name | | 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. | | 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 countrycode used to validate address's postal codes | | POSTALCODE_COUNTRYCODE | String/CountryCode | N/A | The country code used to validate address postal codes |
| PHONE_COUNTRYCODE | String/CountryCode | null (international) | The countrycode used to validate phone numers | | 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 some example data set this to true | | SEED_TEST_DATA | Boolean | false | If you want the app to seed example data, set this to true |
| MAILER_URL | String(Url) | N/A | The mailer's base url (no trailing slash) | | STATION_TOKEN_SECRET | String | N/A | Secret key for HMAC-SHA256 station token generation (min 32 chars). **Required.** |
| MAILER_KEY | String | N/A | The mailer's api key. | | NATS_URL | String(URL) | nats://localhost:4222 | NATS server connection URL for KV cache |
| SELFSERVICE_URL | String(Url) | N/A | The link to selfservice (no trailing slash) | | NATS_PREWARM | Boolean | false | Preload all runner state into NATS cache at startup (eliminates DB reads on first scan) |
| IMPRINT_URL | String(Url) | /imprint | The link to a imprint page for the system (Defaults to the frontend's imprint) | | MAILER_URL | String(URL) | N/A | The mailer's base URL (no trailing slash) |
| PRIVACY_URL | String(Url) | /privacy | The link to a privacy page for the system (Defaults to the frontend's privacy page) | | 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 ## Recommended Editor

2883
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. 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 # release-it
**Author**: [object Object] **Author**: [object Object]
**Repo**: [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. 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 # typedoc
**Author**: undefined **Author**: undefined
**Repo**: [object Object] **Repo**: [object Object]

View File

@@ -1,7 +1,8 @@
const dotenv = require('dotenv'); const dotenv = require('dotenv');
dotenv.config(); 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 = { module.exports = {
type: process.env.DB_TYPE, type: process.env.DB_TYPE,
host: process.env.DB_HOST, host: process.env.DB_HOST,
@@ -9,8 +10,7 @@ module.exports = {
username: process.env.DB_USER, username: process.env.DB_USER,
password: process.env.DB_PASSWORD, password: process.env.DB_PASSWORD,
database: process.env.DB_NAME, database: process.env.DB_NAME,
// entities: ["src/**/entities/*.ts"], // Always load compiled .js files from dist/ (TypeORM entities have circular deps)
entities: [ `${SOURCE_PATH}/**/entities/*{.ts,.js}` ], entities: [ `${SOURCE_PATH}/**/entities/*.js` ],
seeds: [ `${SOURCE_PATH}/**/seeds/*{.ts,.js}` ] seeds: [ `${SOURCE_PATH}/**/seeds/*.js` ]
// seeds: ['src/seeds/*.ts'],
}; };

View File

@@ -1,6 +1,6 @@
{ {
"name": "@odit/lfk-backend", "name": "@odit/lfk-backend",
"version": "1.6.0", "version": "1.7.1",
"main": "src/app.ts", "main": "src/app.ts",
"repository": "https://git.odit.services/lfk/backend", "repository": "https://git.odit.services/lfk/backend",
"author": { "author": {
@@ -63,27 +63,26 @@
"auto-changelog": "2.4.0", "auto-changelog": "2.4.0",
"cp-cli": "2.0.0", "cp-cli": "2.0.0",
"jest": "26.6.3", "jest": "26.6.3",
"nodemon": "2.0.7",
"release-it": "14.2.2", "release-it": "14.2.2",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"start-server-and-test": "1.11.7", "start-server-and-test": "1.11.7",
"ts-jest": "26.5.0", "ts-jest": "26.5.0",
"ts-node": "10.9.2",
"typedoc": "0.20.19", "typedoc": "0.20.19",
"typescript": "5.9.3" "typescript": "5.9.3"
}, },
"scripts": { "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", "build": "rimraf ./dist && tsc && cp-cli ./src/static ./dist/static",
"docs": "typedoc --out docs src", "docs": "typedoc --out docs src",
"test": "jest", "test": "jest",
"test:watch": "jest --watchAll", "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: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", "test:ci": "bun run test:ci:generate_env && bun run test:ci:run",
"benchmark": "ts-node scripts/benchmark_scan_intake.ts", "benchmark": "bun 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", "seed": "bun ./node_modules/typeorm/cli.js schema:sync && bun ./node_modules/typeorm-seeding/dist/cli.js seed",
"openapi:export": "ts-node scripts/openapi_export.ts", "openapi:export": "bun scripts/openapi_export.ts",
"licenses:export": "license-exporter --markdown", "licenses:export": "license-exporter --markdown",
"changelog:export": "auto-changelog --commit-limit false -p -u --hide-credit", "changelog:export": "auto-changelog --commit-limit false -p -u --hide-credit",
"release": "release-it --only-version" "release": "release-it --only-version"
@@ -103,13 +102,7 @@
"publish": false "publish": false
}, },
"hooks": { "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 * Scan Intake Benchmark Script
* *
* Measures TrackScan creation performance before and after each optimisation phase. * 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: * Usage:
* npx ts-node scripts/benchmark_scan_intake.ts * bun run benchmark
* npx ts-node scripts/benchmark_scan_intake.ts --base http://localhost:4010 * bun scripts/benchmark_scan_intake.ts --base http://localhost:4010
* *
* What it measures: * What it measures:
* 1. Single sequential scans — baseline latency per request (p50/p95/p99/max) * 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 { ErrorHandler } from './middlewares/ErrorHandler';
import UserChecker from './middlewares/UserChecker'; 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({ const app = createExpressServer({
authorizationChecker: authchecker, authorizationChecker: authchecker,
currentUserChecker: UserChecker, currentUserChecker: UserChecker,

View File

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

View File

@@ -1,5 +1,5 @@
import { IsInt, IsNotEmpty, IsPositive } from "class-validator"; import { IsInt, IsNotEmpty, IsPositive } from "class-validator";
import { ChildEntity, Column, ManyToOne } from "typeorm"; import { ChildEntity, Column, Index, ManyToOne } from "typeorm";
import { ResponseDistanceDonation } from '../responses/ResponseDistanceDonation'; import { ResponseDistanceDonation } from '../responses/ResponseDistanceDonation';
import { Donation } from "./Donation"; import { Donation } from "./Donation";
import { Runner } from "./Runner"; import { Runner } from "./Runner";
@@ -7,8 +7,9 @@ import { Runner } from "./Runner";
/** /**
* Defines the DistanceDonation entity. * Defines the DistanceDonation entity.
* For distanceDonations a donor pledges to donate a certain amount for each kilometer ran by a runner. * For distanceDonations a donor pledges to donate a certain amount for each kilometer ran by a runner.
*/ */
@ChildEntity() @ChildEntity()
@Index(['runner'])
export class DistanceDonation extends Donation { export class DistanceDonation extends Donation {
/** /**
* The donation's associated runner. * The donation's associated runner.

View File

@@ -2,7 +2,7 @@ import {
IsInt, IsInt,
IsPositive IsPositive
} from "class-validator"; } from "class-validator";
import { BeforeInsert, BeforeUpdate, Column, Entity, ManyToOne, PrimaryGeneratedColumn, TableInheritance } from "typeorm"; import { BeforeInsert, BeforeUpdate, Column, Entity, Index, ManyToOne, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
import { ResponseDonation } from '../responses/ResponseDonation'; import { ResponseDonation } from '../responses/ResponseDonation';
import { Donor } from './Donor'; import { Donor } from './Donor';
@@ -10,9 +10,10 @@ import { Donor } from './Donor';
* Defines the Donation entity. * Defines the Donation entity.
* A donation just associates a donor with a donation amount. * A donation just associates a donor with a donation amount.
* The specifics of the amoun's determination has to be implemented in child classes. * The specifics of the amoun's determination has to be implemented in child classes.
*/ */
@Entity() @Entity()
@TableInheritance({ column: { name: "type", type: "varchar" } }) @TableInheritance({ column: { name: "type", type: "varchar" } })
@Index(['donor'])
export abstract class Donation { export abstract class Donation {
/** /**
* Autogenerated unique id (primary key). * Autogenerated unique id (primary key).

View File

@@ -9,18 +9,19 @@ import {
IsString IsString
} from "class-validator"; } from "class-validator";
import { BeforeInsert, BeforeUpdate, Column, Entity, PrimaryGeneratedColumn, TableInheritance } from "typeorm"; import { BeforeInsert, BeforeUpdate, Column, Entity, Index, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
import { config } from '../../config'; import { config } from '../../config';
import { ResponseParticipant } from '../responses/ResponseParticipant'; import { ResponseParticipant } from '../responses/ResponseParticipant';
import { Address } from "./Address"; import { Address } from "./Address";
/** /**
* Defines the Participant entity. * Defines the Participant entity.
* Participans can donate and therefor be associated with donation entities. * Participans can donate and therefor be associated with donation entities.
*/ */
@Entity() @Entity()
@TableInheritance({ column: { name: "type", type: "varchar" } }) @TableInheritance({ column: { name: "type", type: "varchar" } })
export abstract class Participant { @Index(['email'])
export abstract class Participant {
/** /**
* Autogenerated unique id (primary key). * Autogenerated unique id (primary key).
*/ */

View File

@@ -1,5 +1,5 @@
import { IsInt, IsNotEmpty, IsOptional, IsString } from "class-validator"; import { IsInt, IsNotEmpty, IsOptional, IsString } from "class-validator";
import { ChildEntity, Column, ManyToOne, OneToMany } from "typeorm"; import { ChildEntity, Column, Index, ManyToOne, OneToMany } from "typeorm";
import { ResponseRunner } from '../responses/ResponseRunner'; import { ResponseRunner } from '../responses/ResponseRunner';
import { DistanceDonation } from "./DistanceDonation"; import { DistanceDonation } from "./DistanceDonation";
import { Participant } from "./Participant"; import { Participant } from "./Participant";
@@ -11,8 +11,9 @@ import { Scan } from "./Scan";
* Defines the runner entity. * Defines the runner entity.
* Runners differ from participants in being able to actually accumulate a ran distance through scans. * Runners differ from participants in being able to actually accumulate a ran distance through scans.
* Runner's get organized in groups. * Runner's get organized in groups.
*/ */
@ChildEntity() @ChildEntity()
@Index(['group'])
export class Runner extends Participant { export class Runner extends Participant {
/** /**
* The runner's associated group. * The runner's associated group.

View File

@@ -6,7 +6,7 @@ import {
IsOptional, IsOptional,
IsPositive IsPositive
} from "class-validator"; } from "class-validator";
import { BeforeInsert, BeforeUpdate, Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm"; import { BeforeInsert, BeforeUpdate, Column, Entity, Index, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { RunnerCardIdOutOfRangeError } from '../../errors/RunnerCardErrors'; import { RunnerCardIdOutOfRangeError } from '../../errors/RunnerCardErrors';
import { ResponseRunnerCard } from '../responses/ResponseRunnerCard'; import { ResponseRunnerCard } from '../responses/ResponseRunnerCard';
import { Runner } from "./Runner"; import { Runner } from "./Runner";
@@ -16,8 +16,10 @@ import { TrackScan } from "./TrackScan";
* Defines the RunnerCard entity. * Defines the RunnerCard entity.
* A runnerCard is a physical representation for a runner. * A runnerCard is a physical representation for a runner.
* It can be associated with a runner to create scans via the scan station's. * It can be associated with a runner to create scans via the scan station's.
*/ */
@Entity() @Entity()
@Index(['runner'])
@Index(['enabled'])
export class RunnerCard { export class RunnerCard {
/** /**
* Autogenerated unique id (primary key). * Autogenerated unique id (primary key).

View File

@@ -1,5 +1,5 @@
import { IsNotEmpty } from "class-validator"; import { IsNotEmpty } from "class-validator";
import { ChildEntity, ManyToOne } from "typeorm"; import { ChildEntity, Index, ManyToOne } from "typeorm";
import { ResponseRunnerTeam } from '../responses/ResponseRunnerTeam'; import { ResponseRunnerTeam } from '../responses/ResponseRunnerTeam';
import { RunnerGroup } from "./RunnerGroup"; import { RunnerGroup } from "./RunnerGroup";
import { RunnerOrganization } from "./RunnerOrganization"; import { RunnerOrganization } from "./RunnerOrganization";
@@ -7,8 +7,9 @@ import { RunnerOrganization } from "./RunnerOrganization";
/** /**
* Defines the RunnerTeam entity. * Defines the RunnerTeam entity.
* This usually is a school class or department in a company. * This usually is a school class or department in a company.
*/ */
@ChildEntity() @ChildEntity()
@Index(['parentGroup'])
export class RunnerTeam extends RunnerGroup { export class RunnerTeam extends RunnerGroup {
/** /**

View File

@@ -5,16 +5,19 @@ import {
IsPositive IsPositive
} from "class-validator"; } from "class-validator";
import { BeforeInsert, BeforeUpdate, Column, Entity, ManyToOne, PrimaryGeneratedColumn, TableInheritance } from "typeorm"; import { BeforeInsert, BeforeUpdate, Column, Entity, Index, ManyToOne, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
import { ResponseScan } from '../responses/ResponseScan'; import { ResponseScan } from '../responses/ResponseScan';
import { Runner } from "./Runner"; import { Runner } from "./Runner";
/** /**
* Defines the Scan entity. * Defines the Scan entity.
* A scan basicly adds a certain distance to a runner's total ran distance. * A scan basicly adds a certain distance to a runner's total ran distance.
*/ */
@Entity() @Entity()
@TableInheritance({ column: { name: "type", type: "varchar" } }) @TableInheritance({ column: { name: "type", type: "varchar" } })
@Index(['runner'])
@Index(['runner', 'created_at'])
@Index(['valid'])
export class Scan { export class Scan {
/** /**
* Autogenerated unique id (primary key). * Autogenerated unique id (primary key).

View File

@@ -6,7 +6,7 @@ import {
IsPositive, IsPositive,
IsString IsString
} from "class-validator"; } from "class-validator";
import { BeforeInsert, BeforeUpdate, Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm"; import { BeforeInsert, BeforeUpdate, Column, Entity, Index, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { ResponseScanStation } from '../responses/ResponseScanStation'; import { ResponseScanStation } from '../responses/ResponseScanStation';
import { Track } from "./Track"; import { Track } from "./Track";
import { TrackScan } from "./TrackScan"; import { TrackScan } from "./TrackScan";
@@ -14,8 +14,11 @@ import { TrackScan } from "./TrackScan";
/** /**
* Defines the ScanStation entity. * Defines the ScanStation entity.
* ScanStations get used to create TrackScans for runners based on a scan of their runnerCard. * ScanStations get used to create TrackScans for runners based on a scan of their runnerCard.
*/ */
@Entity() @Entity()
@Index(['track'])
@Index(['prefix'])
@Index(['enabled'])
export class ScanStation { export class ScanStation {
/** /**
* Autogenerated unique id (primary key). * Autogenerated unique id (primary key).

View File

@@ -6,7 +6,7 @@ import {
IsPositive IsPositive
} from "class-validator"; } from "class-validator";
import { ChildEntity, Column, ManyToOne } from "typeorm"; import { ChildEntity, Column, Index, ManyToOne } from "typeorm";
import { ResponseTrackScan } from '../responses/ResponseTrackScan'; import { ResponseTrackScan } from '../responses/ResponseTrackScan';
import { RunnerCard } from "./RunnerCard"; import { RunnerCard } from "./RunnerCard";
import { Scan } from "./Scan"; import { Scan } from "./Scan";
@@ -16,8 +16,13 @@ import { Track } from "./Track";
/** /**
* Defines the TrackScan entity. * Defines the TrackScan entity.
* A track scan usaually get's generated by a scan station. * A track scan usaually get's generated by a scan station.
*/ */
@ChildEntity() @ChildEntity()
@Index(['track'])
@Index(['card'])
@Index(['station'])
@Index(['timestamp'])
@Index(['station', 'timestamp'])
export class TrackScan extends Scan { export class TrackScan extends Scan {
/** /**
* The scan's associated track. * The scan's associated track.

View File

@@ -1,5 +1,5 @@
import { IsBoolean, IsEmail, IsInt, IsNotEmpty, IsOptional, IsPhoneNumber, IsString, IsUrl, IsUUID } from "class-validator"; import { IsBoolean, IsEmail, IsInt, IsNotEmpty, IsOptional, IsPhoneNumber, IsString, IsUrl, IsUUID } from "class-validator";
import { ChildEntity, Column, JoinTable, ManyToMany, OneToMany } from "typeorm"; import { ChildEntity, Column, Index, JoinTable, ManyToMany, OneToMany } from "typeorm";
import { config } from '../../config'; import { config } from '../../config';
import { ResponsePrincipal } from '../responses/ResponsePrincipal'; import { ResponsePrincipal } from '../responses/ResponsePrincipal';
import { ResponseUser } from '../responses/ResponseUser'; import { ResponseUser } from '../responses/ResponseUser';
@@ -8,12 +8,13 @@ import { Principal } from './Principal';
import { UserAction } from './UserAction'; import { UserAction } from './UserAction';
import { UserGroup } from './UserGroup'; import { UserGroup } from './UserGroup';
/** /**
* Defines the User entity. * Defines the User entity.
* Users are the ones that can use the "admin" webui and do stuff in the backend. * Users are the ones that can use the "admin" webui and do stuff in the backend.
*/ */
@ChildEntity() @ChildEntity()
export class User extends Principal { @Index(['enabled'])
export class User extends Principal {
/** /**
* The user's uuid. * The user's uuid.
* Mainly gets used as a per-user salt for the password hash. * Mainly gets used as a per-user salt for the password hash.

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';