From 0f8bd3df6a483a9e3e08eb8d019760d899748264 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Sun, 23 Feb 2025 13:56:40 +0100 Subject: [PATCH 01/16] chore: Basic logging met loki --- backend/src/app.ts | 29 ++++++++++++++++++++- config/loki-config.yml | 30 ++++++++++++++++++++++ docker-compose.yml | 36 ++++++++++++++++---------- package-lock.json | 58 +++++++++++++++++++++++++++++++++++------- package.json | 3 +++ 5 files changed, 133 insertions(+), 23 deletions(-) create mode 100644 config/loki-config.yml diff --git a/backend/src/app.ts b/backend/src/app.ts index 65dd8a7a..756cf409 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -19,4 +19,31 @@ async function startServer() { }); } -startServer(); + +import { LokiClient, LogError, LokiLabels, LogInfo } from 'loki-logger-ts'; + +const HostData = { + url: "http://localhost:3100/loki/api/v1/push", +}; + +const labels: LokiLabels = { + source: "Test", + job: "TestJob", + host: "localhost", +}; + +async function main() { + const client = new LokiClient(HostData.url); + + const msg = 'Hello World'; + await LogError(client, msg, labels); + await LogInfo(client, 'Dit is een goed bericht', labels); + + console.log(client.showMetrics()); + + console.log(client.getMetrics()); +} + +main(); + +// startServer(); diff --git a/config/loki-config.yml b/config/loki-config.yml new file mode 100644 index 00000000..bedf3c80 --- /dev/null +++ b/config/loki-config.yml @@ -0,0 +1,30 @@ + +# This is a complete configuration to deploy Loki backed by the filesystem. +# The index will be shipped to the storage via tsdb-shipper. + +auth_enabled: false + +server: + http_listen_port: 3100 + +common: + ring: + instance_addr: 127.0.0.1 + kvstore: + store: inmemory + replication_factor: 1 + path_prefix: /tmp/loki + +schema_config: + configs: + - from: 2020-05-15 + store: tsdb + object_store: filesystem + schema: v13 + index: + prefix: index_ + period: 24h + +storage_config: + filesystem: + directory: /tmp/loki/chunks diff --git a/docker-compose.yml b/docker-compose.yml index e8efb530..b29b1470 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,16 +1,26 @@ services: - db: - image: postgres:latest - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: postgres - ports: - - "5432:5432" - network_mode: "host" - volumes: - - postgres_data:/var/lib/postgresql/data - - ./backend/config/db/init.sql:/docker-entrypoint-initdb.d/init.sql + db: + image: postgres:latest + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: postgres + ports: + - '5432:5432' + network_mode: 'host' + volumes: + - dwengo_postgres_data:/var/lib/postgresql/data + - ./backend/config/db/init.sql:/docker-entrypoint-initdb.d/init.sql + + logging: + image: grafana/loki:latest + ports: + - '3100:3100' + - '9095:9095' + network_mode: 'host' + volumes: + - ./config/loki-config.yml:/etc/loki/config.yaml + command: -config.file=/etc/loki/config.yaml volumes: - postgres_data: + dwengo_postgres_data: diff --git a/package-lock.json b/package-lock.json index a00e4f54..394ee797 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,10 +7,14 @@ "": { "name": "dwengo-1-monorepo", "version": "0.0.1", + "license": "MIT", "workspaces": [ "backend", "frontend" ], + "dependencies": { + "loki-logger-ts": "^1.0.2" + }, "devDependencies": { "@eslint/compat": "^1.2.6", "@eslint/js": "^9.20.0", @@ -26,7 +30,6 @@ "backend": { "name": "dwengo-1-backend", "version": "0.0.1", - "license": "MIT", "dependencies": { "@mikro-orm/core": "^6.4.6", "@mikro-orm/postgresql": "^6.4.6", @@ -3142,9 +3145,19 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, "license": "MIT" }, + "node_modules/axios": { + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3510,7 +3523,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -3778,7 +3790,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -3993,7 +4004,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -4735,6 +4745,26 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", @@ -4756,7 +4786,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", - "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -4772,7 +4801,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -4782,7 +4810,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -5067,7 +5094,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -5786,6 +5812,14 @@ "dev": true, "license": "MIT" }, + "node_modules/loki-logger-ts": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/loki-logger-ts/-/loki-logger-ts-1.0.2.tgz", + "integrity": "sha512-SV/B5o+9jaxiThcU5N3LUxCNTx20IgR9xjCjx/ED/pVc/097mqKSRpmvSjvx9ezFcjJlUF7GBkrBBpR6veNp7Q==", + "dependencies": { + "axios": "^1.4.0" + } + }, "node_modules/loupe": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", @@ -6763,6 +6797,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/package.json b/package.json index 59a2e01d..ff3ceb88 100644 --- a/package.json +++ b/package.json @@ -34,5 +34,8 @@ "eslint-config-prettier": "^10.0.1", "jiti": "^2.4.2", "typescript-eslint": "^8.24.1" + }, + "dependencies": { + "loki-logger-ts": "^1.0.2" } } From 7fd6305fd9f039ce956872a5cf2d84dd9a7562f8 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Sun, 23 Feb 2025 17:29:11 +0100 Subject: [PATCH 02/16] chore(backend): Loki configureren De meeste console statements vervangen door Loki --- backend/package.json | 6 +- backend/src/app.ts | 41 +- backend/src/logging/initalize.ts | 47 ++ backend/src/logging/responseTimeLogger.ts | 23 + backend/src/orm.ts | 7 + config/{loki-config.yml => loki/config.yml} | 2 +- docker-compose.yml | 18 +- package-lock.json | 663 +++++++++++++++++++- 8 files changed, 767 insertions(+), 40 deletions(-) create mode 100644 backend/src/logging/initalize.ts create mode 100644 backend/src/logging/responseTimeLogger.ts rename config/{loki-config.yml => loki/config.yml} (95%) diff --git a/backend/package.json b/backend/package.json index 85a4d255..84a4e23e 100644 --- a/backend/package.json +++ b/backend/package.json @@ -18,12 +18,16 @@ "@mikro-orm/postgresql": "^6.4.6", "@mikro-orm/reflection": "^6.4.6", "dotenv": "^16.4.7", - "express": "^5.0.1" + "express": "^5.0.1", + "response-time": "^2.3.3", + "winston": "^3.17.0", + "winston-loki": "^6.1.3" }, "devDependencies": { "@mikro-orm/cli": "^6.4.6", "@types/express": "^5.0.0", "@types/node": "^22.13.4", + "@types/response-time": "^2.3.8", "globals": "^15.15.0", "ts-node": "^10.9.2", "tsx": "^4.19.3", diff --git a/backend/src/app.ts b/backend/src/app.ts index 756cf409..29807f6b 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -1,11 +1,21 @@ import express, { Express, Response } from 'express'; import initORM from './orm.js'; +import { getLogger } from './logging/initalize.js'; +import { responseTimeLogger } from './logging/responseTimeLogger.js'; +import responseTime from 'response-time'; +import { Logger } from 'winston'; + +const logger: Logger = getLogger(); const app: Express = express(); const port: string | number = process.env.PORT || 3000; +app.use(express.json()); +app.use(responseTime(responseTimeLogger)); + // TODO Replace with Express routes app.get('/', (_, res: Response) => { + logger.debug('GET /'); res.json({ message: 'Hello Dwengo!', }); @@ -15,35 +25,8 @@ async function startServer() { await initORM(); app.listen(port, () => { - console.log(`Server is running at http://localhost:${port}`); + logger.info(`Server is running at http://localhost:${port}`); }); } - -import { LokiClient, LogError, LokiLabels, LogInfo } from 'loki-logger-ts'; - -const HostData = { - url: "http://localhost:3100/loki/api/v1/push", -}; - -const labels: LokiLabels = { - source: "Test", - job: "TestJob", - host: "localhost", -}; - -async function main() { - const client = new LokiClient(HostData.url); - - const msg = 'Hello World'; - await LogError(client, msg, labels); - await LogInfo(client, 'Dit is een goed bericht', labels); - - console.log(client.showMetrics()); - - console.log(client.getMetrics()); -} - -main(); - -// startServer(); +startServer(); diff --git a/backend/src/logging/initalize.ts b/backend/src/logging/initalize.ts new file mode 100644 index 00000000..6a4d52c5 --- /dev/null +++ b/backend/src/logging/initalize.ts @@ -0,0 +1,47 @@ +import { createLogger, format, Logger, transports } from 'winston'; +import LokiTransport from 'winston-loki'; +import { LokiLabels } from 'loki-logger-ts'; + +const LoggingLevel = 'development' === process.env.NODE_ENV ? 'debug' : 'info'; +const Host = 'http://localhost:3102'; +const Labels: LokiLabels = { + source: 'Dwengo-Backend', + job: 'Dwengo-Backend', + host: 'localhost', +}; + +let logger: Logger; + +function initializeLogger() { + if (logger !== undefined) { + return logger; + } + + const lokiTransport: LokiTransport = new LokiTransport({ + host: Host, + labels: Labels, + level: LoggingLevel, + json: true, + format: format.combine(format.timestamp(), format.json()), + onConnectionError: (err) => { + console.error(`Connection error: ${err}`); + }, + }); + + const consoleTransport = new transports.Console({ + level: LoggingLevel, + format: format.combine(format.simple(), format.colorize()), + }); + + logger = createLogger({ + transports: [lokiTransport, consoleTransport], + }); + + logger.debug('Logger initialized'); + return logger; +} + +export function getLogger(): Logger { + logger ||= initializeLogger(); + return logger; +} diff --git a/backend/src/logging/responseTimeLogger.ts b/backend/src/logging/responseTimeLogger.ts new file mode 100644 index 00000000..5baf63f4 --- /dev/null +++ b/backend/src/logging/responseTimeLogger.ts @@ -0,0 +1,23 @@ +import { getLogger } from './initalize.js'; +import { Logger } from 'winston'; +import { Request, Response } from 'express'; + +export function responseTimeLogger(req: Request, res: Response, time: number) { + const logger: Logger = getLogger(); + + const method = req.method; + const url = req.url; + const status = res.statusCode; + + logger.info({ + message: 'Request completed', + method: method, + url: url, + status: status, + responseTime: Number(time), + labels: { + origin: 'api', + type: 'responseTime', + }, + }); +} diff --git a/backend/src/orm.ts b/backend/src/orm.ts index d9de328f..f4dbf0e9 100644 --- a/backend/src/orm.ts +++ b/backend/src/orm.ts @@ -1,6 +1,13 @@ import { MikroORM } from '@mikro-orm/core'; import config from './mikro-orm.config.js'; +import { getLogger } from './logging/initalize.js'; +import { Logger } from 'winston'; export default async function initORM() { + const logger: Logger = getLogger(); + + logger.info('Initializing ORM'); + logger.debug('MikroORM config is', config); + await MikroORM.init(config); } diff --git a/config/loki-config.yml b/config/loki/config.yml similarity index 95% rename from config/loki-config.yml rename to config/loki/config.yml index bedf3c80..f5ba3799 100644 --- a/config/loki-config.yml +++ b/config/loki/config.yml @@ -5,7 +5,7 @@ auth_enabled: false server: - http_listen_port: 3100 + http_listen_port: 3102 common: ring: diff --git a/docker-compose.yml b/docker-compose.yml index b29b1470..1f8a4c98 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,6 @@ services: POSTGRES_DB: postgres ports: - '5432:5432' - network_mode: 'host' volumes: - dwengo_postgres_data:/var/lib/postgresql/data - ./backend/config/db/init.sql:/docker-entrypoint-initdb.d/init.sql @@ -15,12 +14,23 @@ services: logging: image: grafana/loki:latest ports: - - '3100:3100' + - '3102:3102' - '9095:9095' - network_mode: 'host' volumes: - - ./config/loki-config.yml:/etc/loki/config.yaml + - ./config/loki/config.yml:/etc/loki/config.yaml + - dwengo_loki_data:/loki command: -config.file=/etc/loki/config.yaml + restart: unless-stopped + + dashboards: + image: grafana/grafana:latest + ports: + - '3000:3000' + volumes: + - dwengo_grafana_data:/var/lib/grafana + restart: unless-stopped volumes: dwengo_postgres_data: + dwengo_loki_data: + dwengo_grafana_data: diff --git a/package-lock.json b/package-lock.json index 394ee797..f9d4faf4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,12 +35,16 @@ "@mikro-orm/postgresql": "^6.4.6", "@mikro-orm/reflection": "^6.4.6", "dotenv": "^16.4.7", - "express": "^5.0.1" + "express": "^5.0.1", + "response-time": "^2.3.3", + "winston": "^3.17.0", + "winston-loki": "^6.1.3" }, "devDependencies": { "@mikro-orm/cli": "^6.4.6", "@types/express": "^5.0.0", "@types/node": "^22.13.4", + "@types/response-time": "^2.3.8", "globals": "^15.15.0", "ts-node": "^10.9.2", "tsx": "^4.19.3", @@ -647,6 +651,15 @@ "node": ">=6.9.0" } }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -775,6 +788,17 @@ "node": ">=18" } }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "license": "MIT", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.0", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", @@ -1674,6 +1698,214 @@ "@mikro-orm/core": "^6.0.0" } }, + "node_modules/@napi-rs/snappy-android-arm-eabi": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-android-arm-eabi/-/snappy-android-arm-eabi-7.2.2.tgz", + "integrity": "sha512-H7DuVkPCK5BlAr1NfSU8bDEN7gYs+R78pSHhDng83QxRnCLmVIZk33ymmIwurmoA1HrdTxbkbuNl+lMvNqnytw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-android-arm64": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-android-arm64/-/snappy-android-arm64-7.2.2.tgz", + "integrity": "sha512-2R/A3qok+nGtpVK8oUMcrIi5OMDckGYNoBLFyli3zp8w6IArPRfg1yOfVUcHvpUDTo9T7LOS1fXgMOoC796eQw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-darwin-arm64": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-darwin-arm64/-/snappy-darwin-arm64-7.2.2.tgz", + "integrity": "sha512-USgArHbfrmdbuq33bD5ssbkPIoT7YCXCRLmZpDS6dMDrx+iM7eD2BecNbOOo7/v1eu6TRmQ0xOzeQ6I/9FIi5g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-darwin-x64": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-darwin-x64/-/snappy-darwin-x64-7.2.2.tgz", + "integrity": "sha512-0APDu8iO5iT0IJKblk2lH0VpWSl9zOZndZKnBYIc+ei1npw2L5QvuErFOTeTdHBtzvUHASB+9bvgaWnQo4PvTQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-freebsd-x64": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-freebsd-x64/-/snappy-freebsd-x64-7.2.2.tgz", + "integrity": "sha512-mRTCJsuzy0o/B0Hnp9CwNB5V6cOJ4wedDTWEthsdKHSsQlO7WU9W1yP7H3Qv3Ccp/ZfMyrmG98Ad7u7lG58WXA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-linux-arm-gnueabihf": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-arm-gnueabihf/-/snappy-linux-arm-gnueabihf-7.2.2.tgz", + "integrity": "sha512-v1uzm8+6uYjasBPcFkv90VLZ+WhLzr/tnfkZ/iD9mHYiULqkqpRuC8zvc3FZaJy5wLQE9zTDkTJN1IvUcZ+Vcg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-linux-arm64-gnu": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-arm64-gnu/-/snappy-linux-arm64-gnu-7.2.2.tgz", + "integrity": "sha512-LrEMa5pBScs4GXWOn6ZYXfQ72IzoolZw5txqUHVGs8eK4g1HR9HTHhb2oY5ySNaKakG5sOgMsb1rwaEnjhChmQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-linux-arm64-musl": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-arm64-musl/-/snappy-linux-arm64-musl-7.2.2.tgz", + "integrity": "sha512-3orWZo9hUpGQcB+3aTLW7UFDqNCQfbr0+MvV67x8nMNYj5eAeUtMmUE/HxLznHO4eZ1qSqiTwLbVx05/Socdlw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-linux-x64-gnu": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-x64-gnu/-/snappy-linux-x64-gnu-7.2.2.tgz", + "integrity": "sha512-jZt8Jit/HHDcavt80zxEkDpH+R1Ic0ssiVCoueASzMXa7vwPJeF4ZxZyqUw4qeSy7n8UUExomu8G8ZbP6VKhgw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-linux-x64-musl": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-x64-musl/-/snappy-linux-x64-musl-7.2.2.tgz", + "integrity": "sha512-Dh96IXgcZrV39a+Tej/owcd9vr5ihiZ3KRix11rr1v0MWtVb61+H1GXXlz6+Zcx9y8jM1NmOuiIuJwkV4vZ4WA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-win32-arm64-msvc": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-win32-arm64-msvc/-/snappy-win32-arm64-msvc-7.2.2.tgz", + "integrity": "sha512-9No0b3xGbHSWv2wtLEn3MO76Yopn1U2TdemZpCaEgOGccz1V+a/1d16Piz3ofSmnA13HGFz3h9NwZH9EOaIgYA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-win32-ia32-msvc": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-win32-ia32-msvc/-/snappy-win32-ia32-msvc-7.2.2.tgz", + "integrity": "sha512-QiGe+0G86J74Qz1JcHtBwM3OYdTni1hX1PFyLRo3HhQUSpmi13Bzc1En7APn+6Pvo7gkrcy81dObGLDSxFAkQQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-win32-x64-msvc": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-win32-x64-msvc/-/snappy-win32-x64-msvc-7.2.2.tgz", + "integrity": "sha512-a43cyx1nK0daw6BZxVcvDEXxKMFLSBSDTAhsFD0VqSKcC7MGUBMaqyoWUcMiI7LBSz4bxUmxDWKfCYzpEmeb3w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1763,6 +1995,70 @@ "dev": true, "license": "MIT" }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, "node_modules/@rollup/pluginutils": { "version": "5.1.4", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", @@ -2229,7 +2525,6 @@ "version": "22.13.4", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.4.tgz", "integrity": "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.20.0" @@ -2249,6 +2544,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/response-time": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@types/response-time/-/response-time-2.3.8.tgz", + "integrity": "sha512-7qGaNYvdxc0zRab8oHpYx7AW17qj+G0xuag1eCrw3M2VWPJQ/HyKaaghWygiaOUl0y9x7QGQwppDpqLJ5V9pzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/node": "*" + } + }, "node_modules/@types/send": { "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", @@ -2279,6 +2585,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.24.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.24.1.tgz", @@ -3141,6 +3453,21 @@ "node": ">=12" } }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -3270,6 +3597,18 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/btoa": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", + "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==", + "license": "(MIT OR Apache-2.0)", + "bin": { + "btoa": "bin/btoa.js" + }, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/bundle-name": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", @@ -3493,6 +3832,16 @@ "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", "license": "MIT" }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3510,7 +3859,31 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "license": "MIT" }, "node_modules/colorette": { @@ -3519,6 +3892,16 @@ "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==", "license": "MIT" }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "license": "MIT", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -3932,6 +4315,12 @@ "dev": true, "license": "MIT" }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -4611,6 +5000,12 @@ "reusify": "^1.0.4" } }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, "node_modules/figlet": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.8.0.tgz", @@ -4745,6 +5140,12 @@ "dev": true, "license": "ISC" }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, "node_modules/follow-redirects": { "version": "1.15.9", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", @@ -5293,6 +5694,12 @@ "node": ">= 0.10" } }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -5769,6 +6176,12 @@ "dev": true, "license": "MIT" }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -5812,6 +6225,23 @@ "dev": true, "license": "MIT" }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/loki-logger-ts": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/loki-logger-ts/-/loki-logger-ts-1.0.2.tgz", @@ -5820,6 +6250,12 @@ "axios": "^1.4.0" } }, + "node_modules/long": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.1.tgz", + "integrity": "sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng==", + "license": "Apache-2.0" + }, "node_modules/loupe": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", @@ -6222,6 +6658,15 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -6231,6 +6676,15 @@ "wrappy": "1" } }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, "node_modules/open": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", @@ -6784,6 +7238,30 @@ "dev": true, "license": "ISC" }, + "node_modules/protobufjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -6898,6 +7376,20 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/rechoir": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", @@ -6966,6 +7458,19 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/response-time": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/response-time/-/response-time-2.3.3.tgz", + "integrity": "sha512-SsjjOPHl/FfrTQNgmc5oen8Hr1Jxpn6LlHNXxCIFdYMHuK1kMeYMobb9XN3mvxaGQm3dbegqYFMX4+GDORfbWg==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "on-headers": "~1.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -7099,6 +7604,15 @@ ], "license": "MIT" }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -7333,6 +7847,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, "node_modules/sirv": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.1.tgz", @@ -7357,6 +7880,35 @@ "node": ">=8" } }, + "node_modules/snappy": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/snappy/-/snappy-7.2.2.tgz", + "integrity": "sha512-iADMq1kY0v3vJmGTuKcFWSXt15qYUz7wFkArOrsSg0IFfI3nJqIJvK2/ZbEIndg7erIJLtAVX2nSOqPz7DcwbA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/snappy-android-arm-eabi": "7.2.2", + "@napi-rs/snappy-android-arm64": "7.2.2", + "@napi-rs/snappy-darwin-arm64": "7.2.2", + "@napi-rs/snappy-darwin-x64": "7.2.2", + "@napi-rs/snappy-freebsd-x64": "7.2.2", + "@napi-rs/snappy-linux-arm-gnueabihf": "7.2.2", + "@napi-rs/snappy-linux-arm64-gnu": "7.2.2", + "@napi-rs/snappy-linux-arm64-musl": "7.2.2", + "@napi-rs/snappy-linux-x64-gnu": "7.2.2", + "@napi-rs/snappy-linux-x64-musl": "7.2.2", + "@napi-rs/snappy-win32-arm64-msvc": "7.2.2", + "@napi-rs/snappy-win32-ia32-msvc": "7.2.2", + "@napi-rs/snappy-win32-x64-msvc": "7.2.2" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -7394,6 +7946,15 @@ "node": ">= 0.6" } }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -7417,6 +7978,15 @@ "dev": true, "license": "MIT" }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -7634,6 +8204,12 @@ "node": ">=8.0.0" } }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, "node_modules/tildify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz", @@ -7764,6 +8340,15 @@ "node": ">=18" } }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/ts-api-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", @@ -7970,7 +8555,6 @@ "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, "license": "MIT" }, "node_modules/unicorn-magic": { @@ -8045,11 +8629,16 @@ "punycode": "^2.1.0" } }, + "node_modules/url-polyfill": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/url-polyfill/-/url-polyfill-1.1.13.tgz", + "integrity": "sha512-tXzkojrv2SujumYthZ/WjF7jaSfNhSXlYMpE5AYdL2I3D7DCeo+mch8KtW2rUuKjDg+3VXODXHVgipt8yGY/eQ==", + "license": "MIT" + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, "license": "MIT" }, "node_modules/utils-merge": { @@ -9084,6 +9673,70 @@ "node": ">=8" } }, + "node_modules/winston": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-loki": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/winston-loki/-/winston-loki-6.1.3.tgz", + "integrity": "sha512-DjWtJ230xHyYQWr9mZJa93yhwHttn3JEtSYWP8vXZWJOahiQheUhf+88dSIidbGXB3u0oLweV6G1vkL/ouT62Q==", + "license": "MIT", + "dependencies": { + "async-exit-hook": "2.0.1", + "btoa": "^1.2.1", + "protobufjs": "^7.2.4", + "url-polyfill": "^1.1.12", + "winston-transport": "^4.3.0" + }, + "optionalDependencies": { + "snappy": "^7.2.2" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", From 9a96e480cd972b497d00c5cee79cc729e0e525f9 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Sun, 23 Feb 2025 17:30:16 +0100 Subject: [PATCH 03/16] chore: Environments in npm commandos --- backend/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/package.json b/backend/package.json index 84a4e23e..008afecf 100644 --- a/backend/package.json +++ b/backend/package.json @@ -5,9 +5,9 @@ "private": true, "type": "module", "scripts": { - "build": "tsc --project tsconfig.json", - "dev": "tsx watch --env-file=.env.development.local src/app.ts", - "start": "node --env-file=.env dist/app.js", + "build": "NODE_ENV=production tsc --project tsconfig.json", + "dev": "NODE_ENV=development tsx watch --env-file=.env.development.local src/app.ts", + "start": "NODE_ENV=production node --env-file=.env dist/app.js", "format": "prettier --write src/", "format-check": "prettier --check src/", "lint": "eslint . --fix", From e6c03c1acaa216f5dc3115a45506700bab49f4fa Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Sun, 2 Mar 2025 13:58:08 +0100 Subject: [PATCH 04/16] chore(backend): Definieer logger voor MikroORM Maak gebruik van juiste logging niveau voor elke taak (namespace) --- backend/src/logging/mikroOrmLogger.ts | 88 +++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 backend/src/logging/mikroOrmLogger.ts diff --git a/backend/src/logging/mikroOrmLogger.ts b/backend/src/logging/mikroOrmLogger.ts new file mode 100644 index 00000000..4fbadb96 --- /dev/null +++ b/backend/src/logging/mikroOrmLogger.ts @@ -0,0 +1,88 @@ +import { DefaultLogger, LogContext, LoggerNamespace } from '@mikro-orm/core'; +import { Logger } from 'winston'; +import { getLogger } from './initalize'; +import { LokiLabels } from 'loki-logger-ts'; + +export class MikroOrmLogger extends DefaultLogger { + private logger: Logger = getLogger(); + + log(namespace: LoggerNamespace, message: string, context?: LogContext) { + if (!this.isEnabled(namespace, context)) { + return; + } + + switch (namespace) { + case 'query': + this.logger.debug( + this.createMessage(namespace, message, context) + ); + break; + case 'query-params': + // TODO Which log level should this be? + this.logger.info( + this.createMessage(namespace, message, context) + ); + break; + case 'schema': + this.logger.info( + this.createMessage(namespace, message, context) + ); + break; + case 'discovery': + this.logger.debug( + this.createMessage(namespace, message, context) + ); + break; + case 'info': + this.logger.info( + this.createMessage(namespace, message, context) + ); + break; + case 'deprecated': + this.logger.warn( + this.createMessage(namespace, message, context) + ); + break; + default: + switch (context?.level) { + case 'info': + this.logger.info( + this.createMessage(namespace, message, context) + ); + break; + case 'warning': + this.logger.warn(message); + break; + case 'error': + this.logger.error(message); + break; + default: + this.logger.debug(message); + break; + } + } + } + + private createMessage( + namespace: LoggerNamespace, + messageArg: string, + context?: LogContext + ) { + const labels: LokiLabels = { + service: 'ORM', + }; + + let message: string; + if (context?.label) { + message = `[${namespace}] (${context?.label}) ${messageArg}`; + } else { + message = `[${namespace}] ${messageArg}`; + } + + return { + message: message, + labels: labels, + context: context, + }; + } +} From eca8d897122021a1f3054f4e9446344900aebdcf Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Sun, 2 Mar 2025 13:59:38 +0100 Subject: [PATCH 05/16] chore(backend): Duidelijkere MikroORM logging Zorgt voor beter formaat en juiste labels --- backend/.env.example | 12 +++++++++++- backend/src/config.ts | 6 ++++++ backend/src/logging/initalize.ts | 20 +++++++++++--------- backend/src/logging/responseTimeLogger.ts | 1 - backend/src/mikro-orm.config.ts | 12 ++++++++++-- 5 files changed, 38 insertions(+), 13 deletions(-) create mode 100644 backend/src/config.ts diff --git a/backend/.env.example b/backend/.env.example index c0c68b1c..165b7a29 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1 +1,11 @@ -PORT=3000 \ No newline at end of file +# +# Basic configuration +# + +PORT=3000 # The port the backend will listen on + +# +# Advanced configuration +# + +# LOKI_HOST=http://localhost:3102 # The address of the Loki instance, used for logging diff --git a/backend/src/config.ts b/backend/src/config.ts new file mode 100644 index 00000000..9edd1437 --- /dev/null +++ b/backend/src/config.ts @@ -0,0 +1,6 @@ +// Logging + +export const LOG_LEVEL: string = + 'development' === process.env.NODE_ENV ? 'debug' : 'info'; +export const LOKI_HOST: string = + process.env.LOKI_HOST || 'http://localhost:3102'; diff --git a/backend/src/logging/initalize.ts b/backend/src/logging/initalize.ts index 6a4d52c5..5da786fe 100644 --- a/backend/src/logging/initalize.ts +++ b/backend/src/logging/initalize.ts @@ -1,43 +1,45 @@ import { createLogger, format, Logger, transports } from 'winston'; import LokiTransport from 'winston-loki'; import { LokiLabels } from 'loki-logger-ts'; +import { LOG_LEVEL, LOKI_HOST } from '../config.js'; -const LoggingLevel = 'development' === process.env.NODE_ENV ? 'debug' : 'info'; -const Host = 'http://localhost:3102'; const Labels: LokiLabels = { source: 'Dwengo-Backend', - job: 'Dwengo-Backend', + service: 'API', host: 'localhost', }; let logger: Logger; -function initializeLogger() { +function initializeLogger(): Logger { if (logger !== undefined) { return logger; } const lokiTransport: LokiTransport = new LokiTransport({ - host: Host, + host: LOKI_HOST, labels: Labels, - level: LoggingLevel, + level: LOG_LEVEL, json: true, format: format.combine(format.timestamp(), format.json()), onConnectionError: (err) => { + // eslint-disable-next-line no-console console.error(`Connection error: ${err}`); }, }); const consoleTransport = new transports.Console({ - level: LoggingLevel, - format: format.combine(format.simple(), format.colorize()), + level: LOG_LEVEL, + format: format.combine(format.cli(), format.colorize()), }); logger = createLogger({ transports: [lokiTransport, consoleTransport], }); - logger.debug('Logger initialized'); + logger.debug( + `Logger initialized with level ${LOG_LEVEL}, Loki host ${LOKI_HOST}` + ); return logger; } diff --git a/backend/src/logging/responseTimeLogger.ts b/backend/src/logging/responseTimeLogger.ts index 5baf63f4..85723e5e 100644 --- a/backend/src/logging/responseTimeLogger.ts +++ b/backend/src/logging/responseTimeLogger.ts @@ -16,7 +16,6 @@ export function responseTimeLogger(req: Request, res: Response, time: number) { status: status, responseTime: Number(time), labels: { - origin: 'api', type: 'responseTime', }, }); diff --git a/backend/src/mikro-orm.config.ts b/backend/src/mikro-orm.config.ts index c4302a37..1beec07b 100644 --- a/backend/src/mikro-orm.config.ts +++ b/backend/src/mikro-orm.config.ts @@ -1,12 +1,20 @@ -import { Options } from '@mikro-orm/core'; +import { LoggerOptions, Options } from '@mikro-orm/core'; import { PostgreSqlDriver } from '@mikro-orm/postgresql'; +import { MikroOrmLogger } from './logging/mikroOrmLogger.js'; +import { LOG_LEVEL } from './config.js'; const config: Options = { driver: PostgreSqlDriver, dbName: 'dwengo', + password: 'postgres', entities: ['dist/**/*.entity.js'], entitiesTs: ['src/**/*.entity.ts'], - debug: true, + + // Logging + debug: LOG_LEVEL === 'debug', + loggerFactory: (options: LoggerOptions) => { + return new MikroOrmLogger(options); + }, }; export default config; From ebf741a0d0e65580b52867f405457bab4dd3e31c Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Sun, 2 Mar 2025 14:08:21 +0100 Subject: [PATCH 06/16] fix(backend): Ontbrekende .js in imports --- backend/src/logging/mikroOrmLogger.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/logging/mikroOrmLogger.ts b/backend/src/logging/mikroOrmLogger.ts index 4fbadb96..49818427 100644 --- a/backend/src/logging/mikroOrmLogger.ts +++ b/backend/src/logging/mikroOrmLogger.ts @@ -1,6 +1,6 @@ import { DefaultLogger, LogContext, LoggerNamespace } from '@mikro-orm/core'; import { Logger } from 'winston'; -import { getLogger } from './initalize'; +import { getLogger } from './initalize.js'; import { LokiLabels } from 'loki-logger-ts'; export class MikroOrmLogger extends DefaultLogger { From 4b40653a1c0962e67ccf5fe53b818e07c8dd66ef Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Sun, 2 Mar 2025 14:10:31 +0100 Subject: [PATCH 07/16] fix(backend): Verander Grafana poort Verander grafana poort naar 3100 om conflict met standaard poort 3000 van backend te vermijden. --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 1f8a4c98..a348d23d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,7 +25,7 @@ services: dashboards: image: grafana/grafana:latest ports: - - '3000:3000' + - '3100:3000' volumes: - dwengo_grafana_data:/var/lib/grafana restart: unless-stopped From c2e3886f3f10ea01551b3c1cef7f3668ab6ab462 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Sun, 2 Mar 2025 15:14:26 +0100 Subject: [PATCH 08/16] chore(backend): Switch console naar logger Maak gebruik van custom logger --- backend/src/controllers/learningObjects.ts | 5 +++-- backend/src/controllers/learningPaths.ts | 3 ++- backend/src/controllers/themes.ts | 8 ++++++-- backend/src/services/learningObjects.ts | 10 +++++++--- backend/src/services/learningPaths.ts | 6 +++++- backend/src/util/apiHelper.ts | 10 ++++++---- 6 files changed, 29 insertions(+), 13 deletions(-) diff --git a/backend/src/controllers/learningObjects.ts b/backend/src/controllers/learningObjects.ts index 4295326a..90cd3f13 100644 --- a/backend/src/controllers/learningObjects.ts +++ b/backend/src/controllers/learningObjects.ts @@ -6,6 +6,7 @@ import { } from '../services/learningObjects.js'; import { FALLBACK_LANG } from '../config.js'; import { FilteredLearningObject } from '../interfaces/learningPath'; +import { getLogger } from '../logging/initalize'; export async function getAllLearningObjects( req: Request, @@ -33,7 +34,7 @@ export async function getAllLearningObjects( res.json(learningObjects); } catch (error) { - console.error('Error fetching learning objects:', error); + getLogger().error('Error fetching learning objects:', error); res.status(500).json({ error: 'Internal server error' }); } } @@ -54,7 +55,7 @@ export async function getLearningObject( const learningObject = await getLearningObjectById(hruid, language); res.json(learningObject); } catch (error) { - console.error('Error fetching learning object:', error); + getLogger().error('Error fetching learning object:', error); res.status(500).json({ error: 'Internal server error' }); } } diff --git a/backend/src/controllers/learningPaths.ts b/backend/src/controllers/learningPaths.ts index 903451be..86878bfd 100644 --- a/backend/src/controllers/learningPaths.ts +++ b/backend/src/controllers/learningPaths.ts @@ -5,6 +5,7 @@ import { fetchLearningPaths, searchLearningPaths, } from '../services/learningPaths.js'; +import { getLogger } from '../logging/initalize.js'; /** * Fetch learning paths based on query parameters. */ @@ -56,7 +57,7 @@ export async function getLearningPaths( ); res.json(learningPaths.data); } catch (error) { - console.error('❌ Unexpected error fetching learning paths:', error); + getLogger().error('❌ Unexpected error fetching learning paths:', error); res.status(500).json({ error: 'Internal server error' }); } } diff --git a/backend/src/controllers/themes.ts b/backend/src/controllers/themes.ts index 817464ab..c10f1b98 100644 --- a/backend/src/controllers/themes.ts +++ b/backend/src/controllers/themes.ts @@ -1,9 +1,13 @@ import fs from 'fs'; import path from 'path'; import yaml from 'js-yaml'; +import { Logger } from 'winston'; import { Request, Response } from 'express'; import { themes } from '../data/themes.js'; import { FALLBACK_LANG } from '../config.js'; +import { getLogger } from '../logging/initalize.js'; + +const logger: Logger = getLogger(); interface Translations { curricula_page: { @@ -17,10 +21,10 @@ function loadTranslations(language: string): Translations { const yamlFile = fs.readFileSync(filePath, 'utf8'); return yaml.load(yamlFile) as Translations; } catch (error) { - console.error( + logger.error( `Cannot load translation for: ${language}, fallen back to Dutch` ); - console.error(error); + logger.error(error); const fallbackPath = path.join(process.cwd(), '_i18n', 'nl.yml'); return yaml.load(fs.readFileSync(fallbackPath, 'utf8')) as Translations; } diff --git a/backend/src/services/learningObjects.ts b/backend/src/services/learningObjects.ts index d1d34ad2..90370fe6 100644 --- a/backend/src/services/learningObjects.ts +++ b/backend/src/services/learningObjects.ts @@ -7,6 +7,10 @@ import { LearningPathResponse, } from '../interfaces/learningPath.js'; import { fetchLearningPaths } from './learningPaths.js'; +import { getLogger } from '../logging/initalize.js'; +import { Logger } from 'winston'; + +const logger: Logger = getLogger(); function filterData( data: LearningObjectMetadata, @@ -49,7 +53,7 @@ export async function getLearningObjectById( ); if (!metadata) { - console.error(`⚠️ WARNING: Learning object "${hruid}" not found.`); + logger.error(`⚠️ WARNING: Learning object "${hruid}" not found.`); return null; } @@ -77,7 +81,7 @@ async function fetchLearningObjects( !learningPathResponse.success || !learningPathResponse.data?.length ) { - console.error( + logger.error( `⚠️ WARNING: Learning path "${hruid}" exists but contains no learning objects.` ); return []; @@ -104,7 +108,7 @@ async function fetchLearningObjects( }); }); } catch (error) { - console.error('❌ Error fetching learning objects:', error); + logger.error('❌ Error fetching learning objects:', error); return []; } } diff --git a/backend/src/services/learningPaths.ts b/backend/src/services/learningPaths.ts index 2a9f15a3..7c445806 100644 --- a/backend/src/services/learningPaths.ts +++ b/backend/src/services/learningPaths.ts @@ -4,6 +4,10 @@ import { LearningPath, LearningPathResponse, } from '../interfaces/learningPath.js'; +import { getLogger } from '../logging/initalize.js'; +import { Logger } from 'winston'; + +const logger: Logger = getLogger(); export async function fetchLearningPaths( hruids: string[], @@ -29,7 +33,7 @@ export async function fetchLearningPaths( ); if (!learningPaths || learningPaths.length === 0) { - console.error(`⚠️ WARNING: No learning paths found for ${source}.`); + logger.error(`⚠️ WARNING: No learning paths found for ${source}.`); return { success: false, source, diff --git a/backend/src/util/apiHelper.ts b/backend/src/util/apiHelper.ts index 76d166c8..4a1cae7f 100644 --- a/backend/src/util/apiHelper.ts +++ b/backend/src/util/apiHelper.ts @@ -1,6 +1,8 @@ import axios, { AxiosRequestConfig } from 'axios'; +import { getLogger } from '../logging/initalize.js'; +import { Logger } from 'winston'; -// !!!! when logger is done -> change +const logger: Logger = getLogger(); /** * Utility function to fetch data from an API endpoint with error handling. @@ -24,16 +26,16 @@ export async function fetchWithLogging( } catch (error: any) { if (error.response) { if (error.response.status === 404) { - console.error( + logger.error( `❌ ERROR: ${description} not found (404) at "${url}".` ); } else { - console.error( + logger.error( `❌ ERROR: Failed to fetch ${description}. Status: ${error.response.status} - ${error.response.statusText} (URL: "${url}")` ); } } else { - console.error( + logger.error( `❌ ERROR: Network or unexpected error when fetching ${description}:`, error.message ); From c37d4d8e0437daa0b8f0f01b0a7e759dc3d8b65e Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Sun, 2 Mar 2025 15:20:57 +0100 Subject: [PATCH 09/16] chore(backend): Export Logger class --- backend/src/app.ts | 3 +-- backend/src/controllers/themes.ts | 3 +-- backend/src/logging/initalize.ts | 13 ++++++++++++- backend/src/logging/mikroOrmLogger.ts | 3 +-- backend/src/logging/responseTimeLogger.ts | 3 +-- backend/src/orm.ts | 3 +-- backend/src/services/learningObjects.ts | 3 +-- backend/src/services/learningPaths.ts | 8 ++------ backend/src/util/apiHelper.ts | 3 +-- 9 files changed, 21 insertions(+), 21 deletions(-) diff --git a/backend/src/app.ts b/backend/src/app.ts index 2161666f..652c27d1 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -12,10 +12,9 @@ import submissionRouter from './routes/submission.js'; import classRouter from './routes/class.js'; import questionRouter from './routes/question.js'; import loginRouter from './routes/login.js'; -import { getLogger } from './logging/initalize.js'; +import { getLogger, Logger } from './logging/initalize.js'; import { responseTimeLogger } from './logging/responseTimeLogger.js'; import responseTime from 'response-time'; -import { Logger } from 'winston'; import { EnvVars, getNumericEnvVar } from './util/envvars.js'; const logger: Logger = getLogger(); diff --git a/backend/src/controllers/themes.ts b/backend/src/controllers/themes.ts index c10f1b98..e60502ed 100644 --- a/backend/src/controllers/themes.ts +++ b/backend/src/controllers/themes.ts @@ -1,11 +1,10 @@ import fs from 'fs'; import path from 'path'; import yaml from 'js-yaml'; -import { Logger } from 'winston'; import { Request, Response } from 'express'; import { themes } from '../data/themes.js'; import { FALLBACK_LANG } from '../config.js'; -import { getLogger } from '../logging/initalize.js'; +import { getLogger, Logger } from '../logging/initalize.js'; const logger: Logger = getLogger(); diff --git a/backend/src/logging/initalize.ts b/backend/src/logging/initalize.ts index 5da786fe..18166408 100644 --- a/backend/src/logging/initalize.ts +++ b/backend/src/logging/initalize.ts @@ -1,8 +1,19 @@ -import { createLogger, format, Logger, transports } from 'winston'; +import { + createLogger, + format, + Logger as WinstonLogger, + transports, +} from 'winston'; import LokiTransport from 'winston-loki'; import { LokiLabels } from 'loki-logger-ts'; import { LOG_LEVEL, LOKI_HOST } from '../config.js'; +export class Logger extends WinstonLogger { + constructor() { + super(); + } +} + const Labels: LokiLabels = { source: 'Dwengo-Backend', service: 'API', diff --git a/backend/src/logging/mikroOrmLogger.ts b/backend/src/logging/mikroOrmLogger.ts index 49818427..e8bc1fad 100644 --- a/backend/src/logging/mikroOrmLogger.ts +++ b/backend/src/logging/mikroOrmLogger.ts @@ -1,6 +1,5 @@ import { DefaultLogger, LogContext, LoggerNamespace } from '@mikro-orm/core'; -import { Logger } from 'winston'; -import { getLogger } from './initalize.js'; +import { getLogger, Logger } from './initalize.js'; import { LokiLabels } from 'loki-logger-ts'; export class MikroOrmLogger extends DefaultLogger { diff --git a/backend/src/logging/responseTimeLogger.ts b/backend/src/logging/responseTimeLogger.ts index 85723e5e..c1bb1e33 100644 --- a/backend/src/logging/responseTimeLogger.ts +++ b/backend/src/logging/responseTimeLogger.ts @@ -1,5 +1,4 @@ -import { getLogger } from './initalize.js'; -import { Logger } from 'winston'; +import { getLogger, Logger } from './initalize.js'; import { Request, Response } from 'express'; export function responseTimeLogger(req: Request, res: Response, time: number) { diff --git a/backend/src/orm.ts b/backend/src/orm.ts index 2205fe89..88decd92 100644 --- a/backend/src/orm.ts +++ b/backend/src/orm.ts @@ -1,8 +1,7 @@ import { EntityManager, MikroORM } from '@mikro-orm/core'; import config from './mikro-orm.config.js'; import { EnvVars, getEnvVar } from './util/envvars.js'; -import { getLogger } from './logging/initalize.js'; -import { Logger } from 'winston'; +import { getLogger, Logger } from './logging/initalize.js'; let orm: MikroORM | undefined; export async function initORM(testingMode: boolean = false) { diff --git a/backend/src/services/learningObjects.ts b/backend/src/services/learningObjects.ts index 90370fe6..709c3298 100644 --- a/backend/src/services/learningObjects.ts +++ b/backend/src/services/learningObjects.ts @@ -7,8 +7,7 @@ import { LearningPathResponse, } from '../interfaces/learningPath.js'; import { fetchLearningPaths } from './learningPaths.js'; -import { getLogger } from '../logging/initalize.js'; -import { Logger } from 'winston'; +import { getLogger, Logger } from '../logging/initalize.js'; const logger: Logger = getLogger(); diff --git a/backend/src/services/learningPaths.ts b/backend/src/services/learningPaths.ts index 7c445806..58703215 100644 --- a/backend/src/services/learningPaths.ts +++ b/backend/src/services/learningPaths.ts @@ -1,11 +1,7 @@ import { fetchWithLogging } from '../util/apiHelper.js'; import { DWENGO_API_BASE } from '../config.js'; -import { - LearningPath, - LearningPathResponse, -} from '../interfaces/learningPath.js'; -import { getLogger } from '../logging/initalize.js'; -import { Logger } from 'winston'; +import { LearningPath, LearningPathResponse } from '../interfaces/learningPath.js'; +import { getLogger, Logger } from '../logging/initalize.js'; const logger: Logger = getLogger(); diff --git a/backend/src/util/apiHelper.ts b/backend/src/util/apiHelper.ts index 4a1cae7f..09492615 100644 --- a/backend/src/util/apiHelper.ts +++ b/backend/src/util/apiHelper.ts @@ -1,6 +1,5 @@ import axios, { AxiosRequestConfig } from 'axios'; -import { getLogger } from '../logging/initalize.js'; -import { Logger } from 'winston'; +import { getLogger, Logger } from '../logging/initalize.js'; const logger: Logger = getLogger(); From ddee299b4ab3a325abc19ced529abf9fa47d9b04 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Sun, 2 Mar 2025 15:21:27 +0100 Subject: [PATCH 10/16] style(backend): Format --- backend/src/app.ts | 1 - backend/src/config.ts | 4 +- backend/src/controllers/learningPaths.ts | 5 ++- .../entities/assignments/assignment.entity.ts | 20 ++++++++-- .../src/entities/assignments/group.entity.ts | 13 ++++++- .../entities/assignments/submission.entity.ts | 20 ++++++++-- .../classes/class-join-request.entity.ts | 18 +++++++-- backend/src/entities/classes/class.entity.ts | 8 +++- .../classes/teacher-invitation.entity.ts | 21 ++++++++-- .../src/entities/content/attachment.entity.ts | 7 +++- .../content/learning-object.entity.ts | 33 +++++++++++++--- .../entities/content/learning-path.entity.ts | 39 ++++++++++++++++--- .../src/entities/questions/answer.entity.ts | 14 ++++++- .../src/entities/questions/question.entity.ts | 13 ++++++- backend/src/entities/users/student.entity.ts | 14 +++++-- backend/src/entities/users/teacher.entity.ts | 4 +- backend/src/routes/assignment.ts | 25 ++++-------- backend/src/routes/class.ts | 37 +++++++----------- backend/src/routes/group.ts | 21 +++++----- backend/src/routes/login.ts | 6 +-- backend/src/routes/question.ts | 24 +++++------- backend/src/routes/student.ts | 28 ++++++------- backend/src/routes/submission.ts | 11 ++---- backend/src/routes/teacher.ts | 24 ++++-------- backend/src/services/learningPaths.ts | 5 ++- 25 files changed, 265 insertions(+), 150 deletions(-) diff --git a/backend/src/app.ts b/backend/src/app.ts index 652c27d1..5769e360 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -22,7 +22,6 @@ const logger: Logger = getLogger(); const app: Express = express(); const port: string | number = getNumericEnvVar(EnvVars.Port); - app.use(express.json()); app.use(responseTime(responseTimeLogger)); diff --git a/backend/src/config.ts b/backend/src/config.ts index 831670a9..b972a1bd 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -7,6 +7,6 @@ export const DWENGO_API_BASE: string = 'https://dwengo.org/backend/api'; // Logging export const LOG_LEVEL: string = - 'development' === process.env.NODE_ENV ? 'debug' : 'info'; + 'development' === process.env.NODE_ENV ? 'debug' : 'info'; export const LOKI_HOST: string = - process.env.LOKI_HOST || 'http://localhost:3102'; + process.env.LOKI_HOST || 'http://localhost:3102'; diff --git a/backend/src/controllers/learningPaths.ts b/backend/src/controllers/learningPaths.ts index 86878bfd..247877e7 100644 --- a/backend/src/controllers/learningPaths.ts +++ b/backend/src/controllers/learningPaths.ts @@ -57,7 +57,10 @@ export async function getLearningPaths( ); res.json(learningPaths.data); } catch (error) { - getLogger().error('❌ Unexpected error fetching learning paths:', error); + getLogger().error( + '❌ Unexpected error fetching learning paths:', + error + ); res.status(500).json({ error: 'Internal server error' }); } } diff --git a/backend/src/entities/assignments/assignment.entity.ts b/backend/src/entities/assignments/assignment.entity.ts index 7909b107..89952c64 100644 --- a/backend/src/entities/assignments/assignment.entity.ts +++ b/backend/src/entities/assignments/assignment.entity.ts @@ -12,7 +12,12 @@ import { Language } from '../content/language.js'; @Entity() export class Assignment { - @ManyToOne({ entity: () => {return Class}, primary: true }) + @ManyToOne({ + entity: () => { + return Class; + }, + primary: true, + }) within!: Class; @PrimaryKey({ type: 'number' }) @@ -27,9 +32,18 @@ export class Assignment { @Property({ type: 'string' }) learningPathHruid!: string; - @Enum({ items: () => {return Language} }) + @Enum({ + items: () => { + return Language; + }, + }) learningPathLanguage!: Language; - @OneToMany({ entity: () => {return Group}, mappedBy: 'assignment' }) + @OneToMany({ + entity: () => { + return Group; + }, + mappedBy: 'assignment', + }) groups!: Group[]; } diff --git a/backend/src/entities/assignments/group.entity.ts b/backend/src/entities/assignments/group.entity.ts index a68eb5a0..5b224087 100644 --- a/backend/src/entities/assignments/group.entity.ts +++ b/backend/src/entities/assignments/group.entity.ts @@ -4,12 +4,21 @@ import { Student } from '../users/student.entity.js'; @Entity() export class Group { - @ManyToOne({ entity: () => {return Assignment}, primary: true }) + @ManyToOne({ + entity: () => { + return Assignment; + }, + primary: true, + }) assignment!: Assignment; @PrimaryKey({ type: 'integer' }) groupNumber!: number; - @ManyToMany({ entity: () => {return Student} }) + @ManyToMany({ + entity: () => { + return Student; + }, + }) members!: Student[]; } diff --git a/backend/src/entities/assignments/submission.entity.ts b/backend/src/entities/assignments/submission.entity.ts index bd0936a1..1bc28add 100644 --- a/backend/src/entities/assignments/submission.entity.ts +++ b/backend/src/entities/assignments/submission.entity.ts @@ -8,7 +8,12 @@ export class Submission { @PrimaryKey({ type: 'string' }) learningObjectHruid!: string; - @Enum({ items: () => {return Language}, primary: true }) + @Enum({ + items: () => { + return Language; + }, + primary: true, + }) learningObjectLanguage!: Language; @PrimaryKey({ type: 'string' }) @@ -17,13 +22,22 @@ export class Submission { @PrimaryKey({ type: 'integer' }) submissionNumber!: number; - @ManyToOne({ entity: () => {return Student} }) + @ManyToOne({ + entity: () => { + return Student; + }, + }) submitter!: Student; @Property({ type: 'datetime' }) submissionTime!: Date; - @ManyToOne({ entity: () => {return Group}, nullable: true }) + @ManyToOne({ + entity: () => { + return Group; + }, + nullable: true, + }) onBehalfOf?: Group; @Property({ type: 'json' }) diff --git a/backend/src/entities/classes/class-join-request.entity.ts b/backend/src/entities/classes/class-join-request.entity.ts index 86a30e3e..0ae38cd1 100644 --- a/backend/src/entities/classes/class-join-request.entity.ts +++ b/backend/src/entities/classes/class-join-request.entity.ts @@ -4,13 +4,25 @@ import { Class } from './class.entity.js'; @Entity() export class ClassJoinRequest { - @ManyToOne({ entity: () => {return Student}, primary: true }) + @ManyToOne({ + entity: () => { + return Student; + }, + primary: true, + }) requester!: Student; - @ManyToOne({ entity: () => {return Class}, primary: true }) + @ManyToOne({ + entity: () => { + return Class; + }, + primary: true, + }) class!: Class; - @Enum(() => {return ClassJoinRequestStatus}) + @Enum(() => { + return ClassJoinRequestStatus; + }) status!: ClassJoinRequestStatus; } diff --git a/backend/src/entities/classes/class.entity.ts b/backend/src/entities/classes/class.entity.ts index 2ad98c84..ecc11748 100644 --- a/backend/src/entities/classes/class.entity.ts +++ b/backend/src/entities/classes/class.entity.ts @@ -17,9 +17,13 @@ export class Class { @Property({ type: 'string' }) displayName!: string; - @ManyToMany(() => {return Teacher}) + @ManyToMany(() => { + return Teacher; + }) teachers!: Collection; - @ManyToMany(() => {return Student}) + @ManyToMany(() => { + return Student; + }) students!: Collection; } diff --git a/backend/src/entities/classes/teacher-invitation.entity.ts b/backend/src/entities/classes/teacher-invitation.entity.ts index feba8fc3..98d2bdd4 100644 --- a/backend/src/entities/classes/teacher-invitation.entity.ts +++ b/backend/src/entities/classes/teacher-invitation.entity.ts @@ -7,12 +7,27 @@ import { Class } from './class.entity.js'; */ @Entity() export class TeacherInvitation { - @ManyToOne({ entity: () => {return Teacher}, primary: true }) + @ManyToOne({ + entity: () => { + return Teacher; + }, + primary: true, + }) sender!: Teacher; - @ManyToOne({ entity: () => {return Teacher}, primary: true }) + @ManyToOne({ + entity: () => { + return Teacher; + }, + primary: true, + }) receiver!: Teacher; - @ManyToOne({ entity: () => {return Class}, primary: true }) + @ManyToOne({ + entity: () => { + return Class; + }, + primary: true, + }) class!: Class; } diff --git a/backend/src/entities/content/attachment.entity.ts b/backend/src/entities/content/attachment.entity.ts index 2ead7262..7a9dd946 100644 --- a/backend/src/entities/content/attachment.entity.ts +++ b/backend/src/entities/content/attachment.entity.ts @@ -3,7 +3,12 @@ import { LearningObject } from './learning-object.entity.js'; @Entity() export class Attachment { - @ManyToOne({ entity: () => {return LearningObject}, primary: true }) + @ManyToOne({ + entity: () => { + return LearningObject; + }, + primary: true, + }) learningObject!: LearningObject; @PrimaryKey({ type: 'integer' }) diff --git a/backend/src/entities/content/learning-object.entity.ts b/backend/src/entities/content/learning-object.entity.ts index c5bfe08f..bf499e8a 100644 --- a/backend/src/entities/content/learning-object.entity.ts +++ b/backend/src/entities/content/learning-object.entity.ts @@ -17,13 +17,22 @@ export class LearningObject { @PrimaryKey({ type: 'string' }) hruid!: string; - @Enum({ items: () => {return Language}, primary: true }) + @Enum({ + items: () => { + return Language; + }, + primary: true, + }) language!: Language; @PrimaryKey({ type: 'string' }) version: string = '1'; - @ManyToMany({ entity: () => {return Teacher} }) + @ManyToMany({ + entity: () => { + return Teacher; + }, + }) admins!: Teacher[]; @Property({ type: 'string' }) @@ -47,7 +56,12 @@ export class LearningObject { @Property({ type: 'array' }) skosConcepts!: string[]; - @Embedded({ entity: () => {return EducationalGoal}, array: true }) + @Embedded({ + entity: () => { + return EducationalGoal; + }, + array: true, + }) educationalGoals: EducationalGoal[] = []; @Property({ type: 'string' }) @@ -62,7 +76,11 @@ export class LearningObject { @Property({ type: 'integer' }) estimatedTime!: number; - @Embedded({ entity: () => {return ReturnValue} }) + @Embedded({ + entity: () => { + return ReturnValue; + }, + }) returnValue!: ReturnValue; @Property({ type: 'bool' }) @@ -71,7 +89,12 @@ export class LearningObject { @Property({ type: 'string', nullable: true }) contentLocation?: string; - @OneToMany({ entity: () => {return Attachment}, mappedBy: 'learningObject' }) + @OneToMany({ + entity: () => { + return Attachment; + }, + mappedBy: 'learningObject', + }) attachments: Attachment[] = []; @Property({ type: 'blob' }) diff --git a/backend/src/entities/content/learning-path.entity.ts b/backend/src/entities/content/learning-path.entity.ts index f758dace..28d3cadd 100644 --- a/backend/src/entities/content/learning-path.entity.ts +++ b/backend/src/entities/content/learning-path.entity.ts @@ -16,10 +16,19 @@ export class LearningPath { @PrimaryKey({ type: 'string' }) hruid!: string; - @Enum({ items: () => {return Language}, primary: true }) + @Enum({ + items: () => { + return Language; + }, + primary: true, + }) language!: Language; - @ManyToMany({ entity: () => {return Teacher} }) + @ManyToMany({ + entity: () => { + return Teacher; + }, + }) admins!: Teacher[]; @Property({ type: 'string' }) @@ -31,7 +40,12 @@ export class LearningPath { @Property({ type: 'blob' }) image!: string; - @Embedded({ entity: () => {return LearningPathNode}, array: true }) + @Embedded({ + entity: () => { + return LearningPathNode; + }, + array: true, + }) nodes: LearningPathNode[] = []; } @@ -40,7 +54,11 @@ export class LearningPathNode { @Property({ type: 'string' }) learningObjectHruid!: string; - @Enum({ items: () => {return Language} }) + @Enum({ + items: () => { + return Language; + }, + }) language!: Language; @Property({ type: 'string' }) @@ -52,7 +70,12 @@ export class LearningPathNode { @Property({ type: 'bool' }) startNode!: boolean; - @Embedded({ entity: () => {return LearningPathTransition}, array: true }) + @Embedded({ + entity: () => { + return LearningPathTransition; + }, + array: true, + }) transitions!: LearningPathTransition[]; } @@ -61,6 +84,10 @@ export class LearningPathTransition { @Property({ type: 'string' }) condition!: string; - @OneToOne({ entity: () => {return LearningPathNode} }) + @OneToOne({ + entity: () => { + return LearningPathNode; + }, + }) next!: LearningPathNode; } diff --git a/backend/src/entities/questions/answer.entity.ts b/backend/src/entities/questions/answer.entity.ts index 2690d50d..34558612 100644 --- a/backend/src/entities/questions/answer.entity.ts +++ b/backend/src/entities/questions/answer.entity.ts @@ -4,10 +4,20 @@ import { Teacher } from '../users/teacher.entity'; @Entity() export class Answer { - @ManyToOne({ entity: () => {return Teacher}, primary: true }) + @ManyToOne({ + entity: () => { + return Teacher; + }, + primary: true, + }) author!: Teacher; - @ManyToOne({ entity: () => {return Question}, primary: true }) + @ManyToOne({ + entity: () => { + return Question; + }, + primary: true, + }) toQuestion!: Question; @PrimaryKey({ type: 'integer' }) diff --git a/backend/src/entities/questions/question.entity.ts b/backend/src/entities/questions/question.entity.ts index 5830c816..444d2179 100644 --- a/backend/src/entities/questions/question.entity.ts +++ b/backend/src/entities/questions/question.entity.ts @@ -7,7 +7,12 @@ export class Question { @PrimaryKey({ type: 'string' }) learningObjectHruid!: string; - @Enum({ items: () => {return Language}, primary: true }) + @Enum({ + items: () => { + return Language; + }, + primary: true, + }) learningObjectLanguage!: Language; @PrimaryKey({ type: 'string' }) @@ -16,7 +21,11 @@ export class Question { @PrimaryKey({ type: 'integer' }) sequenceNumber!: number; - @ManyToOne({ entity: () => {return Student} }) + @ManyToOne({ + entity: () => { + return Student; + }, + }) author!: Student; @Property({ type: 'datetime' }) diff --git a/backend/src/entities/users/student.entity.ts b/backend/src/entities/users/student.entity.ts index ccfa7dfc..c5632e84 100644 --- a/backend/src/entities/users/student.entity.ts +++ b/backend/src/entities/users/student.entity.ts @@ -4,12 +4,20 @@ import { Class } from '../classes/class.entity.js'; import { Group } from '../assignments/group.entity.js'; import { StudentRepository } from '../../data/users/student-repository.js'; -@Entity({ repository: () => {return StudentRepository} }) +@Entity({ + repository: () => { + return StudentRepository; + }, +}) export class Student extends User { - @ManyToMany(() => {return Class}) + @ManyToMany(() => { + return Class; + }) classes!: Collection; - @ManyToMany(() => {return Group}) + @ManyToMany(() => { + return Group; + }) groups!: Collection; constructor( diff --git a/backend/src/entities/users/teacher.entity.ts b/backend/src/entities/users/teacher.entity.ts index eaaa8327..9f11a3b0 100644 --- a/backend/src/entities/users/teacher.entity.ts +++ b/backend/src/entities/users/teacher.entity.ts @@ -4,6 +4,8 @@ import { Class } from '../classes/class.entity.js'; @Entity() export class Teacher extends User { - @ManyToMany(() => {return Class}) + @ManyToMany(() => { + return Class; + }) classes!: Collection; } diff --git a/backend/src/routes/assignment.ts b/backend/src/routes/assignment.ts index eb49144f..4ae5756d 100644 --- a/backend/src/routes/assignment.ts +++ b/backend/src/routes/assignment.ts @@ -1,13 +1,10 @@ -import express from 'express' +import express from 'express'; const router = express.Router(); // Root endpoint used to search objects router.get('/', (req, res) => { res.json({ - assignments: [ - '0', - '1', - ] + assignments: ['0', '1'], }); }); @@ -17,7 +14,7 @@ router.get('/:id', (req, res) => { id: req.params.id, title: 'Dit is een test assignment', description: 'Een korte beschrijving', - groups: [ '0' ], + groups: ['0'], learningPath: '0', class: '0', links: { @@ -25,30 +22,24 @@ router.get('/:id', (req, res) => { submissions: `${req.baseUrl}/${req.params.id}`, }, }); -}) +}); router.get('/:id/submissions', (req, res) => { res.json({ - submissions: [ - '0' - ], + submissions: ['0'], }); }); router.get('/:id/groups', (req, res) => { res.json({ - groups: [ - '0' - ], + groups: ['0'], }); }); router.get('/:id/questions', (req, res) => { res.json({ - questions: [ - '0' - ], + questions: ['0'], }); }); -export default router \ No newline at end of file +export default router; diff --git a/backend/src/routes/class.ts b/backend/src/routes/class.ts index fa7a2310..6f8f324e 100644 --- a/backend/src/routes/class.ts +++ b/backend/src/routes/class.ts @@ -1,13 +1,10 @@ -import express from 'express' +import express from 'express'; const router = express.Router(); // Root endpoint used to search objects router.get('/', (req, res) => { res.json({ - classes: [ - '0', - '1', - ] + classes: ['0', '1'], }); }); @@ -16,40 +13,34 @@ router.get('/:id', (req, res) => { res.json({ id: req.params.id, displayName: 'Klas 4B', - teachers: [ '0' ], - students: [ '0' ], - joinRequests: [ '0' ], + teachers: ['0'], + students: ['0'], + joinRequests: ['0'], links: { self: `${req.baseUrl}/${req.params.id}`, classes: `${req.baseUrl}/${req.params.id}/invitations`, questions: `${req.baseUrl}/${req.params.id}/assignments`, students: `${req.baseUrl}/${req.params.id}/students`, - } + }, }); -}) +}); router.get('/:id/invitations', (req, res) => { res.json({ - invitations: [ - '0' - ], + invitations: ['0'], }); -}) +}); router.get('/:id/assignments', (req, res) => { res.json({ - assignments: [ - '0' - ], + assignments: ['0'], }); -}) +}); router.get('/:id/students', (req, res) => { res.json({ - students: [ - '0' - ], + students: ['0'], }); -}) +}); -export default router \ No newline at end of file +export default router; diff --git a/backend/src/routes/group.ts b/backend/src/routes/group.ts index e951a8a7..303f5215 100644 --- a/backend/src/routes/group.ts +++ b/backend/src/routes/group.ts @@ -1,13 +1,10 @@ -import express from 'express' +import express from 'express'; const router = express.Router(); // Root endpoint used to search objects router.get('/', (req, res) => { res.json({ - groups: [ - '0', - '1', - ] + groups: ['0', '1'], }); }); @@ -16,19 +13,19 @@ router.get('/:id', (req, res) => { res.json({ id: req.params.id, assignment: '0', - students: [ '0' ], - submissions: [ '0' ], + students: ['0'], + submissions: ['0'], // Reference to other endpoint // Should be less hardcoded - questions: `/group/${req.params.id}/question`, + questions: `/group/${req.params.id}/question`, }); -}) +}); // The list of questions a group has made router.get('/:id/question', (req, res) => { res.json({ - questions: [ '0' ], + questions: ['0'], }); -}) +}); -export default router \ No newline at end of file +export default router; diff --git a/backend/src/routes/login.ts b/backend/src/routes/login.ts index bc2ed3d8..33d5e6c3 100644 --- a/backend/src/routes/login.ts +++ b/backend/src/routes/login.ts @@ -1,4 +1,4 @@ -import express from 'express' +import express from 'express'; const router = express.Router(); // Returns login paths for IDP @@ -9,6 +9,6 @@ router.get('/', (req, res) => { leerkracht: '/login-leerkracht', leerling: '/login-leerling', }); -}) +}); -export default router \ No newline at end of file +export default router; diff --git a/backend/src/routes/question.ts b/backend/src/routes/question.ts index 040f742d..f683d998 100644 --- a/backend/src/routes/question.ts +++ b/backend/src/routes/question.ts @@ -1,13 +1,10 @@ -import express from 'express' +import express from 'express'; const router = express.Router(); // Root endpoint used to search objects router.get('/', (req, res) => { res.json({ - questions: [ - '0', - '1', - ] + questions: ['0', '1'], }); }); @@ -18,21 +15,20 @@ router.get('/:id', (req, res) => { student: '0', group: '0', time: new Date(2025, 1, 1), - content: 'Zijn alle gehele getallen groter dan 2 gelijk aan de som van 2 priemgetallen????', + content: + 'Zijn alle gehele getallen groter dan 2 gelijk aan de som van 2 priemgetallen????', learningObject: '0', links: { self: `${req.baseUrl}/${req.params.id}`, answers: `${req.baseUrl}/${req.params.id}/answers`, - } + }, }); -}) +}); router.get('/:id/answers', (req, res) => { res.json({ - answers: [ - '0' - ], - }) -}) + answers: ['0'], + }); +}); -export default router \ No newline at end of file +export default router; diff --git a/backend/src/routes/student.ts b/backend/src/routes/student.ts index bc3f588b..9cb0cdee 100644 --- a/backend/src/routes/student.ts +++ b/backend/src/routes/student.ts @@ -1,13 +1,10 @@ -import express from 'express' +import express from 'express'; const router = express.Router(); // Root endpoint used to search objects router.get('/', (req, res) => { res.json({ - students: [ - '0', - '1', - ] + students: ['0', '1'], }); }); @@ -30,30 +27,29 @@ router.get('/:id', (req, res) => { // The list of classes a student is in router.get('/:id/classes', (req, res) => { res.json({ - classes: [ '0' ], + classes: ['0'], }); -}) +}); // The list of submissions a student has made router.get('/:id/submissions', (req, res) => { res.json({ - submissions: [ '0' ], + submissions: ['0'], }); -}) +}); - // The list of assignments a student has router.get('/:id/assignments', (req, res) => { res.json({ - assignments: [ '0' ], + assignments: ['0'], }); -}) - +}); + // The list of groups a student is in router.get('/:id/groups', (req, res) => { res.json({ - groups: [ '0' ], + groups: ['0'], }); -}) +}); -export default router \ No newline at end of file +export default router; diff --git a/backend/src/routes/submission.ts b/backend/src/routes/submission.ts index 98acc842..cb4d3e85 100644 --- a/backend/src/routes/submission.ts +++ b/backend/src/routes/submission.ts @@ -1,13 +1,10 @@ -import express from 'express' +import express from 'express'; const router = express.Router(); // Root endpoint used to search objects router.get('/', (req, res) => { res.json({ - submissions: [ - '0', - '1', - ] + submissions: ['0', '1'], }); }); @@ -21,6 +18,6 @@ router.get('/:id', (req, res) => { content: 'Wortel 2 is rationeel', learningObject: '0', }); -}) +}); -export default router \ No newline at end of file +export default router; diff --git a/backend/src/routes/teacher.ts b/backend/src/routes/teacher.ts index f9de3aa5..a7c60bc9 100644 --- a/backend/src/routes/teacher.ts +++ b/backend/src/routes/teacher.ts @@ -1,13 +1,10 @@ -import express from 'express' +import express from 'express'; const router = express.Router(); // Root endpoint used to search objects router.get('/', (req, res) => { res.json({ - teachers: [ - '0', - '1', - ] + teachers: ['0', '1'], }); }); @@ -25,34 +22,27 @@ router.get('/:id', (req, res) => { invitations: `${req.baseUrl}/${req.params.id}/invitations`, }, }); -}) +}); // The questions students asked a teacher router.get('/:id/questions', (req, res) => { res.json({ - questions: [ - '0' - ], + questions: ['0'], }); }); // Invitations to other classes a teacher received router.get('/:id/invitations', (req, res) => { res.json({ - invitations: [ - '0' - ], + invitations: ['0'], }); }); // A list with ids of classes a teacher is in router.get('/:id/classes', (req, res) => { res.json({ - classes: [ - '0' - ], + classes: ['0'], }); }); - -export default router \ No newline at end of file +export default router; diff --git a/backend/src/services/learningPaths.ts b/backend/src/services/learningPaths.ts index 58703215..6baf74c7 100644 --- a/backend/src/services/learningPaths.ts +++ b/backend/src/services/learningPaths.ts @@ -1,6 +1,9 @@ import { fetchWithLogging } from '../util/apiHelper.js'; import { DWENGO_API_BASE } from '../config.js'; -import { LearningPath, LearningPathResponse } from '../interfaces/learningPath.js'; +import { + LearningPath, + LearningPathResponse, +} from '../interfaces/learningPath.js'; import { getLogger, Logger } from '../logging/initalize.js'; const logger: Logger = getLogger(); From 74ecd3453887cf07f6dfa7f434a20a17d868b29d Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Sun, 2 Mar 2025 15:25:27 +0100 Subject: [PATCH 11/16] fix: .js toevoegen aan imports --- backend/src/controllers/learningObjects.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/controllers/learningObjects.ts b/backend/src/controllers/learningObjects.ts index 90cd3f13..6fde1208 100644 --- a/backend/src/controllers/learningObjects.ts +++ b/backend/src/controllers/learningObjects.ts @@ -5,8 +5,8 @@ import { getLearningObjectsFromPath, } from '../services/learningObjects.js'; import { FALLBACK_LANG } from '../config.js'; -import { FilteredLearningObject } from '../interfaces/learningPath'; -import { getLogger } from '../logging/initalize'; +import { FilteredLearningObject } from '../interfaces/learningPath.js'; +import { getLogger } from '../logging/initalize.js'; export async function getAllLearningObjects( req: Request, From 57a16acbdd617da0fd1f892c0611a2d4747d9fc4 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Tue, 4 Mar 2025 16:33:37 +0100 Subject: [PATCH 12/16] chore(backend): Verander API call logging niveau Als de Dwengo API fouten geeft, dan hoeft dit maar als warning afgehandeld worden, niet als error. --- backend/src/services/learningObjects.ts | 4 ++-- backend/src/services/learningPaths.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/services/learningObjects.ts b/backend/src/services/learningObjects.ts index 709c3298..59cf6d31 100644 --- a/backend/src/services/learningObjects.ts +++ b/backend/src/services/learningObjects.ts @@ -52,7 +52,7 @@ export async function getLearningObjectById( ); if (!metadata) { - logger.error(`⚠️ WARNING: Learning object "${hruid}" not found.`); + logger.warn(`⚠️ WARNING: Learning object "${hruid}" not found.`); return null; } @@ -80,7 +80,7 @@ async function fetchLearningObjects( !learningPathResponse.success || !learningPathResponse.data?.length ) { - logger.error( + logger.warn( `⚠️ WARNING: Learning path "${hruid}" exists but contains no learning objects.` ); return []; diff --git a/backend/src/services/learningPaths.ts b/backend/src/services/learningPaths.ts index 6baf74c7..52b168ee 100644 --- a/backend/src/services/learningPaths.ts +++ b/backend/src/services/learningPaths.ts @@ -32,7 +32,7 @@ export async function fetchLearningPaths( ); if (!learningPaths || learningPaths.length === 0) { - logger.error(`⚠️ WARNING: No learning paths found for ${source}.`); + logger.warn(`⚠️ WARNING: No learning paths found for ${source}.`); return { success: false, source, From 703e00efc7f6e742a2faf32b0cec400642713a7e Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Tue, 4 Mar 2025 16:35:50 +0100 Subject: [PATCH 13/16] chore(backend): Log apiHelper op debug niveau --- backend/src/util/apiHelper.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/util/apiHelper.ts b/backend/src/util/apiHelper.ts index 09492615..83c3e975 100644 --- a/backend/src/util/apiHelper.ts +++ b/backend/src/util/apiHelper.ts @@ -25,16 +25,16 @@ export async function fetchWithLogging( } catch (error: any) { if (error.response) { if (error.response.status === 404) { - logger.error( + logger.debug( `❌ ERROR: ${description} not found (404) at "${url}".` ); } else { - logger.error( + logger.debug( `❌ ERROR: Failed to fetch ${description}. Status: ${error.response.status} - ${error.response.statusText} (URL: "${url}")` ); } } else { - logger.error( + logger.debug( `❌ ERROR: Network or unexpected error when fetching ${description}:`, error.message ); From 55ab0c0b47222de7390fcdb86bc5f8ca163902b6 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Tue, 4 Mar 2025 16:53:39 +0100 Subject: [PATCH 14/16] chore(backend): translationHelper logger --- backend/src/controllers/themes.ts | 3 +-- backend/src/util/translationHelper.ts | 11 +++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/backend/src/controllers/themes.ts b/backend/src/controllers/themes.ts index 4b59751e..a85cac21 100644 --- a/backend/src/controllers/themes.ts +++ b/backend/src/controllers/themes.ts @@ -1,7 +1,6 @@ import { Request, Response } from 'express'; import { themes } from '../data/themes.js'; -import { loadTranslations } from "../util/translationHelper.js"; -import { FALLBACK_LANG } from '../config.js'; +import { loadTranslations } from '../util/translationHelper.js'; interface Translations { curricula_page: { diff --git a/backend/src/util/translationHelper.ts b/backend/src/util/translationHelper.ts index f4443531..eaeced49 100644 --- a/backend/src/util/translationHelper.ts +++ b/backend/src/util/translationHelper.ts @@ -1,7 +1,10 @@ import fs from 'fs'; import path from 'path'; import yaml from 'js-yaml'; -import {FALLBACK_LANG} from "../../config"; +import {FALLBACK_LANG} from "../../config.js"; +import { getLogger, Logger } from '../logging/initalize.js'; + +const logger: Logger = getLogger(); export function loadTranslations(language: string): T { try { @@ -9,10 +12,10 @@ export function loadTranslations(language: string): T { const yamlFile = fs.readFileSync(filePath, 'utf8'); return yaml.load(yamlFile) as T; } catch (error) { - console.error( - `Cannot load translation for ${language}, fallen back to dutch` + logger.warn( + `Cannot load translation for ${language}, fallen back to dutch`, + error ); - console.error(error); const fallbackPath = path.join(process.cwd(), '_i18n', `${FALLBACK_LANG}.yml`); return yaml.load(fs.readFileSync(fallbackPath, 'utf8')) as T; } From 8d4cbb5529f6d162a625637d46d3005b5a1c9bed Mon Sep 17 00:00:00 2001 From: Lint Action Date: Thu, 6 Mar 2025 11:12:46 +0000 Subject: [PATCH 15/16] style: fix linting issues met ESLint --- backend/src/mikro-orm.config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/mikro-orm.config.ts b/backend/src/mikro-orm.config.ts index 83863c51..f9629bef 100644 --- a/backend/src/mikro-orm.config.ts +++ b/backend/src/mikro-orm.config.ts @@ -48,7 +48,7 @@ function config(testingMode: boolean = false): Options { driver: SqliteDriver, dbName: getEnvVar(EnvVars.DbName), entities: entities, - // entitiesTs: entitiesTs, + // EntitiesTs: entitiesTs, // Workaround: vitest: `TypeError: Unknown file extension ".ts"` (ERR_UNKNOWN_FILE_EXTENSION) // (see https://mikro-orm.io/docs/guide/project-setup#testing-the-endpoint) @@ -66,7 +66,7 @@ function config(testingMode: boolean = false): Options { user: getEnvVar(EnvVars.DbUsername), password: getEnvVar(EnvVars.DbPassword), entities: entities, - // entitiesTs: entitiesTs, + // EntitiesTs: entitiesTs, // Logging debug: LOG_LEVEL === 'debug', From 3e3c8e0587e86578dfff4a843e07b014397fe4b6 Mon Sep 17 00:00:00 2001 From: Lint Action Date: Thu, 6 Mar 2025 11:12:49 +0000 Subject: [PATCH 16/16] style: fix linting issues met Prettier --- backend/tests/data/users.test.ts | 6 ++++-- config/loki/config.yml | 1 - 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/tests/data/users.test.ts b/backend/tests/data/users.test.ts index b066b059..887748a2 100644 --- a/backend/tests/data/users.test.ts +++ b/backend/tests/data/users.test.ts @@ -20,7 +20,8 @@ describe('StudentRepository', () => { new Student(username, firstName, lastName) ); - const retrievedStudent = await studentRepository.findByUsername(username); + const retrievedStudent = + await studentRepository.findByUsername(username); expect(retrievedStudent).toBeTruthy(); expect(retrievedStudent?.firstName).toBe(firstName); expect(retrievedStudent?.lastName).toBe(lastName); @@ -29,7 +30,8 @@ describe('StudentRepository', () => { it('should no longer return the queried student after he was removed again', async () => { await studentRepository.deleteByUsername(username); - const retrievedStudent = await studentRepository.findByUsername(username); + const retrievedStudent = + await studentRepository.findByUsername(username); expect(retrievedStudent).toBeNull(); }); }); diff --git a/config/loki/config.yml b/config/loki/config.yml index f5ba3799..b84377bd 100644 --- a/config/loki/config.yml +++ b/config/loki/config.yml @@ -1,4 +1,3 @@ - # This is a complete configuration to deploy Loki backed by the filesystem. # The index will be shipped to the storage via tsdb-shipper.