@macss/modular-api 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -13
- package/dist/core/logger/logger.d.ts +79 -0
- package/dist/core/logger/logger.js +132 -0
- package/dist/core/logger/logging_middleware.d.ts +25 -0
- package/dist/core/logger/logging_middleware.js +60 -0
- package/dist/core/modular_api.d.ts +7 -0
- package/dist/core/modular_api.js +12 -1
- package/dist/core/usecase.d.ts +7 -0
- package/dist/core/usecase_handler.js +6 -0
- package/dist/core/usecase_test_handler.d.ts +4 -1
- package/dist/core/usecase_test_handler.js +5 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +8 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Use-case centric toolkit for building modular APIs with Express.
|
|
4
4
|
Define `UseCase` classes (input → validate → execute → output), connect them to HTTP routes, and get automatic Swagger/OpenAPI documentation.
|
|
5
5
|
|
|
6
|
-
>
|
|
6
|
+
> Also available in **Dart**: [modular_api](https://pub.dev/packages/modular_api)
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
@@ -54,6 +54,7 @@ See `example/example.ts` for the full implementation including Input, Output, Us
|
|
|
54
54
|
- Swagger UI at `/docs` — auto-generated from registered use cases
|
|
55
55
|
- Health check at `GET /health` — [IETF Health Check Response Format](doc/health_check_guide.md)
|
|
56
56
|
- Prometheus metrics at `GET /metrics` — [Prometheus exposition format](doc/metrics_guide.md)
|
|
57
|
+
- Structured JSON logging — Loki/Grafana compatible, [request-scoped with trace_id](doc/logger_guide.md)
|
|
57
58
|
- All endpoints default to `POST` (configurable per use case)
|
|
58
59
|
- Full TypeScript declarations (`.d.ts`) included
|
|
59
60
|
|
|
@@ -62,7 +63,7 @@ See `example/example.ts` for the full implementation including Input, Output, Us
|
|
|
62
63
|
## Installation
|
|
63
64
|
|
|
64
65
|
```bash
|
|
65
|
-
npm install
|
|
66
|
+
npm install @macss/modular-api
|
|
66
67
|
```
|
|
67
68
|
|
|
68
69
|
---
|
|
@@ -116,17 +117,6 @@ HTTP Request → ModularApi → Module → UseCase → Business Logic → Output
|
|
|
116
117
|
|
|
117
118
|
---
|
|
118
119
|
|
|
119
|
-
## Dart version
|
|
120
|
-
|
|
121
|
-
This is the TypeScript port. The original Dart version is available at:
|
|
122
|
-
|
|
123
|
-
- **pub.dev**: [modular_api](https://pub.dev/packages/modular_api)
|
|
124
|
-
- **GitHub**: [macss-dev/modular_api](https://github.com/macss-dev/modular_api)
|
|
125
|
-
|
|
126
|
-
Both SDKs share the same architecture and API surface at v0.1.0.
|
|
127
|
-
|
|
128
|
-
---
|
|
129
|
-
|
|
130
120
|
## License
|
|
131
121
|
|
|
132
122
|
MIT © [ccisne.dev](https://ccisne.dev)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 5424 log levels in descending severity order.
|
|
3
|
+
*
|
|
4
|
+
* Filtering rule: if configured `logLevel = X`, only messages with
|
|
5
|
+
* `value <= X` are emitted. Higher values produce total silence.
|
|
6
|
+
*/
|
|
7
|
+
export declare enum LogLevel {
|
|
8
|
+
emergency = 0,// system unusable
|
|
9
|
+
alert = 1,// immediate action required
|
|
10
|
+
critical = 2,// critical condition
|
|
11
|
+
error = 3,// operation error, 5xx
|
|
12
|
+
warning = 4,// abnormal condition, 4xx
|
|
13
|
+
notice = 5,// normal but significant
|
|
14
|
+
info = 6,// normal flow, 2xx/3xx
|
|
15
|
+
debug = 7
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Public logger interface exposed to UseCases.
|
|
19
|
+
*
|
|
20
|
+
* Each method corresponds to an RFC 5424 severity level.
|
|
21
|
+
* `fields` is an optional map of structured data attached to the log entry.
|
|
22
|
+
*/
|
|
23
|
+
export interface ModularLogger {
|
|
24
|
+
/** Request-scoped trace ID for correlation. */
|
|
25
|
+
readonly traceId: string;
|
|
26
|
+
emergency(msg: string, fields?: Record<string, unknown>): void;
|
|
27
|
+
alert(msg: string, fields?: Record<string, unknown>): void;
|
|
28
|
+
critical(msg: string, fields?: Record<string, unknown>): void;
|
|
29
|
+
error(msg: string, fields?: Record<string, unknown>): void;
|
|
30
|
+
warning(msg: string, fields?: Record<string, unknown>): void;
|
|
31
|
+
notice(msg: string, fields?: Record<string, unknown>): void;
|
|
32
|
+
info(msg: string, fields?: Record<string, unknown>): void;
|
|
33
|
+
debug(msg: string, fields?: Record<string, unknown>): void;
|
|
34
|
+
}
|
|
35
|
+
/** Function signature for the output sink — defaults to `console.log`. */
|
|
36
|
+
export type WriteFn = (line: string) => void;
|
|
37
|
+
/**
|
|
38
|
+
* Per-request logger that carries `traceId` and respects `logLevel` filtering.
|
|
39
|
+
*
|
|
40
|
+
* Created by `loggingMiddleware` for each incoming HTTP request and injected
|
|
41
|
+
* into the UseCase via the `logger` property.
|
|
42
|
+
*
|
|
43
|
+
* Accepts an optional `writeFn` for output — defaults to `console.log`.
|
|
44
|
+
* In tests, pass a capturing function to inspect output without side-effects.
|
|
45
|
+
*/
|
|
46
|
+
export declare class RequestScopedLogger implements ModularLogger {
|
|
47
|
+
readonly traceId: string;
|
|
48
|
+
readonly logLevel: LogLevel;
|
|
49
|
+
readonly serviceName: string;
|
|
50
|
+
private readonly writeFn;
|
|
51
|
+
constructor(traceId: string, logLevel: LogLevel, serviceName: string, writeFn?: WriteFn);
|
|
52
|
+
emergency(msg: string, fields?: Record<string, unknown>): void;
|
|
53
|
+
alert(msg: string, fields?: Record<string, unknown>): void;
|
|
54
|
+
critical(msg: string, fields?: Record<string, unknown>): void;
|
|
55
|
+
error(msg: string, fields?: Record<string, unknown>): void;
|
|
56
|
+
warning(msg: string, fields?: Record<string, unknown>): void;
|
|
57
|
+
notice(msg: string, fields?: Record<string, unknown>): void;
|
|
58
|
+
info(msg: string, fields?: Record<string, unknown>): void;
|
|
59
|
+
debug(msg: string, fields?: Record<string, unknown>): void;
|
|
60
|
+
/** Emits a "request received" log at `info` level. */
|
|
61
|
+
logRequest(opts: {
|
|
62
|
+
method: string;
|
|
63
|
+
route: string;
|
|
64
|
+
}): void;
|
|
65
|
+
/** Emits a "request completed" log at the level determined by `statusCode`. */
|
|
66
|
+
logResponse(opts: {
|
|
67
|
+
method: string;
|
|
68
|
+
route: string;
|
|
69
|
+
statusCode: number;
|
|
70
|
+
durationMs: number;
|
|
71
|
+
}): void;
|
|
72
|
+
/** Emits an "unhandled exception" log at `error` level. No stack trace. */
|
|
73
|
+
logUnhandledException(opts: {
|
|
74
|
+
route: string;
|
|
75
|
+
}): void;
|
|
76
|
+
private log;
|
|
77
|
+
/** Maps HTTP status code → RFC 5424 log level. */
|
|
78
|
+
static levelForStatus(status: number): LogLevel;
|
|
79
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ============================================================
|
|
3
|
+
// core/logger/logger.ts
|
|
4
|
+
// LogLevel enum, ModularLogger interface, RequestScopedLogger.
|
|
5
|
+
// Mirror of logger.dart — RFC 5424, JSON to stdout, zero deps.
|
|
6
|
+
// ============================================================
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.RequestScopedLogger = exports.LogLevel = void 0;
|
|
9
|
+
/**
|
|
10
|
+
* RFC 5424 log levels in descending severity order.
|
|
11
|
+
*
|
|
12
|
+
* Filtering rule: if configured `logLevel = X`, only messages with
|
|
13
|
+
* `value <= X` are emitted. Higher values produce total silence.
|
|
14
|
+
*/
|
|
15
|
+
var LogLevel;
|
|
16
|
+
(function (LogLevel) {
|
|
17
|
+
LogLevel[LogLevel["emergency"] = 0] = "emergency";
|
|
18
|
+
LogLevel[LogLevel["alert"] = 1] = "alert";
|
|
19
|
+
LogLevel[LogLevel["critical"] = 2] = "critical";
|
|
20
|
+
LogLevel[LogLevel["error"] = 3] = "error";
|
|
21
|
+
LogLevel[LogLevel["warning"] = 4] = "warning";
|
|
22
|
+
LogLevel[LogLevel["notice"] = 5] = "notice";
|
|
23
|
+
LogLevel[LogLevel["info"] = 6] = "info";
|
|
24
|
+
LogLevel[LogLevel["debug"] = 7] = "debug";
|
|
25
|
+
})(LogLevel || (exports.LogLevel = LogLevel = {}));
|
|
26
|
+
/** Human-readable name for each LogLevel value. */
|
|
27
|
+
const LEVEL_NAME = {
|
|
28
|
+
[LogLevel.emergency]: 'emergency',
|
|
29
|
+
[LogLevel.alert]: 'alert',
|
|
30
|
+
[LogLevel.critical]: 'critical',
|
|
31
|
+
[LogLevel.error]: 'error',
|
|
32
|
+
[LogLevel.warning]: 'warning',
|
|
33
|
+
[LogLevel.notice]: 'notice',
|
|
34
|
+
[LogLevel.info]: 'info',
|
|
35
|
+
[LogLevel.debug]: 'debug',
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Per-request logger that carries `traceId` and respects `logLevel` filtering.
|
|
39
|
+
*
|
|
40
|
+
* Created by `loggingMiddleware` for each incoming HTTP request and injected
|
|
41
|
+
* into the UseCase via the `logger` property.
|
|
42
|
+
*
|
|
43
|
+
* Accepts an optional `writeFn` for output — defaults to `console.log`.
|
|
44
|
+
* In tests, pass a capturing function to inspect output without side-effects.
|
|
45
|
+
*/
|
|
46
|
+
class RequestScopedLogger {
|
|
47
|
+
constructor(traceId, logLevel, serviceName, writeFn = (line) => process.stdout.write(line + '\n')) {
|
|
48
|
+
this.traceId = traceId;
|
|
49
|
+
this.logLevel = logLevel;
|
|
50
|
+
this.serviceName = serviceName;
|
|
51
|
+
this.writeFn = writeFn;
|
|
52
|
+
}
|
|
53
|
+
// ─── Public API (8 RFC 5424 levels) ──────────────────────────────
|
|
54
|
+
emergency(msg, fields) {
|
|
55
|
+
this.log(LogLevel.emergency, msg, { fields });
|
|
56
|
+
}
|
|
57
|
+
alert(msg, fields) {
|
|
58
|
+
this.log(LogLevel.alert, msg, { fields });
|
|
59
|
+
}
|
|
60
|
+
critical(msg, fields) {
|
|
61
|
+
this.log(LogLevel.critical, msg, { fields });
|
|
62
|
+
}
|
|
63
|
+
error(msg, fields) {
|
|
64
|
+
this.log(LogLevel.error, msg, { fields });
|
|
65
|
+
}
|
|
66
|
+
warning(msg, fields) {
|
|
67
|
+
this.log(LogLevel.warning, msg, { fields });
|
|
68
|
+
}
|
|
69
|
+
notice(msg, fields) {
|
|
70
|
+
this.log(LogLevel.notice, msg, { fields });
|
|
71
|
+
}
|
|
72
|
+
info(msg, fields) {
|
|
73
|
+
this.log(LogLevel.info, msg, { fields });
|
|
74
|
+
}
|
|
75
|
+
debug(msg, fields) {
|
|
76
|
+
this.log(LogLevel.debug, msg, { fields });
|
|
77
|
+
}
|
|
78
|
+
// ─── Framework-internal: request/response logging ────────────────
|
|
79
|
+
/** Emits a "request received" log at `info` level. */
|
|
80
|
+
logRequest(opts) {
|
|
81
|
+
this.log(LogLevel.info, 'request received', {
|
|
82
|
+
extra: { method: opts.method, route: opts.route },
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/** Emits a "request completed" log at the level determined by `statusCode`. */
|
|
86
|
+
logResponse(opts) {
|
|
87
|
+
this.log(RequestScopedLogger.levelForStatus(opts.statusCode), 'request completed', {
|
|
88
|
+
extra: {
|
|
89
|
+
method: opts.method,
|
|
90
|
+
route: opts.route,
|
|
91
|
+
status: opts.statusCode,
|
|
92
|
+
duration_ms: opts.durationMs,
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
/** Emits an "unhandled exception" log at `error` level. No stack trace. */
|
|
97
|
+
logUnhandledException(opts) {
|
|
98
|
+
this.log(LogLevel.error, 'unhandled exception', {
|
|
99
|
+
extra: { route: opts.route, status: 500 },
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
// ─── Internal ────────────────────────────────────────────────────
|
|
103
|
+
log(level, msg, opts = {}) {
|
|
104
|
+
// Filtering: only emit if the message level <= configured logLevel.
|
|
105
|
+
if (level > this.logLevel)
|
|
106
|
+
return;
|
|
107
|
+
const entry = {
|
|
108
|
+
ts: Date.now() / 1000,
|
|
109
|
+
level: LEVEL_NAME[level],
|
|
110
|
+
severity: level,
|
|
111
|
+
msg,
|
|
112
|
+
service: this.serviceName,
|
|
113
|
+
trace_id: this.traceId,
|
|
114
|
+
};
|
|
115
|
+
if (opts.extra)
|
|
116
|
+
Object.assign(entry, opts.extra);
|
|
117
|
+
if (opts.fields)
|
|
118
|
+
entry['fields'] = opts.fields;
|
|
119
|
+
this.writeFn(JSON.stringify(entry));
|
|
120
|
+
}
|
|
121
|
+
/** Maps HTTP status code → RFC 5424 log level. */
|
|
122
|
+
static levelForStatus(status) {
|
|
123
|
+
if (status >= 500)
|
|
124
|
+
return LogLevel.error;
|
|
125
|
+
if (status >= 400)
|
|
126
|
+
return LogLevel.warning;
|
|
127
|
+
if (status >= 200 && status < 400)
|
|
128
|
+
return LogLevel.info;
|
|
129
|
+
return LogLevel.notice; // 1xx
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
exports.RequestScopedLogger = RequestScopedLogger;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { RequestHandler } from 'express';
|
|
2
|
+
import { LogLevel } from './logger';
|
|
3
|
+
import type { WriteFn } from './logger';
|
|
4
|
+
/** Key used in `res.locals` to propagate the logger to downstream handlers. */
|
|
5
|
+
export declare const LOGGER_LOCALS_KEY = "modularLogger";
|
|
6
|
+
export interface LoggingMiddlewareOptions {
|
|
7
|
+
logLevel: LogLevel;
|
|
8
|
+
serviceName: string;
|
|
9
|
+
excludedRoutes?: string[];
|
|
10
|
+
/** Override output for testing. Defaults to stdout. */
|
|
11
|
+
writeFn?: WriteFn;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Creates an Express middleware that:
|
|
15
|
+
*
|
|
16
|
+
* 1. Reads or generates a `trace_id` (from `X-Request-ID` header).
|
|
17
|
+
* 2. Creates a {@link RequestScopedLogger} scoped to the current request.
|
|
18
|
+
* 3. Emits a `"request received"` log at `info` level.
|
|
19
|
+
* 4. Passes the logger via `res.locals` for downstream handlers.
|
|
20
|
+
* 5. Emits a `"request completed"` log (level based on status code).
|
|
21
|
+
* 6. Returns the `X-Request-ID` header in the response.
|
|
22
|
+
*
|
|
23
|
+
* Requests whose path matches `excludedRoutes` are passed through silently.
|
|
24
|
+
*/
|
|
25
|
+
export declare function loggingMiddleware(opts: LoggingMiddlewareOptions): RequestHandler;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ============================================================
|
|
3
|
+
// core/logger/logging_middleware.ts
|
|
4
|
+
// Express middleware — trace_id, structured JSON logs.
|
|
5
|
+
// Mirror of logging_middleware.dart.
|
|
6
|
+
// ============================================================
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.LOGGER_LOCALS_KEY = void 0;
|
|
9
|
+
exports.loggingMiddleware = loggingMiddleware;
|
|
10
|
+
const node_crypto_1 = require("node:crypto");
|
|
11
|
+
const logger_1 = require("./logger");
|
|
12
|
+
/** Key used in `res.locals` to propagate the logger to downstream handlers. */
|
|
13
|
+
exports.LOGGER_LOCALS_KEY = 'modularLogger';
|
|
14
|
+
/**
|
|
15
|
+
* Creates an Express middleware that:
|
|
16
|
+
*
|
|
17
|
+
* 1. Reads or generates a `trace_id` (from `X-Request-ID` header).
|
|
18
|
+
* 2. Creates a {@link RequestScopedLogger} scoped to the current request.
|
|
19
|
+
* 3. Emits a `"request received"` log at `info` level.
|
|
20
|
+
* 4. Passes the logger via `res.locals` for downstream handlers.
|
|
21
|
+
* 5. Emits a `"request completed"` log (level based on status code).
|
|
22
|
+
* 6. Returns the `X-Request-ID` header in the response.
|
|
23
|
+
*
|
|
24
|
+
* Requests whose path matches `excludedRoutes` are passed through silently.
|
|
25
|
+
*/
|
|
26
|
+
function loggingMiddleware(opts) {
|
|
27
|
+
const excludedSet = new Set(opts.excludedRoutes ?? []);
|
|
28
|
+
return (req, res, next) => {
|
|
29
|
+
const path = req.path;
|
|
30
|
+
// Skip excluded routes (health, metrics, docs).
|
|
31
|
+
if (excludedSet.has(path)) {
|
|
32
|
+
return next();
|
|
33
|
+
}
|
|
34
|
+
// 1. Resolve trace_id
|
|
35
|
+
const headerValue = req.headers['x-request-id'];
|
|
36
|
+
const traceId = typeof headerValue === 'string' && headerValue.length > 0 ? headerValue : (0, node_crypto_1.randomUUID)();
|
|
37
|
+
// 2. Create per-request logger
|
|
38
|
+
const logger = new logger_1.RequestScopedLogger(traceId, opts.logLevel, opts.serviceName, opts.writeFn);
|
|
39
|
+
const method = req.method.toUpperCase();
|
|
40
|
+
const route = path;
|
|
41
|
+
// 3. "request received"
|
|
42
|
+
logger.logRequest({ method, route });
|
|
43
|
+
// 4. Propagate logger via res.locals
|
|
44
|
+
res.locals[exports.LOGGER_LOCALS_KEY] = logger;
|
|
45
|
+
// 5. Attach X-Request-ID to response
|
|
46
|
+
res.setHeader('X-Request-ID', traceId);
|
|
47
|
+
// 6. Capture timing and emit response log on finish
|
|
48
|
+
const startNs = process.hrtime.bigint();
|
|
49
|
+
res.on('finish', () => {
|
|
50
|
+
const durationMs = Number(process.hrtime.bigint() - startNs) / 1e6;
|
|
51
|
+
logger.logResponse({
|
|
52
|
+
method,
|
|
53
|
+
route,
|
|
54
|
+
statusCode: res.statusCode,
|
|
55
|
+
durationMs,
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
next();
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -2,6 +2,7 @@ import { type RequestHandler } from 'express';
|
|
|
2
2
|
import { ModuleBuilder } from './module_builder';
|
|
3
3
|
import type { HealthCheck } from './health/health_check';
|
|
4
4
|
import { MetricsRegistrar } from './metrics/metric_registry';
|
|
5
|
+
import { LogLevel } from './logger/logger';
|
|
5
6
|
export interface ModularApiOptions {
|
|
6
7
|
/** Base path prefix for all module routes. Default: '/api' */
|
|
7
8
|
basePath?: string;
|
|
@@ -20,6 +21,11 @@ export interface ModularApiOptions {
|
|
|
20
21
|
metricsPath?: string;
|
|
21
22
|
/** Routes excluded from instrumentation. Default: ['/metrics', '/health', '/docs'] */
|
|
22
23
|
excludedMetricsRoutes?: string[];
|
|
24
|
+
/**
|
|
25
|
+
* Minimum log level for the structured JSON logger.
|
|
26
|
+
* Default: LogLevel.info (emits emergency..info, suppresses debug).
|
|
27
|
+
*/
|
|
28
|
+
logLevel?: LogLevel;
|
|
23
29
|
}
|
|
24
30
|
/**
|
|
25
31
|
* Main entry point for modular_api.
|
|
@@ -57,6 +63,7 @@ export declare class ModularApi {
|
|
|
57
63
|
private readonly httpRequestsTotal?;
|
|
58
64
|
private readonly httpRequestsInFlight?;
|
|
59
65
|
private readonly httpRequestDuration?;
|
|
66
|
+
private readonly logLevel;
|
|
60
67
|
/** Public accessor for custom-metric registration. Undefined when metrics are disabled. */
|
|
61
68
|
get metrics(): MetricsRegistrar | undefined;
|
|
62
69
|
constructor(options?: ModularApiOptions);
|
package/dist/core/modular_api.js
CHANGED
|
@@ -17,6 +17,8 @@ const health_service_1 = require("./health/health_service");
|
|
|
17
17
|
const health_handler_1 = require("./health/health_handler");
|
|
18
18
|
const metric_registry_1 = require("./metrics/metric_registry");
|
|
19
19
|
const metrics_middleware_1 = require("./metrics/metrics_middleware");
|
|
20
|
+
const logging_middleware_1 = require("./logger/logging_middleware");
|
|
21
|
+
const logger_1 = require("./logger/logger");
|
|
20
22
|
const registry_1 = require("./registry");
|
|
21
23
|
/**
|
|
22
24
|
* Main entry point for modular_api.
|
|
@@ -56,6 +58,8 @@ class ModularApi {
|
|
|
56
58
|
this.metricsEnabled = options.metricsEnabled ?? false;
|
|
57
59
|
this.metricsPath = options.metricsPath ?? '/metrics';
|
|
58
60
|
this.excludedMetricsRoutes = options.excludedMetricsRoutes ?? ['/metrics', '/health', '/docs'];
|
|
61
|
+
// Logging
|
|
62
|
+
this.logLevel = options.logLevel ?? logger_1.LogLevel.info;
|
|
59
63
|
if (this.metricsEnabled) {
|
|
60
64
|
this.metricRegistry = new metric_registry_1.MetricRegistry();
|
|
61
65
|
this._metricsRegistrar = new metric_registry_1.MetricsRegistrar(this.metricRegistry);
|
|
@@ -134,7 +138,14 @@ class ModularApi {
|
|
|
134
138
|
serve(options) {
|
|
135
139
|
const { port, host = '0.0.0.0' } = options;
|
|
136
140
|
return new Promise((resolve) => {
|
|
137
|
-
//
|
|
141
|
+
// Logging middleware FIRST — trace_id + structured JSON logs.
|
|
142
|
+
const excludedLogRoutes = ['/health', this.metricsPath, '/docs', '/docs/'];
|
|
143
|
+
this.app.use((0, logging_middleware_1.loggingMiddleware)({
|
|
144
|
+
logLevel: this.logLevel,
|
|
145
|
+
serviceName: this.title,
|
|
146
|
+
excludedRoutes: excludedLogRoutes,
|
|
147
|
+
}));
|
|
148
|
+
// Metrics middleware — before user middlewares & routes.
|
|
138
149
|
// Created here so registeredPaths is populated from apiRegistry.
|
|
139
150
|
if (this.metricsEnabled &&
|
|
140
151
|
this.httpRequestsTotal &&
|
package/dist/core/usecase.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ModularLogger } from './logger/logger';
|
|
1
2
|
/**
|
|
2
3
|
* **Contract** — use `implements Input`.
|
|
3
4
|
*
|
|
@@ -94,6 +95,12 @@ export declare abstract class UseCase<I extends Input, O extends Output> {
|
|
|
94
95
|
abstract readonly input: I;
|
|
95
96
|
/** Output DTO — set in execute(). */
|
|
96
97
|
abstract output: O;
|
|
98
|
+
/**
|
|
99
|
+
* Request-scoped logger injected by the framework's logging middleware.
|
|
100
|
+
* Available inside `execute()`. Undefined when running without middleware
|
|
101
|
+
* or in tests that don't provide one.
|
|
102
|
+
*/
|
|
103
|
+
logger?: ModularLogger;
|
|
97
104
|
/**
|
|
98
105
|
* Synchronous validation.
|
|
99
106
|
* Return a human-readable error string to abort execution with HTTP 400.
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
8
|
exports.useCaseHandler = useCaseHandler;
|
|
9
9
|
const use_case_exception_1 = require("./use_case_exception");
|
|
10
|
+
const logging_middleware_1 = require("./logger/logging_middleware");
|
|
10
11
|
const JSON_HEADERS = { 'Content-Type': 'application/json; charset=utf-8' };
|
|
11
12
|
/**
|
|
12
13
|
* Wraps any UseCase factory into an Express RequestHandler.
|
|
@@ -36,6 +37,11 @@ function useCaseHandler(factory) {
|
|
|
36
37
|
: (req.body ?? {});
|
|
37
38
|
// 2. Build use case
|
|
38
39
|
const useCase = factory(data);
|
|
40
|
+
// 2b. Inject request-scoped logger (if logging middleware is active)
|
|
41
|
+
const logger = res.locals[logging_middleware_1.LOGGER_LOCALS_KEY];
|
|
42
|
+
if (logger) {
|
|
43
|
+
useCase.logger = logger;
|
|
44
|
+
}
|
|
39
45
|
// 3. Validate
|
|
40
46
|
const validationError = useCase.validate();
|
|
41
47
|
if (validationError !== null) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { UseCaseFactory, Input, Output } from './usecase';
|
|
2
|
+
import type { ModularLogger } from './logger/logger';
|
|
2
3
|
export interface TestResponse {
|
|
3
4
|
statusCode: number;
|
|
4
5
|
body: Record<string, unknown>;
|
|
@@ -21,4 +22,6 @@ export interface TestResponse {
|
|
|
21
22
|
* expect(response.body).toEqual({ message: 'Hello, World!' });
|
|
22
23
|
* ```
|
|
23
24
|
*/
|
|
24
|
-
export declare function useCaseTestHandler<I extends Input, O extends Output>(factory: UseCaseFactory<I, O>, input?: Record<string, unknown
|
|
25
|
+
export declare function useCaseTestHandler<I extends Input, O extends Output>(factory: UseCaseFactory<I, O>, input?: Record<string, unknown>, options?: {
|
|
26
|
+
logger?: ModularLogger;
|
|
27
|
+
}): Promise<TestResponse>;
|
|
@@ -25,9 +25,13 @@ const use_case_exception_1 = require("./use_case_exception");
|
|
|
25
25
|
* expect(response.body).toEqual({ message: 'Hello, World!' });
|
|
26
26
|
* ```
|
|
27
27
|
*/
|
|
28
|
-
async function useCaseTestHandler(factory, input = {}) {
|
|
28
|
+
async function useCaseTestHandler(factory, input = {}, options) {
|
|
29
29
|
try {
|
|
30
30
|
const useCase = factory(input);
|
|
31
|
+
// Inject logger if provided
|
|
32
|
+
if (options?.logger) {
|
|
33
|
+
useCase.logger = options.logger;
|
|
34
|
+
}
|
|
31
35
|
const validationError = useCase.validate();
|
|
32
36
|
if (validationError !== null) {
|
|
33
37
|
return {
|
package/dist/index.d.ts
CHANGED
|
@@ -17,3 +17,7 @@ export { healthHandler } from './core/health/health_handler';
|
|
|
17
17
|
export { MetricRegistry, MetricsRegistrar } from './core/metrics/metric_registry';
|
|
18
18
|
export { metricsMiddleware, metricsHandler } from './core/metrics/metrics_middleware';
|
|
19
19
|
export type { MetricsMiddlewareOptions } from './core/metrics/metrics_middleware';
|
|
20
|
+
export { LogLevel, RequestScopedLogger } from './core/logger/logger';
|
|
21
|
+
export type { ModularLogger } from './core/logger/logger';
|
|
22
|
+
export { loggingMiddleware, LOGGER_LOCALS_KEY } from './core/logger/logging_middleware';
|
|
23
|
+
export type { LoggingMiddlewareOptions } from './core/logger/logging_middleware';
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// import { ModularApi, UseCase, Input, Output } from 'modular_api'
|
|
6
6
|
// ============================================================
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
-
exports.metricsHandler = exports.metricsMiddleware = exports.MetricsRegistrar = exports.MetricRegistry = exports.healthHandler = exports.HealthResponse = exports.HealthService = exports.HealthCheckResult = exports.HealthCheck = exports.cors = exports.useCaseTestHandler = exports.ModuleBuilder = exports.ModularApi = exports.UseCaseException = exports.UseCase = exports.Output = exports.Input = void 0;
|
|
8
|
+
exports.LOGGER_LOCALS_KEY = exports.loggingMiddleware = exports.RequestScopedLogger = exports.LogLevel = exports.metricsHandler = exports.metricsMiddleware = exports.MetricsRegistrar = exports.MetricRegistry = exports.healthHandler = exports.HealthResponse = exports.HealthService = exports.HealthCheckResult = exports.HealthCheck = exports.cors = exports.useCaseTestHandler = exports.ModuleBuilder = exports.ModularApi = exports.UseCaseException = exports.UseCase = exports.Output = exports.Input = void 0;
|
|
9
9
|
// Core abstractions
|
|
10
10
|
var usecase_1 = require("./core/usecase");
|
|
11
11
|
Object.defineProperty(exports, "Input", { enumerable: true, get: function () { return usecase_1.Input; } });
|
|
@@ -42,3 +42,10 @@ Object.defineProperty(exports, "MetricsRegistrar", { enumerable: true, get: func
|
|
|
42
42
|
var metrics_middleware_1 = require("./core/metrics/metrics_middleware");
|
|
43
43
|
Object.defineProperty(exports, "metricsMiddleware", { enumerable: true, get: function () { return metrics_middleware_1.metricsMiddleware; } });
|
|
44
44
|
Object.defineProperty(exports, "metricsHandler", { enumerable: true, get: function () { return metrics_middleware_1.metricsHandler; } });
|
|
45
|
+
// Logger — Structured JSON logging (Loki/Grafana compatible)
|
|
46
|
+
var logger_1 = require("./core/logger/logger");
|
|
47
|
+
Object.defineProperty(exports, "LogLevel", { enumerable: true, get: function () { return logger_1.LogLevel; } });
|
|
48
|
+
Object.defineProperty(exports, "RequestScopedLogger", { enumerable: true, get: function () { return logger_1.RequestScopedLogger; } });
|
|
49
|
+
var logging_middleware_1 = require("./core/logger/logging_middleware");
|
|
50
|
+
Object.defineProperty(exports, "loggingMiddleware", { enumerable: true, get: function () { return logging_middleware_1.loggingMiddleware; } });
|
|
51
|
+
Object.defineProperty(exports, "LOGGER_LOCALS_KEY", { enumerable: true, get: function () { return logging_middleware_1.LOGGER_LOCALS_KEY; } });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@macss/modular-api",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Use-case-centric toolkit for building modular APIs with Express. Define UseCase classes (input → validate → execute → output), connect them to HTTP routes, and expose Swagger/OpenAPI documentation automatically.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|