Compare commits
9 Commits
63dc90a8c8
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| ab42660e80 | |||
| ce85dd1ead | |||
| f6d0afb98a | |||
| d332e08665 | |||
| 64407db695 | |||
| 1587ed7428 | |||
| cb34256276 | |||
| 3fe157541c | |||
| e18e3523e5 |
3
.npmrc
Normal file
3
.npmrc
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package-lock=false
|
||||||
|
save-exact=true
|
||||||
|
@techniker-me:registry=https://npm.techniker.me
|
||||||
@@ -1,365 +0,0 @@
|
|||||||
import {describe, test, expect, beforeEach} from 'bun:test';
|
|
||||||
import {DependencyManager} from '../../src/di/DependencyManager';
|
|
||||||
|
|
||||||
// Test classes for dependency injection
|
|
||||||
class Logger {
|
|
||||||
log(message: string) {
|
|
||||||
return `LOG: ${message}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Database {
|
|
||||||
constructor(private logger: Logger) {}
|
|
||||||
|
|
||||||
query(sql: string) {
|
|
||||||
this.logger.log(`Executing: ${sql}`);
|
|
||||||
return 'result';
|
|
||||||
}
|
|
||||||
|
|
||||||
getLogger() {
|
|
||||||
return this.logger;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class UserService {
|
|
||||||
constructor(
|
|
||||||
private db: Database,
|
|
||||||
private logger: Logger
|
|
||||||
) {}
|
|
||||||
|
|
||||||
getUser(id: number) {
|
|
||||||
this.logger.log(`Getting user ${id}`);
|
|
||||||
return this.db.query(`SELECT * FROM users WHERE id = ${id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
getDb() {
|
|
||||||
return this.db;
|
|
||||||
}
|
|
||||||
|
|
||||||
getLogger() {
|
|
||||||
return this.logger;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class CircularA {
|
|
||||||
constructor(public b: CircularB) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
class CircularB {
|
|
||||||
constructor(public a: CircularA) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('DependencyManager', () => {
|
|
||||||
let dm: DependencyManager;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
dm = new DependencyManager();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('register', () => {
|
|
||||||
test('should register a class without dependencies', () => {
|
|
||||||
expect(() => {
|
|
||||||
dm.register('Logger', Logger, []);
|
|
||||||
}).not.toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should register a class with dependencies', () => {
|
|
||||||
dm.register('Logger', Logger, []);
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
dm.register('Database', Database, ['Logger']);
|
|
||||||
}).not.toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should throw error when registering duplicate name', () => {
|
|
||||||
dm.register('Logger', Logger, []);
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
dm.register('Logger', Logger, []);
|
|
||||||
}).toThrow('Error: [Logger] is already registered');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should throw error for invalid lifecycle', () => {
|
|
||||||
expect(() => {
|
|
||||||
dm.register('Logger', Logger, [], 'invalid' as any);
|
|
||||||
}).toThrow('Invalid lifecycle [invalid]');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should accept valid lifecycle types', () => {
|
|
||||||
expect(() => {
|
|
||||||
dm.register('Logger1', Logger, [], 'singleton');
|
|
||||||
}).not.toThrow();
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
dm.register('Logger2', Logger, [], 'instance');
|
|
||||||
}).not.toThrow();
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
dm.register('Logger3', Logger, [], 'service');
|
|
||||||
}).not.toThrow();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('resolve', () => {
|
|
||||||
test('should resolve a simple class without dependencies', () => {
|
|
||||||
dm.register('Logger', Logger, []);
|
|
||||||
|
|
||||||
const logger = dm.resolve<Logger>('Logger');
|
|
||||||
|
|
||||||
expect(logger).toBeInstanceOf(Logger);
|
|
||||||
expect(logger.log('test')).toBe('LOG: test');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should throw error when resolving unregistered dependency', () => {
|
|
||||||
expect(() => {
|
|
||||||
dm.resolve('NonExistent');
|
|
||||||
}).toThrow('Error: Unable to resolve unregistered [NonExistent]');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should resolve class with dependencies', () => {
|
|
||||||
dm.register('Logger', Logger, []);
|
|
||||||
dm.register('Database', Database, ['Logger']);
|
|
||||||
|
|
||||||
const db = dm.resolve<Database>('Database');
|
|
||||||
|
|
||||||
expect(db).toBeInstanceOf(Database);
|
|
||||||
expect(db.getLogger()).toBeInstanceOf(Logger);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should resolve nested dependencies', () => {
|
|
||||||
dm.register('Logger', Logger, []);
|
|
||||||
dm.register('Database', Database, ['Logger']);
|
|
||||||
dm.register('UserService', UserService, ['Database', 'Logger']);
|
|
||||||
|
|
||||||
const userService = dm.resolve<UserService>('UserService');
|
|
||||||
|
|
||||||
expect(userService).toBeInstanceOf(UserService);
|
|
||||||
expect(userService.getDb()).toBeInstanceOf(Database);
|
|
||||||
expect(userService.getLogger()).toBeInstanceOf(Logger);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('singleton lifecycle', () => {
|
|
||||||
test('should return same instance for singleton lifecycle', () => {
|
|
||||||
dm.register('Logger', Logger, [], 'singleton');
|
|
||||||
|
|
||||||
const logger1 = dm.resolve<Logger>('Logger');
|
|
||||||
const logger2 = dm.resolve<Logger>('Logger');
|
|
||||||
|
|
||||||
expect(logger1).toBe(logger2); // Same instance
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should inject same singleton instance into multiple dependents', () => {
|
|
||||||
dm.register('Logger', Logger, [], 'singleton');
|
|
||||||
dm.register('Database', Database, ['Logger']);
|
|
||||||
dm.register('UserService', UserService, ['Database', 'Logger']);
|
|
||||||
|
|
||||||
const userService = dm.resolve<UserService>('UserService');
|
|
||||||
const db = userService.getDb();
|
|
||||||
const loggerFromUserService = userService.getLogger();
|
|
||||||
const loggerFromDatabase = db.getLogger();
|
|
||||||
|
|
||||||
expect(loggerFromUserService).toBe(loggerFromDatabase); // Same singleton instance
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should use singleton by default', () => {
|
|
||||||
dm.register('Logger', Logger, []); // No lifecycle specified
|
|
||||||
|
|
||||||
const logger1 = dm.resolve<Logger>('Logger');
|
|
||||||
const logger2 = dm.resolve<Logger>('Logger');
|
|
||||||
|
|
||||||
expect(logger1).toBe(logger2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('instance/service lifecycle (transient)', () => {
|
|
||||||
test('should return new instance each time for instance lifecycle', () => {
|
|
||||||
dm.register('Logger', Logger, [], 'instance');
|
|
||||||
|
|
||||||
const logger1 = dm.resolve<Logger>('Logger');
|
|
||||||
const logger2 = dm.resolve<Logger>('Logger');
|
|
||||||
|
|
||||||
expect(logger1).not.toBe(logger2); // Different instances
|
|
||||||
expect(logger1).toBeInstanceOf(Logger);
|
|
||||||
expect(logger2).toBeInstanceOf(Logger);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should return new instance each time for service lifecycle', () => {
|
|
||||||
dm.register('Logger', Logger, [], 'service');
|
|
||||||
|
|
||||||
const logger1 = dm.resolve<Logger>('Logger');
|
|
||||||
const logger2 = dm.resolve<Logger>('Logger');
|
|
||||||
|
|
||||||
expect(logger1).not.toBe(logger2); // Different instances
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should inject new instances into each dependent', () => {
|
|
||||||
dm.register('Logger', Logger, [], 'instance');
|
|
||||||
dm.register('Database', Database, ['Logger']);
|
|
||||||
dm.register('UserService', UserService, ['Database', 'Logger']);
|
|
||||||
|
|
||||||
const userService = dm.resolve<UserService>('UserService');
|
|
||||||
const loggerFromUserService = userService.getLogger();
|
|
||||||
const loggerFromDatabase = userService.getDb().getLogger();
|
|
||||||
|
|
||||||
expect(loggerFromUserService).not.toBe(loggerFromDatabase); // Different instances
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('circular dependency detection', () => {
|
|
||||||
test('should detect direct circular dependency', () => {
|
|
||||||
dm.register('CircularA', CircularA, ['CircularB']);
|
|
||||||
dm.register('CircularB', CircularB, ['CircularA']);
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
dm.resolve('CircularA');
|
|
||||||
}).toThrow('Circular dependency detected');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should detect self-referencing circular dependency', () => {
|
|
||||||
dm.register('SelfRef', Logger, ['SelfRef']);
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
dm.resolve('SelfRef');
|
|
||||||
}).toThrow('Circular dependency detected: [SelfRef]');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should detect indirect circular dependency chain', () => {
|
|
||||||
class A {
|
|
||||||
constructor(public b: any) {}
|
|
||||||
}
|
|
||||||
class B {
|
|
||||||
constructor(public c: any) {}
|
|
||||||
}
|
|
||||||
class C {
|
|
||||||
constructor(public a: any) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
dm.register('A', A, ['B']);
|
|
||||||
dm.register('B', B, ['C']);
|
|
||||||
dm.register('C', C, ['A']);
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
dm.resolve('A');
|
|
||||||
}).toThrow('Circular dependency detected');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('has', () => {
|
|
||||||
test('should return true for registered dependency', () => {
|
|
||||||
dm.register('Logger', Logger, []);
|
|
||||||
|
|
||||||
expect(dm.has('Logger')).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should return false for unregistered dependency', () => {
|
|
||||||
expect(dm.has('NonExistent')).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should not throw error for unregistered dependency', () => {
|
|
||||||
expect(() => {
|
|
||||||
dm.has('NonExistent');
|
|
||||||
}).not.toThrow();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('clearSingletons', () => {
|
|
||||||
test('should clear cached singleton instances', () => {
|
|
||||||
dm.register('Logger', Logger, [], 'singleton');
|
|
||||||
|
|
||||||
const logger1 = dm.resolve<Logger>('Logger');
|
|
||||||
dm.clearSingletons();
|
|
||||||
const logger2 = dm.resolve<Logger>('Logger');
|
|
||||||
|
|
||||||
expect(logger1).not.toBe(logger2); // Different instances after clearing
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should not affect registrations', () => {
|
|
||||||
dm.register('Logger', Logger, []);
|
|
||||||
dm.clearSingletons();
|
|
||||||
|
|
||||||
expect(dm.has('Logger')).toBe(true);
|
|
||||||
expect(() => {
|
|
||||||
dm.resolve('Logger');
|
|
||||||
}).not.toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should work with multiple singletons', () => {
|
|
||||||
dm.register('Logger', Logger, [], 'singleton');
|
|
||||||
dm.register('Database', Database, ['Logger'], 'singleton');
|
|
||||||
|
|
||||||
const logger1 = dm.resolve<Logger>('Logger');
|
|
||||||
const db1 = dm.resolve<Database>('Database');
|
|
||||||
|
|
||||||
dm.clearSingletons();
|
|
||||||
|
|
||||||
const logger2 = dm.resolve<Logger>('Logger');
|
|
||||||
const db2 = dm.resolve<Database>('Database');
|
|
||||||
|
|
||||||
expect(logger1).not.toBe(logger2);
|
|
||||||
expect(db1).not.toBe(db2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('complex scenarios', () => {
|
|
||||||
test('should handle mixed lifecycles', () => {
|
|
||||||
dm.register('Logger', Logger, [], 'instance'); // Transient
|
|
||||||
dm.register('Database', Database, ['Logger'], 'singleton');
|
|
||||||
dm.register('UserService', UserService, ['Database', 'Logger']);
|
|
||||||
|
|
||||||
const userService1 = dm.resolve<UserService>('UserService');
|
|
||||||
const userService2 = dm.resolve<UserService>('UserService');
|
|
||||||
|
|
||||||
// UserService is singleton by default
|
|
||||||
expect(userService1).toBe(userService2);
|
|
||||||
|
|
||||||
// Database should be same singleton
|
|
||||||
expect(userService1.getDb()).toBe(userService2.getDb());
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle multiple dependencies of same type', () => {
|
|
||||||
class MultiDependent {
|
|
||||||
constructor(
|
|
||||||
public logger1: Logger,
|
|
||||||
public logger2: Logger
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
dm.register('Logger', Logger, [], 'instance');
|
|
||||||
dm.register('MultiDependent', MultiDependent, ['Logger', 'Logger']);
|
|
||||||
|
|
||||||
const multiDep = dm.resolve<MultiDependent>('MultiDependent');
|
|
||||||
|
|
||||||
// Should be different instances since Logger is transient
|
|
||||||
expect(multiDep.logger1).not.toBe(multiDep.logger2);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle no dependencies (empty array)', () => {
|
|
||||||
dm.register('Logger', Logger, []);
|
|
||||||
|
|
||||||
const logger = dm.resolve<Logger>('Logger');
|
|
||||||
|
|
||||||
expect(logger).toBeInstanceOf(Logger);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('type safety', () => {
|
|
||||||
test('should support generic type parameter', () => {
|
|
||||||
dm.register('Logger', Logger, []);
|
|
||||||
|
|
||||||
// TypeScript should infer the correct type
|
|
||||||
const logger = dm.resolve<Logger>('Logger');
|
|
||||||
|
|
||||||
// This should compile and work
|
|
||||||
expect(logger.log('test')).toBe('LOG: test');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should work without explicit type parameter', () => {
|
|
||||||
dm.register('Logger', Logger, []);
|
|
||||||
|
|
||||||
const logger = dm.resolve('Logger');
|
|
||||||
|
|
||||||
expect(logger).toBeInstanceOf(Logger);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
58
__test__/di/WhenManagingDependencies.test.ts
Normal file
58
__test__/di/WhenManagingDependencies.test.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
dependencyManager = new DependencyManager();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GIVEN classes with no dependencies are registered with the dependency manager', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
dependencyManager.register('Logger', Logger, [], 'transient');
|
||||||
|
dependencyManager.register('Database', Database, [], 'transient');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves the class', () => {
|
||||||
|
const logger = dependencyManager.resolve<Logger>('Logger');
|
||||||
|
|
||||||
|
expect(logger).toBeDefined();
|
||||||
|
expect(logger).toBeInstanceOf(Logger);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GIVEN the class is registered a second time', () => {
|
||||||
|
it('throws an error', () => {
|
||||||
|
expect(() => dependencyManager.register('Logger', Logger, [])).toThrow('Error: [Logger] is already registered');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GIVEN a singleton class is registered with the dependency manager', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
dependencyManager.register('Logger', Logger, [], 'singleton');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves the class', () => {
|
||||||
|
const logger = dependencyManager.resolve<Logger>('Logger');
|
||||||
|
|
||||||
|
expect(logger).toBeDefined();
|
||||||
|
expect(logger).toBeInstanceOf(Logger);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GIVEN a UserService with Logger dependency is registered', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
dependencyManager.register('UserService', UserService, ['Logger'], 'singleton');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves the same instance when requested multiple times', () => {
|
||||||
|
const userService = dependencyManager.resolve<UserService>('UserService');
|
||||||
|
const userService2 = dependencyManager.resolve<UserService>('UserService');
|
||||||
|
|
||||||
|
expect(userService).toBe(userService2);
|
||||||
|
expect(userService2).toBe(userService);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
15
__test__/mocks/MockDatabase.ts
Normal file
15
__test__/mocks/MockDatabase.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import type {Logger} from './MockLogger';
|
||||||
|
|
||||||
|
export class Database {
|
||||||
|
private _logger: Logger;
|
||||||
|
|
||||||
|
constructor(logger: Logger) {
|
||||||
|
this._logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public query(sql: string) {
|
||||||
|
this._logger.debug(`Executing: ${sql}`);
|
||||||
|
|
||||||
|
return 'result';
|
||||||
|
}
|
||||||
|
}
|
||||||
31
__test__/mocks/MockLogger.ts
Normal file
31
__test__/mocks/MockLogger.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
export class Logger {
|
||||||
|
private _category: string;
|
||||||
|
|
||||||
|
constructor(category: string) {
|
||||||
|
this._category = category;
|
||||||
|
}
|
||||||
|
|
||||||
|
public info(message: string, ...optionalParams: any[]) {
|
||||||
|
console.info(this.formatMessage(message), ...optionalParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
public debug(message: string, ...optionalParams: any[]) {
|
||||||
|
console.debug(this.formatMessage(message), ...optionalParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
public error(message: string, ...optionalParams: any[]) {
|
||||||
|
console.error(this.formatMessage(message), ...optionalParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
public warn(message: string, ...optionalParams: any[]) {
|
||||||
|
console.warn(this.formatMessage(message), ...optionalParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
public trace(message: string, ...optionalParams: any[]) {
|
||||||
|
console.trace(this.formatMessage(message), ...optionalParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatMessage(message: string) {
|
||||||
|
return `${new Date().toISOString()} [${this._category}] ${message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
14
__test__/mocks/MockUserServices.ts
Normal file
14
__test__/mocks/MockUserServices.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import type {Logger} from './MockLogger';
|
||||||
|
import type {Database} from './MockDatabase';
|
||||||
|
|
||||||
|
export class UserService {
|
||||||
|
constructor(
|
||||||
|
private db: Database,
|
||||||
|
private logger: Logger
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public getUser(id: number) {
|
||||||
|
this.logger.debug(`Getting user ${id}`);
|
||||||
|
return this.db.query(`SELECT * FROM users WHERE id = ${id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
3
__test__/mocks/index.ts
Normal file
3
__test__/mocks/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './MockDatabase';
|
||||||
|
export * from './MockLogger';
|
||||||
|
export * from './MockUserServices';
|
||||||
70
bun.lock
70
bun.lock
@@ -1,18 +1,24 @@
|
|||||||
{
|
{
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 1,
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"": {
|
"": {
|
||||||
"name": "myoopapp",
|
"name": "myoopapp",
|
||||||
|
"dependencies": {
|
||||||
|
"@techniker-me/tools": "2025.0.16",
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.38.0",
|
"@eslint/js": "9.38.0",
|
||||||
"@eslint/json": "^0.13.2",
|
"@eslint/json": "0.13.2",
|
||||||
"@eslint/markdown": "^7.5.0",
|
"@eslint/markdown": "7.5.0",
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"eslint": "^9.38.0",
|
"@types/express": "5.0.5",
|
||||||
"globals": "^16.4.0",
|
"@types/morgan": "1.9.10",
|
||||||
"jiti": "^2.6.1",
|
"eslint": "9.38.0",
|
||||||
|
"globals": "16.4.0",
|
||||||
|
"jiti": "2.6.1",
|
||||||
"prettier": "3.6.2",
|
"prettier": "3.6.2",
|
||||||
"typescript-eslint": "^8.46.2",
|
"typescript-eslint": "8.46.2",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "5.9.3",
|
"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-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=="],
|
"@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=="],
|
"@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/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/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/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/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/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=="],
|
"@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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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-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/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/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=="],
|
"@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/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=="],
|
"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=="],
|
"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=="],
|
"@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=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
28
package.json
28
package.json
@@ -1,22 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "myoopapp",
|
"name": "myoopapp",
|
||||||
"module": "index.ts",
|
|
||||||
"type": "module",
|
|
||||||
"author": "Alexander Zinn",
|
"author": "Alexander Zinn",
|
||||||
"private": true,
|
"module": "index.ts",
|
||||||
"scripts": {
|
|
||||||
"format": "prettier --write .",
|
|
||||||
"lint": "eslint --max-warnings 0 .",
|
|
||||||
"prelint:fix": "bun run format",
|
|
||||||
"lint:fix": "eslint --fix .",
|
|
||||||
"test": "bun test",
|
|
||||||
"test:watch": "bun test --watch"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "9.38.0",
|
"@eslint/js": "9.38.0",
|
||||||
"@eslint/json": "0.13.2",
|
"@eslint/json": "0.13.2",
|
||||||
"@eslint/markdown": "7.5.0",
|
"@eslint/markdown": "7.5.0",
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
|
"@types/express": "5.0.5",
|
||||||
|
"@types/morgan": "1.9.10",
|
||||||
"eslint": "9.38.0",
|
"eslint": "9.38.0",
|
||||||
"globals": "16.4.0",
|
"globals": "16.4.0",
|
||||||
"jiti": "2.6.1",
|
"jiti": "2.6.1",
|
||||||
@@ -25,5 +17,19 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "5.9.3"
|
"typescript": "5.9.3"
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"@techniker-me/tools": "2025.0.16"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11
src/Default.ts
Normal file
11
src/Default.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import {LoggingLevel} from './logger/LoggingLevel';
|
||||||
|
|
||||||
|
export default class Default {
|
||||||
|
static get loggingLevel(): LoggingLevel {
|
||||||
|
return LoggingLevel.Info;
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
throw new Error('Defaults is a static class that may not be instantiated');
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/api/ApiRouteTemplate.ts
Normal file
25
src/api/ApiRouteTemplate.ts
Normal 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
60
src/api/HealthCheckApi.ts
Normal 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
8
src/api/IApiRoute.ts
Normal 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>;
|
||||||
|
}
|
||||||
25
src/api/UserApiRoute.ts
Normal file
25
src/api/UserApiRoute.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
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> = {};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
104
src/di/ConfigurationReader.ts
Normal file
104
src/di/ConfigurationReader.ts
Normal 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';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ type Registration = {
|
|||||||
/**
|
/**
|
||||||
* Lifecycle types:
|
* Lifecycle types:
|
||||||
* - 'singleton': A single shared instance cached for the lifetime of the manager
|
* - 'singleton': 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)
|
* - 'transient': A new instance created on each resolve (transient)
|
||||||
*/
|
*/
|
||||||
type Lifecycle = 'transient' | 'singleton';
|
type Lifecycle = 'transient' | 'singleton';
|
||||||
|
|||||||
42
src/di/FileConfigurationLoader.ts
Normal file
42
src/di/FileConfigurationLoader.ts
Normal 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';
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/di/IConfigurationLoader.ts
Normal file
6
src/di/IConfigurationLoader.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import type { Definition } from "./ConfigurationReader";
|
||||||
|
|
||||||
|
export interface IConfigurationLoader {
|
||||||
|
load(name: string): Definition | Definition[];
|
||||||
|
toString(): string;
|
||||||
|
}
|
||||||
13
src/di/IDependencyManger.ts
Normal file
13
src/di/IDependencyManger.ts
Normal 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;
|
||||||
|
}
|
||||||
22
src/di/NamedType.ts
Normal file
22
src/di/NamedType.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import {Type} from './Type';
|
||||||
|
|
||||||
|
export class NamedType extends Type {
|
||||||
|
private readonly _name: string;
|
||||||
|
|
||||||
|
constructor(name: string, type: string) {
|
||||||
|
super(type);
|
||||||
|
this._name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getName(): string {
|
||||||
|
return this._name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public equals(other: NamedType): boolean {
|
||||||
|
return super.equal(other) && other instanceof NamedType && this.getName() === other.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override toURN(): string {
|
||||||
|
return 'urn:namedtype:' + super.getType() + '#' + this._name;
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/di/Type.ts
Normal file
19
src/di/Type.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
export class Type {
|
||||||
|
private readonly _type: string;
|
||||||
|
|
||||||
|
constructor(type: string) {
|
||||||
|
this._type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getType(): string {
|
||||||
|
return this._type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public equal(other: Type): boolean {
|
||||||
|
return other instanceof Type && this.getType() === other.getType();
|
||||||
|
}
|
||||||
|
|
||||||
|
public toURN() {
|
||||||
|
return 'urn:type:' + this._type;
|
||||||
|
}
|
||||||
|
}
|
||||||
53
src/di/providers/DependencyProvider.ts
Normal file
53
src/di/providers/DependencyProvider.ts
Normal 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()}]`;
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/di/providers/IDependencyProvider.ts
Normal file
7
src/di/providers/IDependencyProvider.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import type {Type} from '../Type';
|
||||||
|
import type {NamedType} from '../NamedType';
|
||||||
|
|
||||||
|
export interface IDependencyProvider {
|
||||||
|
canProvide(type: Type | NamedType): boolean;
|
||||||
|
provide(type: Type | NamedType): unknown;
|
||||||
|
}
|
||||||
13
src/health/HealthCheck.ts
Normal file
13
src/health/HealthCheck.ts
Normal 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()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/lang/assertUnreachable.ts
Normal file
3
src/lang/assertUnreachable.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default function assertUnreachable(x: never): never {
|
||||||
|
throw new Error(`Unreachable code [${x}]`);
|
||||||
|
}
|
||||||
23
src/lang/disposables/Disposable.ts
Normal file
23
src/lang/disposables/Disposable.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import type IDisposable from './IDisposable';
|
||||||
|
|
||||||
|
export default class Disposable implements IDisposable {
|
||||||
|
private _isDisposed = false;
|
||||||
|
private _disposable: () => void;
|
||||||
|
|
||||||
|
constructor(disposable: () => void) {
|
||||||
|
this._disposable = disposable;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isDisposed(): boolean {
|
||||||
|
return this._isDisposed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public dispose(): void {
|
||||||
|
if (this._isDisposed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._disposable();
|
||||||
|
this._isDisposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/lang/disposables/DisposableList.ts
Normal file
17
src/lang/disposables/DisposableList.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import type IDisposable from './IDisposable';
|
||||||
|
|
||||||
|
export default class DisposableList implements IDisposable {
|
||||||
|
private readonly _disposables: IDisposable[] = [];
|
||||||
|
|
||||||
|
public add(disposable: IDisposable): void {
|
||||||
|
this._disposables.push(disposable);
|
||||||
|
}
|
||||||
|
|
||||||
|
public dispose(): void {
|
||||||
|
while (this._disposables.length) {
|
||||||
|
const disposable = this._disposables.shift() as IDisposable;
|
||||||
|
|
||||||
|
disposable.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/lang/disposables/IDisposable.ts
Normal file
3
src/lang/disposables/IDisposable.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default interface IDisposable {
|
||||||
|
dispose(): void;
|
||||||
|
}
|
||||||
6
src/lang/disposables/index.ts
Normal file
6
src/lang/disposables/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import type IDisposable from './IDisposable';
|
||||||
|
import Disposable from './Disposable';
|
||||||
|
import DisposableList from './DisposableList';
|
||||||
|
|
||||||
|
export type {IDisposable};
|
||||||
|
export {Disposable, DisposableList};
|
||||||
18
src/lang/observables/ReadOnlySubject.ts
Normal file
18
src/lang/observables/ReadOnlySubject.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import Disposable from '../disposables/Disposable';
|
||||||
|
import Subject from './Subject';
|
||||||
|
|
||||||
|
export default class ReadOnlySubject<T> {
|
||||||
|
private readonly _subject: Subject<T>;
|
||||||
|
|
||||||
|
constructor(subject: Subject<T>) {
|
||||||
|
this._subject = subject;
|
||||||
|
}
|
||||||
|
|
||||||
|
get value(): T {
|
||||||
|
return this._subject.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public subscribe(listener: (value: T) => void): Disposable {
|
||||||
|
return this._subject.subscribe(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
39
src/lang/observables/Subject.ts
Normal file
39
src/lang/observables/Subject.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import Disposable from '../disposables/Disposable';
|
||||||
|
|
||||||
|
export default class Subject<T> {
|
||||||
|
private readonly _listeners: Array<(value: T) => void> = [];
|
||||||
|
private _value: T;
|
||||||
|
|
||||||
|
constructor(value: T) {
|
||||||
|
this._value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
set value(value: T) {
|
||||||
|
if (this._value === value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._value = value;
|
||||||
|
this.notifyListeners(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
get value(): T {
|
||||||
|
return this._value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public subscribe(listener: (value: T) => void): Disposable {
|
||||||
|
const listenerIndex = this._listeners.push(listener);
|
||||||
|
|
||||||
|
listener(this.value);
|
||||||
|
|
||||||
|
return new Disposable(() => {
|
||||||
|
this._listeners.splice(listenerIndex, 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private notifyListeners(value: T): void {
|
||||||
|
for (const listener of this._listeners) {
|
||||||
|
listener(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
4
src/lang/observables/index.ts
Normal file
4
src/lang/observables/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import Subject from './Subject';
|
||||||
|
import ReadOnlySubject from './ReadOnlySubject';
|
||||||
|
|
||||||
|
export {Subject, ReadOnlySubject};
|
||||||
7
src/logger/ILogger.ts
Normal file
7
src/logger/ILogger.ts
Normal file
@@ -0,0 +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;
|
||||||
|
}
|
||||||
102
src/logger/Logger.ts
Normal file
102
src/logger/Logger.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
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 {
|
||||||
|
private readonly _category: string;
|
||||||
|
private readonly _threshold: Threshold;
|
||||||
|
private readonly _appenders: Set<IAppender>;
|
||||||
|
constructor(category: string, threshold: Threshold, appenders: Set<IAppender>) {
|
||||||
|
this._category = category;
|
||||||
|
this._threshold = threshold;
|
||||||
|
this._appenders = appenders;
|
||||||
|
}
|
||||||
|
|
||||||
|
public info(message: string, ...optionalParameters: unknown[]): void {
|
||||||
|
if (this._threshold.value > LoggingLevel.Info) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log(LoggingLevel.Info, message, ...optionalParameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
public warn(message: string, ...optionalParameters: unknown[]): void {
|
||||||
|
if (this._threshold.value > LoggingLevel.Warn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log(LoggingLevel.Warn, message, ...optionalParameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
public error(message: string, ...optionalParameters: unknown[]): void {
|
||||||
|
if (this._threshold.value > LoggingLevel.Error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log(LoggingLevel.Error, message, ...optionalParameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
public debug(message: string, ...optionalParameters: unknown[]): void {
|
||||||
|
if (this._threshold.value > LoggingLevel.Debug) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log(LoggingLevel.Debug, message, ...optionalParameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
public trace(message: string, ...optionalParameters: unknown[]): void {
|
||||||
|
if (this._threshold.value > LoggingLevel.Trace) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log(LoggingLevel.Trace, message, ...optionalParameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
public silly(message: string, ...optionalParameters: unknown[]): void {
|
||||||
|
if (this._threshold.value > LoggingLevel.Silly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log(LoggingLevel.Silly, message, ...optionalParameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatMessage(message: string, ...optionalParameters: unknown[]): string {
|
||||||
|
let optionalParameterIndex = 0;
|
||||||
|
|
||||||
|
return message.replace(/%[sdj]/g, match => {
|
||||||
|
if (optionalParameterIndex >= optionalParameters.length) {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
|
||||||
|
const param = optionalParameters[optionalParameterIndex++];
|
||||||
|
|
||||||
|
switch (match) {
|
||||||
|
case '%s':
|
||||||
|
return String(param);
|
||||||
|
case '%d':
|
||||||
|
return typeof param === 'number' ? param.toString() : 'NaN';
|
||||||
|
case '%j':
|
||||||
|
try {
|
||||||
|
return JSON.stringify(param);
|
||||||
|
} catch {
|
||||||
|
return '[Circular]';
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private log(loggingLevel: LoggingLevel, message: string, ...optionalParameters: unknown[]): void {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
const formattedMessage = this.formatMessage(message, ...optionalParameters);
|
||||||
|
const level = LoggingLevelMapping.convertLoggingLevelToLoggingLevelType(loggingLevel);
|
||||||
|
|
||||||
|
for (const appender of this._appenders) {
|
||||||
|
appender.log(timestamp, level, this._category, formattedMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/logger/LoggerFactory.ts
Normal file
43
src/logger/LoggerFactory.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
type Category = string;
|
||||||
|
|
||||||
|
export default class LoggerFactory {
|
||||||
|
private static readonly _appenders: Set<IAppender> = new Set();
|
||||||
|
private static readonly _threshold: Threshold = new Threshold();
|
||||||
|
private static readonly _loggers: Map<Category, Logger> = new Map();
|
||||||
|
|
||||||
|
static {
|
||||||
|
this.applyConsoleAppender();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getLogger(category: string): Logger {
|
||||||
|
if (!LoggerFactory._loggers.has(category)) {
|
||||||
|
this._loggers.set(category, new Logger(category, LoggerFactory._threshold, LoggerFactory._appenders));
|
||||||
|
}
|
||||||
|
|
||||||
|
return LoggerFactory._loggers.get(category) as Logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static applyAppender(appender: IAppender): Disposable {
|
||||||
|
LoggerFactory._appenders.add(appender);
|
||||||
|
|
||||||
|
return new Disposable(() => LoggerFactory._appenders.delete(appender));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static setLoggingLevel(loggingLevelType: LoggingLevelType): void {
|
||||||
|
const loggingLevel = LoggingLevelMapping.convertLoggingLevelTypeToLoggingLevel(loggingLevelType);
|
||||||
|
|
||||||
|
LoggerFactory._threshold.value = loggingLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static applyConsoleAppender(): void {
|
||||||
|
LoggerFactory.applyAppender(new ConsoleAppender());
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/logger/LoggingLevel.ts
Normal file
12
src/logger/LoggingLevel.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export enum LoggingLevel {
|
||||||
|
Off = -1,
|
||||||
|
Debug = 10,
|
||||||
|
Trace = 20,
|
||||||
|
Silly = 30,
|
||||||
|
Info = 40,
|
||||||
|
Warn = 50,
|
||||||
|
Error = 60,
|
||||||
|
All = 70
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LoggingLevelType = 'Off' | 'Debug' | 'Trace' | 'Info' | 'Silly' | 'Warn' | 'Error' | 'All';
|
||||||
51
src/logger/LoggingLevelMapping.ts
Normal file
51
src/logger/LoggingLevelMapping.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import {assertUnreachable} from '@techniker-me/tools';
|
||||||
|
import {LoggingLevel, type LoggingLevelType} from './LoggingLevel';
|
||||||
|
|
||||||
|
export default class LoggingLevelMapping {
|
||||||
|
public static convertLoggingLevelToLoggingLevelType(loggingLevel: LoggingLevel): LoggingLevelType {
|
||||||
|
switch (loggingLevel) {
|
||||||
|
case LoggingLevel.Off:
|
||||||
|
return 'Off';
|
||||||
|
case LoggingLevel.Info:
|
||||||
|
return 'Info';
|
||||||
|
case LoggingLevel.Warn:
|
||||||
|
return 'Warn';
|
||||||
|
case LoggingLevel.Error:
|
||||||
|
return 'Error';
|
||||||
|
case LoggingLevel.Debug:
|
||||||
|
return 'Debug';
|
||||||
|
case LoggingLevel.Trace:
|
||||||
|
return 'Trace';
|
||||||
|
case LoggingLevel.Silly:
|
||||||
|
return 'Silly';
|
||||||
|
case LoggingLevel.All:
|
||||||
|
return 'All';
|
||||||
|
|
||||||
|
default:
|
||||||
|
assertUnreachable(loggingLevel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static convertLoggingLevelTypeToLoggingLevel(loggingLevelType: LoggingLevelType): LoggingLevel {
|
||||||
|
switch (loggingLevelType) {
|
||||||
|
case 'Off':
|
||||||
|
return LoggingLevel.Off;
|
||||||
|
case 'Info':
|
||||||
|
return LoggingLevel.Info;
|
||||||
|
case 'Warn':
|
||||||
|
return LoggingLevel.Warn;
|
||||||
|
case 'Error':
|
||||||
|
return LoggingLevel.Error;
|
||||||
|
case 'Debug':
|
||||||
|
return LoggingLevel.Debug;
|
||||||
|
case 'Trace':
|
||||||
|
return LoggingLevel.Trace;
|
||||||
|
case 'Silly':
|
||||||
|
return LoggingLevel.Silly;
|
||||||
|
case 'All':
|
||||||
|
return LoggingLevel.All;
|
||||||
|
default:
|
||||||
|
assertUnreachable(loggingLevelType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/logger/Threshold.ts
Normal file
19
src/logger/Threshold.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import Defaults from '../Default';
|
||||||
|
import {Subject} from '../lang/observables';
|
||||||
|
import {LoggingLevel} from './LoggingLevel';
|
||||||
|
|
||||||
|
export class Threshold {
|
||||||
|
private _threshold: Subject<LoggingLevel>;
|
||||||
|
|
||||||
|
constructor(loggingLevel?: LoggingLevel) {
|
||||||
|
this._threshold = new Subject(loggingLevel ?? Defaults.loggingLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
set value(value: LoggingLevel) {
|
||||||
|
this._threshold.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
get value(): LoggingLevel {
|
||||||
|
return this._threshold.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
63
src/logger/appenders/Appender.ts
Normal file
63
src/logger/appenders/Appender.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import {type LoggingLevelType} from '../LoggingLevel';
|
||||||
|
import type ILogMessage from './LogMessage';
|
||||||
|
|
||||||
|
export type AppenderOptions = {
|
||||||
|
domain?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class Appender {
|
||||||
|
private readonly _logRecorderUrl: string = 'https://logserver.techniker.me/api/logs';
|
||||||
|
// @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;
|
||||||
|
|
||||||
|
constructor(logRecorderUrl: string, {domain}: AppenderOptions) {
|
||||||
|
this._logRecorderUrl = logRecorderUrl;
|
||||||
|
this._domain = domain ?? this._domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
public log(timestamp: string, level: LoggingLevelType, category: string, message: string): void {
|
||||||
|
const logMessage = {
|
||||||
|
timestamp,
|
||||||
|
domain: this._domain,
|
||||||
|
level,
|
||||||
|
category,
|
||||||
|
message
|
||||||
|
};
|
||||||
|
this.queueMessage(logMessage);
|
||||||
|
this.postLogMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async postLogMessage(): Promise<void> {
|
||||||
|
const logMessage = this._logMessageQueue.shift();
|
||||||
|
|
||||||
|
if (!logMessage || this._pendingPostLogMessagePromise !== undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (typeof fetch === 'undefined') {
|
||||||
|
console.error('Fetch API is not available in this environment');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._pendingPostLogMessagePromise = fetch(this._logRecorderUrl, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json'
|
||||||
|
},
|
||||||
|
mode: 'no-cors',
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(logMessage)
|
||||||
|
}).then(() => (this._pendingPostLogMessagePromise = undefined));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Unable to send logs due to [%o]', e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private queueMessage(logMessage: ILogMessage): void {
|
||||||
|
this._logMessageQueue.push(logMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/logger/appenders/AppenderFactory.ts
Normal file
12
src/logger/appenders/AppenderFactory.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import Appender from './Appender';
|
||||||
|
import type {AppenderOptions} from './Appender';
|
||||||
|
|
||||||
|
export default class AppnederFactory {
|
||||||
|
public static createRemoteAppender(remoteAppenderUrl: string, {domain}: AppenderOptions): Appender {
|
||||||
|
return new Appender(remoteAppenderUrl, {domain: domain ?? ''});
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
throw new Error('AppenderFactory is a static class that may not be instantiated');
|
||||||
|
}
|
||||||
|
}
|
||||||
31
src/logger/appenders/ConsoleAppender.ts
Normal file
31
src/logger/appenders/ConsoleAppender.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import {LoggingLevel, type LoggingLevelType} from '../LoggingLevel';
|
||||||
|
import LoggingLevelMapping from '../LoggingLevelMapping';
|
||||||
|
import type {IAppender} from './IAppender';
|
||||||
|
import {assertUnreachable} from '@techniker-me/tools';
|
||||||
|
|
||||||
|
export default class ConsoleAppender implements IAppender {
|
||||||
|
public log(timestamp: string, level: LoggingLevelType, category: string, message: string) {
|
||||||
|
const loggingLevel = LoggingLevelMapping.convertLoggingLevelTypeToLoggingLevel(level);
|
||||||
|
|
||||||
|
switch (loggingLevel) {
|
||||||
|
case LoggingLevel.Off:
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LoggingLevel.Warn:
|
||||||
|
case LoggingLevel.Error:
|
||||||
|
console.error(`${timestamp} [${level}] [${category}] ${message}`);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LoggingLevel.Info:
|
||||||
|
case LoggingLevel.Debug:
|
||||||
|
case LoggingLevel.Trace:
|
||||||
|
case LoggingLevel.Silly:
|
||||||
|
case LoggingLevel.All:
|
||||||
|
console.log(`${timestamp} [${level}] [${category}] ${message}`);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
assertUnreachable(loggingLevel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/logger/appenders/IAppender.ts
Normal file
5
src/logger/appenders/IAppender.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import type {LoggingLevelType} from '../LoggingLevel';
|
||||||
|
|
||||||
|
export interface IAppender {
|
||||||
|
log(timestamp: string, level: LoggingLevelType, category: string, message: string): void;
|
||||||
|
}
|
||||||
6
src/logger/appenders/LogMessage.ts
Normal file
6
src/logger/appenders/LogMessage.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default interface ILogMessage {
|
||||||
|
timestamp: string;
|
||||||
|
level: string;
|
||||||
|
category: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
55
src/logger/appenders/TechnikerMeAppender.ts
Normal file
55
src/logger/appenders/TechnikerMeAppender.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
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-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;
|
||||||
|
|
||||||
|
public log(timestamp: string, level: LoggingLevelType, category: string, message: string): void {
|
||||||
|
const logMessage = {
|
||||||
|
timestamp,
|
||||||
|
domain: this._domain,
|
||||||
|
level,
|
||||||
|
category,
|
||||||
|
message
|
||||||
|
};
|
||||||
|
this.queueMessage(logMessage);
|
||||||
|
this.postLogMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async postLogMessage(): Promise<void> {
|
||||||
|
const logMessage = this._logMessageQueue.shift();
|
||||||
|
|
||||||
|
if (!logMessage || this._pendingPostLogMessagePromise !== undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (typeof fetch === 'undefined') {
|
||||||
|
console.error('Fetch API is not available in this environment');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._pendingPostLogMessagePromise = fetch(this._logRecorderUrl, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json'
|
||||||
|
},
|
||||||
|
mode: 'no-cors',
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(logMessage)
|
||||||
|
}).then(() => (this._pendingPostLogMessagePromise = undefined));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Unable to send logs due to [%o]', e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private queueMessage(logMessage: ILogMessage): void {
|
||||||
|
this._logMessageQueue.push(logMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
95
src/net/http/HttpRouteManger.ts
Normal file
95
src/net/http/HttpRouteManger.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import assertUnreachable from '../../lang/assertUnreachable';
|
||||||
|
import type express from 'express';
|
||||||
|
|
||||||
|
export type HttpRoute = string;
|
||||||
|
export type RoutePath = string;
|
||||||
|
export type RouteHandler = (request: express.Request, response: express.Response) => void | Promise<void>;
|
||||||
|
|
||||||
|
export class HttpRouteManager {
|
||||||
|
private readonly _staticGETRoutes: Record<HttpRoute, RoutePath>;
|
||||||
|
private readonly _getRoutes: Record<HttpRoute, RouteHandler>;
|
||||||
|
private readonly _postRoutes: Record<HttpRoute, RouteHandler>;
|
||||||
|
private readonly _putRoutes: Record<HttpRoute, RouteHandler>;
|
||||||
|
private readonly _deleteRoutes: Record<HttpRoute, RouteHandler>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this._staticGETRoutes = {};
|
||||||
|
this._getRoutes = {};
|
||||||
|
this._postRoutes = {};
|
||||||
|
this._putRoutes = {};
|
||||||
|
this._deleteRoutes = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
public registerStaticRoute(routePath: RoutePath, destination: string) {
|
||||||
|
if (this._staticGETRoutes[routePath]) {
|
||||||
|
throw new Error(`Error: Static route [${routePath}] is already defined`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._staticGETRoutes[routePath] = destination;
|
||||||
|
}
|
||||||
|
|
||||||
|
public register(verb: 'GET' | 'POST' | 'PUT' | 'DELETE', route: string, handler: RouteHandler) {
|
||||||
|
const isRouteDefined = (routes: Record<HttpRoute, RouteHandler>, routePath: string) => !!routes[routePath];
|
||||||
|
|
||||||
|
switch (verb) {
|
||||||
|
case 'GET':
|
||||||
|
if (isRouteDefined(this._getRoutes, route)) {
|
||||||
|
throw new Error(`Error: Route [${route}] is already defined`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._getRoutes[route] = handler;
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'POST':
|
||||||
|
if (isRouteDefined(this._postRoutes, route)) {
|
||||||
|
throw new Error(`Error: Route [${route}] is already defined`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._postRoutes[route] = handler;
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'PUT':
|
||||||
|
if (isRouteDefined(this._putRoutes, route)) {
|
||||||
|
throw new Error(`Error: Route [${route}] is already defined`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._putRoutes[route] = handler;
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'DELETE':
|
||||||
|
if (isRouteDefined(this._deleteRoutes, route)) {
|
||||||
|
throw new Error(`Error: Route [${route}] is already defined`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._deleteRoutes[route] = handler;
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
assertUnreachable(verb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getStaticGETRoutes(): Record<HttpRoute, RoutePath> {
|
||||||
|
return this._staticGETRoutes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getGETRoutes(): Record<HttpRoute, RouteHandler> {
|
||||||
|
return this._getRoutes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getPOSTRoutes(): Record<HttpRoute, RouteHandler> {
|
||||||
|
return this._postRoutes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getPUTRoutes(): Record<HttpRoute, RouteHandler> {
|
||||||
|
return this._putRoutes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getDELETERoutes(): Record<HttpRoute, RouteHandler> {
|
||||||
|
return this._deleteRoutes;
|
||||||
|
}
|
||||||
|
}
|
||||||
56
src/net/http/HttpServer.ts
Normal file
56
src/net/http/HttpServer.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import express, {type Express} from 'express';
|
||||||
|
import type {HttpRouteManager} from './HttpRouteManger';
|
||||||
|
import path from 'path';
|
||||||
|
import morgan from 'morgan';
|
||||||
|
|
||||||
|
type Routes = Record<string, RouteHandler>;
|
||||||
|
type RouteHandler = (req: express.Request, res: express.Response) => void | Promise<void>;
|
||||||
|
|
||||||
|
export class HttpServer {
|
||||||
|
private _app: Express;
|
||||||
|
|
||||||
|
constructor(private readonly routeManager: HttpRouteManager) {
|
||||||
|
this._app = express();
|
||||||
|
this._app.use(morgan('dev'));
|
||||||
|
|
||||||
|
this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
public start(port: number) {
|
||||||
|
this._app.listen(port, () => console.log('Server listening on [%o]', port));
|
||||||
|
}
|
||||||
|
|
||||||
|
private initialize() {
|
||||||
|
this.applyMiddleware();
|
||||||
|
this.applyStaticRoutes();
|
||||||
|
this.applyRoutes('get', this.routeManager.getGETRoutes());
|
||||||
|
this.applyRoutes('post', this.routeManager.getPOSTRoutes());
|
||||||
|
this.applyRoutes('put', this.routeManager.getPUTRoutes());
|
||||||
|
this.applyRoutes('delete', this.routeManager.getDELETERoutes());
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyRoutes(httpVerb: 'get' | 'post' | 'put' | 'delete', routes: Routes) {
|
||||||
|
Object.entries(routes).forEach(([route, handler]) => this._app[httpVerb as keyof typeof this._app](route, handler));
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyStaticRoutes() {
|
||||||
|
Object.entries(this.routeManager.getStaticGETRoutes()).forEach(([route, destination]) => {
|
||||||
|
this._app.use(
|
||||||
|
route,
|
||||||
|
express.static(path.resolve(destination), {
|
||||||
|
setHeaders: (res, filePath) => {
|
||||||
|
console.log(filePath);
|
||||||
|
if (filePath.endsWith('.js')) {
|
||||||
|
res.setHeader('Content-Type', 'application/javascript');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyMiddleware() {
|
||||||
|
this._app.use(express.json());
|
||||||
|
this._app.use(express.urlencoded({extended: true}));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user