Compare commits

...

2 Commits

Author SHA1 Message Date
ab42660e80 Implement configuration management and dependency injection framework
- Added ConfigurationReader for loading dependency definitions
- Introduced FileConfigurationLoader for file-based configuration loading
- Created IDependencyManager and IDependencyProvider interfaces for managing dependencies
- Implemented DependencyProvider for providing instances based on lifecycle
- Added Default class for logging level configuration
- Enhanced HealthCheckApi with improved route setup and health check handling
2025-11-15 05:00:02 -05:00
ce85dd1ead Enhance API routing and health check functionality
- Added ApiRouteTemplate as a base class for defining API routes
- Implemented HealthCheckApi for health check endpoint management
- Introduced IApiRoute interface for consistent route handling
- Updated UserApiRoute to align with new routing structure
- Added HealthCheck class for health status retrieval
- Improved dependency management with new types and interfaces
2025-11-10 22:12:41 -05:00
27 changed files with 435 additions and 75 deletions

View File

@@ -1,6 +1,6 @@
import { describe, it, expect, beforeEach } from 'bun:test';
import { DependencyManager } from '../../src/di/DependencyManager';
import { Logger, Database, UserService } from '../mocks';
import {describe, it, expect, beforeEach} from 'bun:test';
import {DependencyManager} from '../../src/di/DependencyManager';
import {Logger, Database, UserService} from '../mocks';
describe('When Managing Dependencies', () => {
let dependencyManager: DependencyManager;

View File

@@ -1,18 +1,24 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "myoopapp",
"dependencies": {
"@techniker-me/tools": "2025.0.16",
},
"devDependencies": {
"@eslint/js": "^9.38.0",
"@eslint/json": "^0.13.2",
"@eslint/markdown": "^7.5.0",
"@eslint/js": "9.38.0",
"@eslint/json": "0.13.2",
"@eslint/markdown": "7.5.0",
"@types/bun": "latest",
"eslint": "^9.38.0",
"globals": "^16.4.0",
"jiti": "^2.6.1",
"@types/express": "5.0.5",
"@types/morgan": "1.9.10",
"eslint": "9.38.0",
"globals": "16.4.0",
"jiti": "2.6.1",
"prettier": "3.6.2",
"typescript-eslint": "^8.46.2",
"typescript-eslint": "8.46.2",
},
"peerDependencies": {
"typescript": "5.9.3",
@@ -26,7 +32,7 @@
"@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="],
"@eslint/config-helpers": ["@eslint/config-helpers@0.4.1", "", { "dependencies": { "@eslint/core": "^0.16.0" } }, "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw=="],
"@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="],
"@eslint/core": ["@eslint/core@0.15.2", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg=="],
@@ -58,21 +64,45 @@
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
"@types/bun": ["@types/bun@1.3.1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="],
"@techniker-me/tools": ["@techniker-me/tools@2025.0.16", "https://npm.techniker.me/@techniker-me/tools/-/tools-2025.0.16.tgz", {}, "sha512-Ul2yj1vd4lCO8g7IW2pHkAsdeRVEUMqGpiIvSedCc1joVXEWPbh4GESW83kMHtisjFjjlZIzb3EVlCE0BCiBWQ=="],
"@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
"@types/bun": ["@types/bun@1.3.2", "", { "dependencies": { "bun-types": "1.3.2" } }, "sha512-t15P7k5UIgHKkxwnMNkJbWlh/617rkDGEdSsDbu+qNHTaz9SKf7aC8fiIlUdD5RPpH6GEkP0cK7WlvmrEBRtWg=="],
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/express": ["@types/express@5.0.5", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^1" } }, "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ=="],
"@types/express-serve-static-core": ["@types/express-serve-static-core@5.1.0", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA=="],
"@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
"@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="],
"@types/morgan": ["@types/morgan@1.9.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA=="],
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
"@types/node": ["@types/node@24.9.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA=="],
"@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
"@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
"@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="],
"@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
"@types/react": ["@types/react@19.2.5", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-keKxkZMqnDicuvFoJbzrhbtdLSPhj/rZThDlKWCDbgXmUg0rEUFtRssDXKYmtXluZlIqiC5VqkCgRwzuyLHKHw=="],
"@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="],
"@types/serve-static": ["@types/serve-static@1.15.10", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw=="],
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
@@ -112,7 +142,7 @@
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="],
"bun-types": ["bun-types@1.3.2", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-i/Gln4tbzKNuxP70OWhJRZz1MRfvqExowP7U6JKoI8cntFrtxg7RJK3jvz7wQW54UuvNC8tbKHHri5fy74FVqg=="],
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
@@ -130,7 +160,7 @@
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"csstype": ["csstype@3.2.0", "", {}, "sha512-si++xzRAY9iPp60roQiFta7OFbhrgvcthrhlNAGeQptSY25uJjkfUV8OArC3KLocB8JT8ohz+qgxWCmz8RhjIg=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
@@ -210,7 +240,7 @@
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
@@ -392,13 +422,15 @@
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"@eslint/config-helpers/@eslint/core": ["@eslint/core@0.16.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q=="],
"@eslint/config-helpers/@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
"@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
"@eslint/markdown/@eslint/core": ["@eslint/core@0.16.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q=="],
"@eslint/markdown/@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.0", "", { "dependencies": { "@eslint/core": "^0.16.0", "levn": "^0.4.1" } }, "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A=="],
"@eslint/markdown/@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
"@types/serve-static/@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="],
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
@@ -406,7 +438,7 @@
"eslint/@eslint/core": ["@eslint/core@0.16.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q=="],
"eslint/@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.0", "", { "dependencies": { "@eslint/core": "^0.16.0", "levn": "^0.4.1" } }, "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A=="],
"eslint/@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
@@ -414,6 +446,10 @@
"mdast-util-frontmatter/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
"@eslint/markdown/@eslint/plugin-kit/@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"eslint/@eslint/plugin-kit/@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
}
}

View File

@@ -24,6 +24,7 @@
"lint": "eslint --max-warnings 0 .",
"prelint:fix": "bun run format",
"lint:fix": "eslint --fix .",
"typecheck": "tsc --noEmit",
"test": "bun test",
"test:watch": "bun test --watch"
},

View File

@@ -1,6 +1,6 @@
import { LoggingLevel } from './logger/LoggingLevel';
import {LoggingLevel} from './logger/LoggingLevel';
export default class Defaults {
export default class Default {
static get loggingLevel(): LoggingLevel {
return LoggingLevel.Info;
}

View File

@@ -0,0 +1,25 @@
import type {RoutePath, RouteHandler} from '../net/http/HttpRouteManger.js';
import type {IApiRoute} from './IApiRoute';
export abstract class ApiRouteTemplate implements IApiRoute {
private readonly _getRoutes: Record<string, RouteHandler> = {};
private readonly _postRoutes: Record<string, RouteHandler> = {};
private readonly _putRoutes: Record<string, RouteHandler> = {};
private readonly _deleteRoutes: Record<string, RouteHandler> = {};
public getGETRoutes(): Record<RoutePath, RouteHandler> {
return this._getRoutes;
}
public getPOSTRoutes(): Record<RoutePath, RouteHandler> {
return this._postRoutes;
}
public getPUTRoutes(): Record<RoutePath, RouteHandler> {
return this._putRoutes;
}
public getDELETERoutes(): Record<RoutePath, RouteHandler> {
return this._deleteRoutes;
}
}

60
src/api/HealthCheckApi.ts Normal file
View File

@@ -0,0 +1,60 @@
import type {HealthCheck} from '../health/HealthCheck.js';
import type {RoutePath, RouteHandler} from '../net/http/HttpRouteManger.js';
import type {IApiRoute} from './IApiRoute';
import type express from 'express';
export class HealthCheckApi implements IApiRoute {
private readonly _getRoutes: Record<string, RouteHandler>;
private readonly _postRoutes: Record<string, RouteHandler>;
private readonly _putRoutes: Record<string, RouteHandler>;
private readonly _deleteRoutes: Record<string, RouteHandler>;
private readonly _healthCheck: HealthCheck;
constructor(healthCheck: HealthCheck) {
this._healthCheck = healthCheck;
this._getRoutes = this.setupGETRoutes();
this._postRoutes = this.setupPOSTRoutes();
this._putRoutes = this.setupPUTRoutes();
this._deleteRoutes = this.setupDELETERoutes();
}
public getGETRoutes(): Record<RoutePath, RouteHandler> {
return this._getRoutes;
}
public getPOSTRoutes(): Record<RoutePath, RouteHandler> {
return this._postRoutes;
}
public getPUTRoutes(): Record<RoutePath, RouteHandler> {
return this._putRoutes;
}
public getDELETERoutes(): Record<RoutePath, RouteHandler> {
return this._deleteRoutes;
}
private setupGETRoutes(): Record<RoutePath, RouteHandler> {
return {
'/_health': this.getHealth.bind(this)
};
}
private setupPOSTRoutes(): Record<RoutePath, RouteHandler> {
return {};
}
private setupPUTRoutes(): Record<RoutePath, RouteHandler> {
return {};
}
private setupDELETERoutes(): Record<RoutePath, RouteHandler> {
return {};
}
private getHealth(req: express.Request, res: express.Response): void {
const health = this._healthCheck.getHealth();
res.send(health);
}
}

8
src/api/IApiRoute.ts Normal file
View File

@@ -0,0 +1,8 @@
import type {RouteHandler, RoutePath} from '../net/http/HttpRouteManger';
export interface IApiRoute {
getGETRoutes(): Record<RoutePath, RouteHandler>;
getPOSTRoutes(): Record<RoutePath, RouteHandler>;
getPUTRoutes(): Record<RoutePath, RouteHandler>;
getDELETERoutes(): Record<RoutePath, RouteHandler>;
}

View File

@@ -1,25 +1,25 @@
import type { RouteHandler } from "../net/http/HttpRouteManger";
import type {RoutePath, RouteHandler} from '../net/http/HttpRouteManger';
import type {IApiRoute} from './IApiRoute';
export class UserApiRoute implements IApiRoute {
private readonly _getRoutes: Record<string, RouteHandler> = {};
private readonly _postRoutes: Record<string, RouteHandler> = {};
private readonly _putRoutes: Record<string, RouteHandler> = {};
private readonly _deleteRoutes: Record<string, RouteHandler> = {};
export class UserApiRoute {
private readonly _getRoutes: Record<string, RouteHandler> = {};
private readonly _postRoutes: Record<string, RouteHandler> = {};
private readonly _putRoutes: Record<string, RouteHandler> = {};
private readonly _deleteRoutes: Record<string, RouteHandler> = {};
public getGETRoutes(): Record<RoutePath, RouteHandler> {
return this._getRoutes;
}
public getGETRoutes(): Record<string, RouteHandler> {
return this._getRoutes;
}
public getPOSTRoutes(): Record<RoutePath, RouteHandler> {
return this._postRoutes;
}
public getPOSTRoutes(): Record<string, RouteHandler> {
return this._postRoutes;
}
public getPUTRoutes(): Record<RoutePath, RouteHandler> {
return this._putRoutes;
}
public getPUTRoutes(): Record<string, RouteHandler> {
return this._putRoutes;
}
public getDELETERoutes(): Record<string, RouteHandler> {
return this._deleteRoutes;
}
public getDELETERoutes(): Record<RoutePath, RouteHandler> {
return this._deleteRoutes;
}
}

View File

@@ -0,0 +1,104 @@
import type { IConfigurationLoader } from "./IConfigurationLoader";
import type { IDependencyManager } from "./IDependencyManger";
import { NamedType } from "./NamedType";
import { Type } from "./Type";
import { DependencyProvider } from "./providers/DependencyProvider";
export type Definition = {
include?: string;
class?: string;
name?: string;
inject?: Array<string | Definition>;
instanceType?: string;
lifecycle?: string;
eager?: boolean;
}
export class ConfigurationReader {
private readonly _dependencyManager: IDependencyManager;
private readonly _configurationLoader: IConfigurationLoader;
constructor(dependencyManager: IDependencyManager, configurationLoader: IConfigurationLoader) {
this._dependencyManager = dependencyManager;
this._configurationLoader = configurationLoader;
}
public load(definitions: Definition[] | Definition): void {
const getType = (definition: Definition): Type | NamedType => {
if (typeof definition.class !== 'string' || definition.class === '') {
throw new Error('Definition must define a class (' + JSON.stringify(definition) + ')');
}
let type: Type | NamedType;
if (definition.name !== undefined) {
type = new NamedType(definition.name, definition.class);
} else {
type = new Type(definition.class);
}
return type;
};
if (!Array.isArray(definitions)) {
definitions = [definitions];
}
definitions.forEach((definition, idx) => {
if (definition.include) {
const importDefinitions = this._configurationLoader.load(definition.include);
if (!Array.isArray(importDefinitions)) {
throw new Error('Invalid import: ' + definition.include);
}
this.load(importDefinitions);
return;
}
if (typeof definition.class !== 'string') {
throw new Error('Definition must define a class (' + idx + ', ' + JSON.stringify(definition) + ')');
}
const type = getType(definition);
const dependencies: Type[] = [];
if (definition.inject) {
if (!Array.isArray(definition.inject)) {
throw new Error('Injection must an array (' + idx + ', ' + JSON.stringify(definition) + ')');
}
definition.inject.forEach(dependency => {
let depType: Type | NamedType;
if (typeof dependency === 'object') {
depType = getType(dependency);
} else {
depType = new Type(dependency);
}
dependencies.push(depType);
});
}
const instanceType = definition.instanceType ? new Type(definition.instanceType) : type;
const lifecycle = definition.lifecycle ? definition.lifecycle : 'instance';
const provider = new DependencyProvider(this._dependencyManager, type, instanceType, lifecycle);
this._dependencyManager.defineDependencies(type, dependencies);
this._dependencyManager.addProvider(provider);
if (definition.eager) {
this._dependencyManager.addEagerTypes(type);
}
});
}
public toString(): string {
return 'ConfigurationReader';
}
}

View File

@@ -7,7 +7,7 @@ type Registration = {
/**
* Lifecycle types:
* - 'singleton': A single shared instance cached for the lifetime of the manager
* - 'sergice': A single shared instance cached for the lifetime of the manager
* - 'service': A single shared instance cached for the lifetime of the manager
* - 'transient': A new instance created on each resolve (transient)
*/
type Lifecycle = 'transient' | 'singleton';
@@ -62,7 +62,7 @@ export class DependencyManager {
}
const registration = this._registrations.get(name);
const { Class, dependencies, lifecycle } = registration!;
const {Class, dependencies, lifecycle} = registration!;
if (lifecycle === 'singleton' && this._singletons.has(name)) {
console.debug(`↻ Using cached singleton [${name}]`);

View File

@@ -0,0 +1,42 @@
import path from 'node:path';
import fs from 'node:fs';
import type {IConfigurationLoader} from './IConfigurationLoader';
export default class FileConfigurationLoader implements IConfigurationLoader {
private readonly _baseDirectory: string;
constructor(baseDirectory: string) {
this._baseDirectory = baseDirectory;
}
public load(name: string) {
let config = path.join(this._baseDirectory, name);
if (!fs.existsSync(config) || !fs.lstatSync(config).isFile()) {
config = path.join(this._baseDirectory, name + '.json');
}
if (!fs.existsSync(config) || !fs.lstatSync(config).isFile()) {
config = path.join(this._baseDirectory, name, 'di.json');
}
if (!fs.existsSync(config) || !fs.lstatSync(config).isFile()) {
throw new Error('DI Configuration "' + name + '" not found');
}
try {
const diConfigText = fs.readFileSync(config);
const diConfig = JSON.parse(diConfigText.toString('utf8'));
return diConfig;
} catch (e) {
console.error(`Failed to load "${name}" from file "${config}": ${e}`);
throw e;
}
}
public toString(): string {
return 'FileConfigurationLoader';
}
}

View File

@@ -0,0 +1,6 @@
import type { Definition } from "./ConfigurationReader";
export interface IConfigurationLoader {
load(name: string): Definition | Definition[];
toString(): string;
}

View File

@@ -0,0 +1,13 @@
import type { IDependencyProvider } from "./providers/IDependencyProvider";
import type { Type } from "./Type";
export interface IDependencyManager {
addProvider(provider: IDependencyProvider): void;
defineDependencies(type: Type, dependencies: Type[]): void;
resolveProvider(type: Type): IDependencyProvider;
instantiateType(type: Type): unknown;
addEagerTypes(type: Type): void;
getEagerTypes(): Type[];
getTypes(): Type[];
toString(): string;
}

View File

@@ -1,4 +1,4 @@
import { Type } from "./Type";
import {Type} from './Type';
export class NamedType extends Type {
private readonly _name: string;

View File

@@ -0,0 +1,53 @@
import type { IDependencyProvider } from './IDependencyProvider';
import type { IDependencyManager } from '../IDependencyManger';
import type { Type } from '../Type';
import type { NamedType } from '../NamedType';
export class DependencyProvider implements IDependencyProvider {
private readonly _dependencyManager: IDependencyManager;
private readonly _type: Type | NamedType;
private readonly _instanceType: Type | NamedType;
private readonly _lifecycle: string;
private _instance: unknown = null;
constructor(
dependencyManager: IDependencyManager,
type: Type | NamedType,
instanceType: Type | NamedType,
lifecycle: string
) {
this._dependencyManager = dependencyManager;
this._type = type;
this._instanceType = instanceType;
this._lifecycle = lifecycle;
}
public canProvide(type: Type | NamedType): boolean {
// Handle NamedType which has equals(), otherwise use equal() from Type
if ('equals' in this._type && typeof this._type.equals === 'function') {
return (this._type as NamedType).equals(type as NamedType);
}
return (this._type as Type).equal(type as Type);
}
public provide(type: Type | NamedType): unknown {
if (!this.canProvide(type)) {
throw new Error(`Cannot provide type: ${type.toString()}`);
}
// Singleton lifecycle - reuse the same instance
if (this._lifecycle === 'singleton') {
if (this._instance === null) {
this._instance = this._dependencyManager.instantiateType(this._instanceType);
}
return this._instance;
}
// Instance lifecycle - create a new instance every time
return this._dependencyManager.instantiateType(this._instanceType);
}
public toString(): string {
return `DependencyProvider[${this._type.toString()}]`;
}
}

View File

@@ -1,5 +1,5 @@
import type { Type } from "../Type";
import type { NamedType } from "../NamedType";
import type {Type} from '../Type';
import type {NamedType} from '../NamedType';
export interface IDependencyProvider {
canProvide(type: Type | NamedType): boolean;

13
src/health/HealthCheck.ts Normal file
View File

@@ -0,0 +1,13 @@
import type {ILogger} from '../logger/ILogger';
import LoggerFactory from '../logger/LoggerFactory';
export class HealthCheck {
private readonly _logger: ILogger = LoggerFactory.getLogger('HealthCheck');
public getHealth() {
return {
status: 'ok',
cpu: process.cpuUsage()
};
}
}

View File

@@ -1,7 +1,7 @@
export interface ILogger {
info(message: string, ...optionalParams: unknown[]): void;
debug(message: string, ...optionalParams: unknown[]): void;
warn(message: string, ...optionalParams: unknown[]): void;
error(message: string, ...optionalParams: unknown[]): void;
trace(message: string, ...optionalParams: unknown[]): void;
info(message: string, ...optionalParams: unknown[]): void;
debug(message: string, ...optionalParams: unknown[]): void;
warn(message: string, ...optionalParams: unknown[]): void;
error(message: string, ...optionalParams: unknown[]): void;
trace(message: string, ...optionalParams: unknown[]): void;
}

View File

@@ -1,7 +1,7 @@
import type { ILogger } from './ILogger';
import { LoggingLevel } from './LoggingLevel';
import type { Threshold } from './Threshold';
import type { IAppender } from './appenders/IAppender';
import type {ILogger} from './ILogger';
import {LoggingLevel} from './LoggingLevel';
import type {Threshold} from './Threshold';
import type {IAppender} from './appenders/IAppender';
import LoggingLevelMapping from './LoggingLevelMapping';
export default class Logger implements ILogger {

View File

@@ -1,10 +1,10 @@
import type { IAppender } from "./appenders/IAppender";
import type {IAppender} from './appenders/IAppender';
import Logger from './Logger';
import type { LoggingLevelType } from "./LoggingLevel";
import LoggingLevelMapping from "./LoggingLevelMapping";
import { Threshold } from "./Threshold";
import ConsoleAppender from "./appenders/ConsoleAppender";
import Disposable from "../lang/disposables/Disposable";
import type {LoggingLevelType} from './LoggingLevel';
import LoggingLevelMapping from './LoggingLevelMapping';
import {Threshold} from './Threshold';
import ConsoleAppender from './appenders/ConsoleAppender';
import Disposable from '../lang/disposables/Disposable';
type Category = string;

View File

@@ -1,4 +1,3 @@
import {assertUnreachable} from '@techniker-me/tools';
import {LoggingLevel, type LoggingLevelType} from './LoggingLevel';

View File

@@ -1,6 +1,6 @@
import Defaults from '../Defaults';
import { Subject } from '../lang/observables';
import { LoggingLevel } from './LoggingLevel';
import Defaults from '../Default';
import {Subject} from '../lang/observables';
import {LoggingLevel} from './LoggingLevel';
export class Threshold {
private _threshold: Subject<LoggingLevel>;

View File

@@ -1,4 +1,4 @@
import {LoggingLevel, type LoggingLevelType} from '../LoggingLevel';
import {type LoggingLevelType} from '../LoggingLevel';
import type ILogMessage from './LogMessage';
export type AppenderOptions = {
@@ -7,7 +7,7 @@ export type AppenderOptions = {
export default class Appender {
private readonly _logRecorderUrl: string = 'https://logserver.techniker.me/api/logs';
// @ts-ignore
// @ts-expect-error browser vs node
private readonly _domain: string = typeof globalThis !== 'undefined' ? (globalThis.location?.hostname ?? '') : '';
private readonly _logMessageQueue: ILogMessage[] = [];
private _pendingPostLogMessagePromise: Promise<Response | undefined> | undefined = undefined;

View File

@@ -1,5 +1,5 @@
import Appender from './Appender';
import type { AppenderOptions } from './Appender';
import type {AppenderOptions} from './Appender';
export default class AppnederFactory {
public static createRemoteAppender(remoteAppenderUrl: string, {domain}: AppenderOptions): Appender {

View File

@@ -1,4 +1,4 @@
import type { LoggingLevelType } from '../LoggingLevel';
import type {LoggingLevelType} from '../LoggingLevel';
export interface IAppender {
log(timestamp: string, level: LoggingLevelType, category: string, message: string): void;

View File

@@ -1,10 +1,10 @@
import type {LoggingLevel, LoggingLevelType} from '../LoggingLevel';
import type {LoggingLevelType} from '../LoggingLevel';
import type {IAppender} from './IAppender';
import type ILogMessage from './LogMessage';
export default class TechnikerMeAppender implements IAppender {
private readonly _logRecorderUrl: string = 'https://logserver.techniker.me/api/logs';
// @ts-ignore
// @ts-expect-error browser vs node
private readonly _domain: string = typeof globalThis !== 'undefined' ? (globalThis.location?.hostname ?? '') : '';
private readonly _logMessageQueue: ILogMessage[] = [];
private _pendingPostLogMessagePromise: Promise<Response | undefined> | undefined = undefined;