Establish project foundation and shared utilities
This commit is contained in:
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "0.1",
|
"version": "0.1",
|
||||||
"import": ["@generalprotocols/cspell-dictionary/cspell.json"],
|
"import": ["@generalprotocols/cspell-dictionary/cspell.json"],
|
||||||
"words": []
|
"words": ["unixepoch", "kysely", "prefault", "upserting", "subsec"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,3 +15,6 @@ coverage/
|
|||||||
*.d.ts
|
*.d.ts
|
||||||
*.d.ts.map
|
*.d.ts.map
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
.env
|
||||||
|
data.db*
|
||||||
|
|||||||
Generated
+1869
-884
File diff suppressed because it is too large
Load Diff
+26
-16
@@ -17,12 +17,14 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "tsdown --clean --no-fixed-extension --sourcemap source/index.ts && esbuild-analyzer dist/",
|
"analyze": "tsdown --clean --no-fixed-extension --sourcemap source/index.ts && esbuild-analyzer dist/",
|
||||||
"build": "tsdown --clean --sourcemap source/index.ts",
|
"build": "tsdown --clean --sourcemap source/index.ts",
|
||||||
|
"dev": "tsx watch source/index.ts",
|
||||||
"docs": "typedoc --hideGenerator --categorizeByGroup",
|
"docs": "typedoc --hideGenerator --categorizeByGroup",
|
||||||
"format": "prettier --write . && eslint --fix",
|
"format": "prettier --write . && eslint --fix",
|
||||||
"spellcheck": "cspell 'source/**' 'test/**' 'playground/**'",
|
"spellcheck": "cspell 'source/**' 'test/**' 'playground/**'",
|
||||||
"style": "eslint",
|
"style": "eslint",
|
||||||
"syntax": "tsc --noEmit",
|
"syntax": "tsc --noEmit",
|
||||||
"test": "vitest --dir test/ --test-timeout=15000 --passWithNoTests --run --coverage"
|
"test": "vitest --dir test/ --test-timeout=15000 --passWithNoTests --run --coverage",
|
||||||
|
"test:watch": "vitest"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
@@ -42,29 +44,37 @@
|
|||||||
"xo cash"
|
"xo cash"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bitauth/libauth": "^3.1.0-next.8"
|
"@bitauth/libauth": "^3.1.0-next.8",
|
||||||
|
"@hono/node-server": "^2.0.5",
|
||||||
|
"@xo-cash/utils": "^0.0.2-development.15512051893",
|
||||||
|
"better-sqlite3": "^12.11.1",
|
||||||
|
"debug": "^4.4.3",
|
||||||
|
"dotenv": "^17.4.2",
|
||||||
|
"hono": "^4.12.26",
|
||||||
|
"kysely": "^0.29.2",
|
||||||
|
"ws": "^8.21.0",
|
||||||
|
"zod": "^4.4.3"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"echarts": "6.1.0"
|
"echarts": "6.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@chalp/eslint-airbnb": "^1.3.0",
|
|
||||||
"@generalprotocols/cspell-dictionary": "^1.0.1",
|
"@generalprotocols/cspell-dictionary": "^1.0.1",
|
||||||
"@stylistic/eslint-plugin": "^5.7.0",
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
"@types/node": "^25.5.0",
|
"@types/debug": "^4.1.13",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.53.1",
|
"@types/node": "^25.9.3",
|
||||||
"@typescript-eslint/parser": "^8.53.1",
|
"@types/ws": "^8.18.1",
|
||||||
"@vitest/coverage-v8": "^4.0.17",
|
"@vitest/coverage-v8": "^4.1.9",
|
||||||
"@viz-kit/esbuild-analyzer": "^1.0.0",
|
"@viz-kit/esbuild-analyzer": "^1.0.0",
|
||||||
"@xo-cash/eslint-config": "1.0.2",
|
"@xo-cash/eslint-config": "1.0.2",
|
||||||
"cspell": "^9.6.0",
|
"cspell": "^10.0.1",
|
||||||
"eslint": "^9.39.2",
|
"prettier": "^3.8.4",
|
||||||
"prettier": "^3.6.2",
|
"tsdown": "^0.22.14",
|
||||||
"tsdown": "^0.20.0-beta.4",
|
"tsx": "^4.22.4",
|
||||||
"typedoc": "^0.28.16",
|
"typedoc": "^0.28.20",
|
||||||
"typedoc-plugin-coverage": "^4.0.2",
|
"typedoc-plugin-coverage": "^4.0.2",
|
||||||
"typescript": "^5.3.2",
|
"typescript": "^6.0.3",
|
||||||
"typescript-eslint": "^8.53.1",
|
"typescript-eslint": "^8.65.0",
|
||||||
"vitest": "^4.0.17"
|
"vitest": "^4.1.9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
/** An expected application failure whose message is safe to send to clients. */
|
||||||
|
export class ApplicationError extends Error {
|
||||||
|
/**
|
||||||
|
* @param statusCode - HTTP-equivalent status returned to the client.
|
||||||
|
* @param message - Client-safe error summary.
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
readonly statusCode: number,
|
||||||
|
message: string,
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'ApplicationError';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './application-error.ts';
|
||||||
|
export * from './unauthorized-error.ts';
|
||||||
|
export * from './utils.ts';
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
/** Authentication failure whose message is safe to return to clients. */
|
||||||
|
export class UnauthorizedError extends Error {
|
||||||
|
/**
|
||||||
|
* @param message - Client-safe unauthorized summary.
|
||||||
|
*/
|
||||||
|
constructor(message = 'Unauthorized') {
|
||||||
|
super(message);
|
||||||
|
this.name = 'UnauthorizedError';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { UnauthorizedError } from './unauthorized-error.ts';
|
||||||
|
import { ApplicationError } from './application-error.ts';
|
||||||
|
|
||||||
|
/** Stable error payload shared by HTTP, SSE, and WebSocket. */
|
||||||
|
export type PublicError = {
|
||||||
|
|
||||||
|
/** Protocol-independent status carried by every transport. */
|
||||||
|
statusCode: number;
|
||||||
|
|
||||||
|
/** Client-safe summary which never exposes an unexpected exception. */
|
||||||
|
error: string;
|
||||||
|
|
||||||
|
/** Structured field failures supplied only for validation errors. */
|
||||||
|
details?: Array<{ path: string; message: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert application failures into the common public transport contract.
|
||||||
|
*
|
||||||
|
* @param error - Any thrown value from a route or transport boundary.
|
||||||
|
* @returns A sanitized error payload safe to encode on the wire.
|
||||||
|
*/
|
||||||
|
export const normalizePublicError = (error: unknown): PublicError => {
|
||||||
|
if (error instanceof z.ZodError) {
|
||||||
|
return {
|
||||||
|
statusCode: 400,
|
||||||
|
error: 'Validation Error',
|
||||||
|
details: error.issues.map((issue) => ({
|
||||||
|
path: issue.path.join('.'),
|
||||||
|
message: issue.message,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof UnauthorizedError) {
|
||||||
|
return { statusCode: 401, error: error.message };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof ApplicationError) {
|
||||||
|
return { statusCode: error.statusCode, error: error.message };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown exceptions are logged by adapters, but their messages stay private.
|
||||||
|
return { statusCode: 500, error: 'Internal Server Error' };
|
||||||
|
};
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The configuration schema for the server.
|
||||||
|
*/
|
||||||
|
const configSchema = z.object({
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The database configuration.
|
||||||
|
*/
|
||||||
|
database: z.object({
|
||||||
|
path: z.string().default('data.db'),
|
||||||
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The server configuration.
|
||||||
|
*/
|
||||||
|
server: z.object({
|
||||||
|
port: z.coerce.number().int()
|
||||||
|
.positive()
|
||||||
|
.default(3000),
|
||||||
|
host: z.string().default('0.0.0.0'),
|
||||||
|
|
||||||
|
/** Maximum encoded HTTP body or WebSocket message size in bytes. */
|
||||||
|
maxRequestBodyBytes: z.coerce
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.positive()
|
||||||
|
.default(1024 * 1024),
|
||||||
|
cors: z
|
||||||
|
.object({
|
||||||
|
origin: z.string().default('*'),
|
||||||
|
methods: z.array(z.string()).default([ 'GET', 'POST', 'PUT', 'DELETE', 'OPTIONS' ]),
|
||||||
|
allowedHeaders: z.array(z.string()).default([ 'Content-Type', 'cache-control', 'X-Timestamp', 'X-PublicKey', 'X-Signature' ]),
|
||||||
|
})
|
||||||
|
.partial()
|
||||||
|
.prefault({}),
|
||||||
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The authentication configuration.
|
||||||
|
*/
|
||||||
|
auth: z
|
||||||
|
.object({
|
||||||
|
timestampWindowMs: z.coerce
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.positive()
|
||||||
|
.default(5 * 60 * 1000),
|
||||||
|
})
|
||||||
|
.prefault({}),
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Raw configuration object accepted before Zod parsing. */
|
||||||
|
type ConfigInput = z.input<typeof configSchema>;
|
||||||
|
|
||||||
|
/** Fully parsed and defaulted configuration shape. */
|
||||||
|
type ConfigSchema = z.output<typeof configSchema>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Typed, validated server configuration loaded from environment or objects.
|
||||||
|
*/
|
||||||
|
export class Config {
|
||||||
|
/**
|
||||||
|
* Creates a new Config from the environment variables.
|
||||||
|
* @returns The Config.
|
||||||
|
*/
|
||||||
|
static fromEnv(): Config {
|
||||||
|
return this.from({
|
||||||
|
database: {
|
||||||
|
path: process.env.DATABASE_PATH,
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: process.env.SERVER_PORT,
|
||||||
|
maxRequestBodyBytes: process.env.SERVER_MAX_REQUEST_BODY_BYTES,
|
||||||
|
host: process.env.SERVER_HOST,
|
||||||
|
cors: {
|
||||||
|
origin: process.env.CORS_ORIGIN,
|
||||||
|
methods: process.env.CORS_METHODS?.split(','),
|
||||||
|
allowedHeaders: process.env.CORS_ALLOWED_HEADERS?.split(','),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
auth: {
|
||||||
|
timestampWindowMs: process.env.AUTH_TIMESTAMP_WINDOW_MS ? Number(process.env.AUTH_TIMESTAMP_WINDOW_MS) : undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Config from a configuration object.
|
||||||
|
* @param config - The configuration object.
|
||||||
|
* @returns The Config.
|
||||||
|
*/
|
||||||
|
static from(config: ConfigInput): Config {
|
||||||
|
return new Config(configSchema.parse(config));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the database configuration.
|
||||||
|
* @returns The database configuration.
|
||||||
|
*/
|
||||||
|
public get database(): Readonly<ConfigSchema['database']> {
|
||||||
|
return this.config.database;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the server configuration.
|
||||||
|
* @returns The server configuration.
|
||||||
|
*/
|
||||||
|
public get server(): Readonly<ConfigSchema['server']> {
|
||||||
|
return this.config.server;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the authentication configuration.
|
||||||
|
* @returns The authentication configuration.
|
||||||
|
*/
|
||||||
|
public get auth(): Readonly<ConfigSchema['auth']> {
|
||||||
|
return this.config.auth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param config - Parsed configuration produced by the Zod schema.
|
||||||
|
*/
|
||||||
|
private constructor(private readonly config: ConfigSchema) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import Debug, { type Debugger } from 'debug';
|
||||||
|
|
||||||
|
type LogHandler = {
|
||||||
|
(...args: Parameters<Debugger>): void;
|
||||||
|
extend: (namespace: string) => LogHandler;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Declares that Logger instances may also be invoked as functions.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line
|
||||||
|
export interface Logger {
|
||||||
|
(...args: unknown[]): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logger class, similar to 'debug' library but you can call `instanceof Logger` to check if a value is a Logger instance.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line
|
||||||
|
export class Logger {
|
||||||
|
public readonly namespace!: string;
|
||||||
|
|
||||||
|
private readonly handler!: LogHandler;
|
||||||
|
|
||||||
|
public constructor(namespace: string, handler: LogHandler = Debug(namespace)) {
|
||||||
|
/**
|
||||||
|
* I'm going to be honest, this file is somewhat an experiment.
|
||||||
|
* The logger from 'debug' is a fancy function with methods on it.
|
||||||
|
* I wanted to extend that functionality to support 'extend' and also determine whether the object is a Logger instance.
|
||||||
|
* This makes it trivial to perform a type check on the logger, since its no longer just a function. But, I wanted to keep the exact same API
|
||||||
|
* as debug, so this uses gross, disgusting, blasphemous prototype methods to assign a function onto this class prototype.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Make a function that just calls the 'debug' function with the given arguments
|
||||||
|
const logger = ((...args: Parameters<Debugger>): void => {
|
||||||
|
handler(...args);
|
||||||
|
}) as Logger;
|
||||||
|
|
||||||
|
// Mutate the logger function to inherit from this class.
|
||||||
|
// This allows us to use the 'instanceof' operator to check if the object is a Logger instance.
|
||||||
|
Object.setPrototypeOf(logger, new.target.prototype);
|
||||||
|
|
||||||
|
// Add the namespace and the handler to the 'logger' function we defined above
|
||||||
|
// Basically, we are combining this Class with the 'logger' function that we created above.
|
||||||
|
Object.defineProperties(logger, {
|
||||||
|
namespace: {
|
||||||
|
value: namespace,
|
||||||
|
enumerable: true,
|
||||||
|
},
|
||||||
|
handler: {
|
||||||
|
value: handler,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Instead of returning the class, we return the 'logger' function we created above.
|
||||||
|
return logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public extend(childNamespace: string): Logger {
|
||||||
|
return new Logger(`${this.namespace}:${childNamespace}`, this.handler.extend(childNamespace));
|
||||||
|
}
|
||||||
|
|
||||||
|
static isLogger(value: unknown): value is Logger {
|
||||||
|
return value instanceof Logger;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import { describe, expect, test, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { Config } from '../../src/services/config.ts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests that the config defaults to a 1 MiB request body limit.
|
||||||
|
*/
|
||||||
|
const testConfigDefaultsTo1MiBRequestBodyLimit = (): void => {
|
||||||
|
const config = Config.from({
|
||||||
|
database: {},
|
||||||
|
server: {},
|
||||||
|
auth: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(config.server.maxRequestBodyBytes).toBe(1024 * 1024);
|
||||||
|
expect(config.database.path).toBe('data.db');
|
||||||
|
expect(config.server.port).toBe(3000);
|
||||||
|
expect(config.server.host).toBe('0.0.0.0');
|
||||||
|
expect(config.server.cors.origin).toBe('*');
|
||||||
|
expect(config.server.cors.methods).toEqual([ 'GET', 'POST', 'PUT', 'DELETE', 'OPTIONS' ]);
|
||||||
|
expect(config.server.cors.allowedHeaders).toEqual([ 'Content-Type', 'cache-control', 'X-Timestamp', 'X-PublicKey', 'X-Signature' ]);
|
||||||
|
expect(config.auth.timestampWindowMs).toBe(300000);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests that the config loads the request body limit from the environment.
|
||||||
|
*/
|
||||||
|
const testConfigLoadsRequestBodyLimitFromEnvironment = (): void => {
|
||||||
|
vi.stubEnv('SERVER_MAX_REQUEST_BODY_BYTES', '2048');
|
||||||
|
|
||||||
|
expect(Config.fromEnv().server.maxRequestBodyBytes).toBe(2048);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests that the config rejects an invalid request body limit.
|
||||||
|
*/
|
||||||
|
const testConfigRejectsInvalidRequestBodyLimit = (): void => {
|
||||||
|
expect(() =>
|
||||||
|
Config.from({
|
||||||
|
database: {},
|
||||||
|
server: { maxRequestBodyBytes: '0' },
|
||||||
|
auth: {},
|
||||||
|
})).toThrow();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* tests that the config loads the database path from the environment.
|
||||||
|
*/
|
||||||
|
const testConfigLoadsDatabasePathFromEnvironment = (): void => {
|
||||||
|
vi.stubEnv('DATABASE_PATH', 'test.db');
|
||||||
|
|
||||||
|
expect(Config.fromEnv().database.path).toBe('test.db');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* tests that the config loads the server port from the environment.
|
||||||
|
*/
|
||||||
|
const testConfigLoadsServerPortFromEnvironment = (): void => {
|
||||||
|
vi.stubEnv('SERVER_PORT', '3000');
|
||||||
|
|
||||||
|
expect(Config.fromEnv().server.port).toBe(3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* tests that the config loads the server host from the environment.
|
||||||
|
*/
|
||||||
|
const testConfigLoadsServerHostFromEnvironment = (): void => {
|
||||||
|
vi.stubEnv('SERVER_HOST', '0.0.0.0');
|
||||||
|
|
||||||
|
expect(Config.fromEnv().server.host).toBe('0.0.0.0');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* tests that the config loads the cors configuration from the environment.
|
||||||
|
*/
|
||||||
|
const testConfigLoadsCorsConfigurationFromEnvironment = (): void => {
|
||||||
|
vi.stubEnv('CORS_ORIGIN', '*');
|
||||||
|
|
||||||
|
expect(Config.fromEnv().server.cors.origin).toBe('*');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* tests that the config loads the cors methods from the environment.
|
||||||
|
*/
|
||||||
|
const testConfigLoadsCorsMethodsFromEnvironment = (): void => {
|
||||||
|
vi.stubEnv('CORS_METHODS', 'GET,POST,PUT,DELETE,OPTIONS');
|
||||||
|
|
||||||
|
expect(Config.fromEnv().server.cors.methods).toEqual([ 'GET', 'POST', 'PUT', 'DELETE', 'OPTIONS' ]);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* tests that the config loads the cors allowed headers from the environment.
|
||||||
|
*/
|
||||||
|
const testConfigLoadsCorsAllowedHeadersFromEnvironment = (): void => {
|
||||||
|
vi.stubEnv('CORS_ALLOWED_HEADERS', 'Content-Type,cache-control,X-Timestamp');
|
||||||
|
|
||||||
|
expect(Config.fromEnv().server.cors.allowedHeaders).toEqual([ 'Content-Type', 'cache-control', 'X-Timestamp' ]);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* tests that the config loads the auth configuration from the environment.
|
||||||
|
*/
|
||||||
|
const testConfigLoadsAuthConfigurationFromEnvironment = (): void => {
|
||||||
|
vi.stubEnv('AUTH_TIMESTAMP_WINDOW_MS', '1000');
|
||||||
|
|
||||||
|
expect(Config.fromEnv().auth.timestampWindowMs).toBe(1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Config', () => {
|
||||||
|
test('defaults to a 1 MiB request body limit', testConfigDefaultsTo1MiBRequestBodyLimit);
|
||||||
|
test('loads the request body limit from the environment', testConfigLoadsRequestBodyLimitFromEnvironment);
|
||||||
|
test('rejects the invalid request body limit', testConfigRejectsInvalidRequestBodyLimit);
|
||||||
|
test('loads the database path from the environment', testConfigLoadsDatabasePathFromEnvironment);
|
||||||
|
test('loads the server port from the environment', testConfigLoadsServerPortFromEnvironment);
|
||||||
|
test('loads the server host from the environment', testConfigLoadsServerHostFromEnvironment);
|
||||||
|
test('loads the cors configuration from the environment', testConfigLoadsCorsConfigurationFromEnvironment);
|
||||||
|
test('loads the cors methods from the environment', testConfigLoadsCorsMethodsFromEnvironment);
|
||||||
|
test('loads the cors allowed headers from the environment', testConfigLoadsCorsAllowedHeadersFromEnvironment);
|
||||||
|
test('loads the auth configuration from the environment', testConfigLoadsAuthConfigurationFromEnvironment);
|
||||||
|
});
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { Logger } from '../../src/utils/logger.ts';
|
||||||
|
import { expect, describe, test } from 'vitest';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests that a logger function is created and that it is a logger.
|
||||||
|
*/
|
||||||
|
const testLoggerCreatesLoggerFunction = (): void => {
|
||||||
|
const logger = new Logger('test');
|
||||||
|
expect(logger).toBeDefined();
|
||||||
|
expect(logger.namespace).toBe('test');
|
||||||
|
|
||||||
|
expect(Logger.isLogger(logger)).toBe(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests that a logger function is a logger.
|
||||||
|
*/
|
||||||
|
const testLoggerFunctionIsLogger = (): void => {
|
||||||
|
const logger = new Logger('test');
|
||||||
|
expect(Logger.isLogger(logger)).toBe(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests that a logger extends.
|
||||||
|
*/
|
||||||
|
const testLoggerExtends = (): void => {
|
||||||
|
const logger = new Logger('test');
|
||||||
|
const extendedLogger = logger.extend('extended');
|
||||||
|
expect(extendedLogger.namespace).toBe('test:extended');
|
||||||
|
expect(Logger.isLogger(extendedLogger)).toBe(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests that a logger is a logger.
|
||||||
|
*/
|
||||||
|
const testLoggerIsLogger = (): void => {
|
||||||
|
const logger = new Logger('test');
|
||||||
|
expect(Logger.isLogger(logger)).toBe(true);
|
||||||
|
|
||||||
|
// Test not a logger
|
||||||
|
const notALogger = [ 'not a logger', 123, true, false, null, undefined, NaN, Infinity, -Infinity ];
|
||||||
|
|
||||||
|
for (const item of notALogger) {
|
||||||
|
expect(Logger.isLogger(item)).toBe(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Logger', () => {
|
||||||
|
test('creates a logger function', testLoggerCreatesLoggerFunction);
|
||||||
|
test('logger function is a logger', testLoggerFunctionIsLogger);
|
||||||
|
test('logger extends', testLoggerExtends);
|
||||||
|
test('logger is logger', testLoggerIsLogger);
|
||||||
|
});
|
||||||
+4
-1
@@ -1,15 +1,18 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"rootDir": "./src",
|
||||||
|
"outDir": "./dist",
|
||||||
"module": "es2022",
|
"module": "es2022",
|
||||||
"target": "es2022",
|
"target": "es2022",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
|
"rewriteRelativeImportExtensions": true,
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"declarationMap": true
|
"declarationMap": true
|
||||||
},
|
},
|
||||||
"exclude": ["node_modules/**/*", "dist/**/*"]
|
"exclude": ["node_modules/**/*", "dist/**/*", "test"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user