@bymax-one/nest-core 1.0.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/CHANGELOG.md +77 -0
- package/LICENSE +21 -0
- package/README.md +716 -0
- package/dist/health/index.cjs +2 -0
- package/dist/health/index.d.cts +64 -0
- package/dist/health/index.d.ts +64 -0
- package/dist/health/index.mjs +1 -0
- package/dist/index.cjs +984 -0
- package/dist/index.d.cts +550 -0
- package/dist/index.d.ts +550 -0
- package/dist/index.mjs +960 -0
- package/dist/pagination/index.cjs +112 -0
- package/dist/pagination/index.d.cts +146 -0
- package/dist/pagination/index.d.ts +146 -0
- package/dist/pagination/index.mjs +105 -0
- package/package.json +169 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,960 @@
|
|
|
1
|
+
import { Catch, Inject, Optional, Injectable, ConfigurableModuleBuilder, Module, HttpException, Get, Res, Controller, HttpStatus } from '@nestjs/common';
|
|
2
|
+
import { HttpAdapterHost, BaseExceptionFilter, APP_FILTER, APP_INTERCEPTOR } from '@nestjs/core';
|
|
3
|
+
import { tap, catchError, throwError } from 'rxjs';
|
|
4
|
+
|
|
5
|
+
var __defProp = Object.defineProperty;
|
|
6
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
7
|
+
var __decorateClass = (decorators, target, key, kind) => {
|
|
8
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
9
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
10
|
+
if (decorator = decorators[i])
|
|
11
|
+
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
12
|
+
if (kind && result) __defProp(target, key, result);
|
|
13
|
+
return result;
|
|
14
|
+
};
|
|
15
|
+
var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
|
|
16
|
+
|
|
17
|
+
// src/core.options.ts
|
|
18
|
+
var DEFAULT_HEALTH_PATH = "health";
|
|
19
|
+
var DEFAULT_INDICATOR_TIMEOUT_MS = 5e3;
|
|
20
|
+
var DEFAULT_METRICS_PATH = "metrics";
|
|
21
|
+
function deepFreeze(value) {
|
|
22
|
+
if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
|
|
23
|
+
Object.freeze(value);
|
|
24
|
+
for (const child of Object.values(value)) {
|
|
25
|
+
deepFreeze(child);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
function resolveEnvelope(raw) {
|
|
31
|
+
return {
|
|
32
|
+
enabled: raw?.enabled ?? true,
|
|
33
|
+
exposeInternals: raw?.exposeInternals ?? false
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function resolveTiming(raw) {
|
|
37
|
+
const enabled = raw?.enabled ?? true;
|
|
38
|
+
const threshold = raw?.slowRequestThresholdMs;
|
|
39
|
+
return threshold === void 0 ? { enabled } : { enabled, slowRequestThresholdMs: threshold };
|
|
40
|
+
}
|
|
41
|
+
function resolveHealth(raw) {
|
|
42
|
+
return {
|
|
43
|
+
enabled: raw?.enabled ?? true,
|
|
44
|
+
path: raw?.path ?? DEFAULT_HEALTH_PATH,
|
|
45
|
+
indicatorTimeoutMs: raw?.indicatorTimeoutMs ?? DEFAULT_INDICATOR_TIMEOUT_MS
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function resolveMetrics(raw) {
|
|
49
|
+
return {
|
|
50
|
+
enabled: raw?.enabled ?? false,
|
|
51
|
+
path: raw?.path ?? DEFAULT_METRICS_PATH,
|
|
52
|
+
collectDefaultMetrics: raw?.collectDefaultMetrics ?? true,
|
|
53
|
+
defaultLabels: { ...raw?.defaultLabels ?? {} }
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function normalizeCoreOptions(raw) {
|
|
57
|
+
return deepFreeze({
|
|
58
|
+
envelope: resolveEnvelope(raw?.envelope),
|
|
59
|
+
timing: resolveTiming(raw?.timing),
|
|
60
|
+
health: resolveHealth(raw?.health),
|
|
61
|
+
metrics: resolveMetrics(raw?.metrics)
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
normalizeCoreOptions();
|
|
65
|
+
|
|
66
|
+
// src/core.tokens.ts
|
|
67
|
+
var BYMAX_CORE_OPTIONS = /* @__PURE__ */ Symbol("BYMAX_CORE_OPTIONS");
|
|
68
|
+
var BYMAX_CORRELATION_PROVIDER = /* @__PURE__ */ Symbol("BYMAX_CORRELATION_PROVIDER");
|
|
69
|
+
var BYMAX_TIMING_SINK = /* @__PURE__ */ Symbol("BYMAX_TIMING_SINK");
|
|
70
|
+
var BYMAX_HEALTH_INDICATORS = /* @__PURE__ */ Symbol("BYMAX_HEALTH_INDICATORS");
|
|
71
|
+
var BYMAX_METRICS_REGISTRY = /* @__PURE__ */ Symbol("BYMAX_METRICS_REGISTRY");
|
|
72
|
+
|
|
73
|
+
// src/timing/timing.clock.ts
|
|
74
|
+
var DEFAULT_MONOTONIC_CLOCK = {
|
|
75
|
+
now: () => performance.now()
|
|
76
|
+
};
|
|
77
|
+
var BYMAX_TIMING_CLOCK = /* @__PURE__ */ Symbol("BYMAX_TIMING_CLOCK");
|
|
78
|
+
|
|
79
|
+
// src/defaults.providers.ts
|
|
80
|
+
var NoopCorrelationIdProvider = class {
|
|
81
|
+
/**
|
|
82
|
+
* Resolve no correlation id.
|
|
83
|
+
*
|
|
84
|
+
* @returns Always `undefined`.
|
|
85
|
+
*/
|
|
86
|
+
getCorrelationId() {
|
|
87
|
+
return void 0;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
var NoopTimingSink = class {
|
|
91
|
+
/**
|
|
92
|
+
* Discard the sample.
|
|
93
|
+
*
|
|
94
|
+
* @param _sample - The sample to discard.
|
|
95
|
+
*/
|
|
96
|
+
record(_sample) {
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
function buildDefaultProviders() {
|
|
100
|
+
return [{ provide: BYMAX_TIMING_CLOCK, useValue: DEFAULT_MONOTONIC_CLOCK }];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/envelope/error-codes.ts
|
|
104
|
+
var BYMAX_BAD_REQUEST = "BYMAX_BAD_REQUEST";
|
|
105
|
+
var BYMAX_VALIDATION_FAILED = "BYMAX_VALIDATION_FAILED";
|
|
106
|
+
var BYMAX_UNAUTHORIZED = "BYMAX_UNAUTHORIZED";
|
|
107
|
+
var BYMAX_FORBIDDEN = "BYMAX_FORBIDDEN";
|
|
108
|
+
var BYMAX_NOT_FOUND = "BYMAX_NOT_FOUND";
|
|
109
|
+
var BYMAX_CONFLICT = "BYMAX_CONFLICT";
|
|
110
|
+
var BYMAX_PAYLOAD_TOO_LARGE = "BYMAX_PAYLOAD_TOO_LARGE";
|
|
111
|
+
var BYMAX_UNSUPPORTED_MEDIA_TYPE = "BYMAX_UNSUPPORTED_MEDIA_TYPE";
|
|
112
|
+
var BYMAX_UNPROCESSABLE_ENTITY = "BYMAX_UNPROCESSABLE_ENTITY";
|
|
113
|
+
var BYMAX_TOO_MANY_REQUESTS = "BYMAX_TOO_MANY_REQUESTS";
|
|
114
|
+
var BYMAX_CLIENT_ERROR = "BYMAX_CLIENT_ERROR";
|
|
115
|
+
var BYMAX_INTERNAL_ERROR = "BYMAX_INTERNAL_ERROR";
|
|
116
|
+
var BYMAX_NOT_IMPLEMENTED = "BYMAX_NOT_IMPLEMENTED";
|
|
117
|
+
var BYMAX_BAD_GATEWAY = "BYMAX_BAD_GATEWAY";
|
|
118
|
+
var BYMAX_SERVICE_UNAVAILABLE = "BYMAX_SERVICE_UNAVAILABLE";
|
|
119
|
+
var BYMAX_GATEWAY_TIMEOUT = "BYMAX_GATEWAY_TIMEOUT";
|
|
120
|
+
var STATUS_CODES = /* @__PURE__ */ new Map([
|
|
121
|
+
[400, BYMAX_BAD_REQUEST],
|
|
122
|
+
[401, BYMAX_UNAUTHORIZED],
|
|
123
|
+
[403, BYMAX_FORBIDDEN],
|
|
124
|
+
[404, BYMAX_NOT_FOUND],
|
|
125
|
+
[409, BYMAX_CONFLICT],
|
|
126
|
+
[413, BYMAX_PAYLOAD_TOO_LARGE],
|
|
127
|
+
[415, BYMAX_UNSUPPORTED_MEDIA_TYPE],
|
|
128
|
+
[422, BYMAX_UNPROCESSABLE_ENTITY],
|
|
129
|
+
[429, BYMAX_TOO_MANY_REQUESTS],
|
|
130
|
+
[500, BYMAX_INTERNAL_ERROR],
|
|
131
|
+
[501, BYMAX_NOT_IMPLEMENTED],
|
|
132
|
+
[502, BYMAX_BAD_GATEWAY],
|
|
133
|
+
[503, BYMAX_SERVICE_UNAVAILABLE],
|
|
134
|
+
[504, BYMAX_GATEWAY_TIMEOUT]
|
|
135
|
+
]);
|
|
136
|
+
var CLIENT_ERROR_MIN = 400;
|
|
137
|
+
var CLIENT_ERROR_MAX = 500;
|
|
138
|
+
function codeForStatus(status) {
|
|
139
|
+
const catalogued = STATUS_CODES.get(status);
|
|
140
|
+
if (catalogued !== void 0) {
|
|
141
|
+
return catalogued;
|
|
142
|
+
}
|
|
143
|
+
if (status >= CLIENT_ERROR_MIN && status < CLIENT_ERROR_MAX) {
|
|
144
|
+
return BYMAX_CLIENT_ERROR;
|
|
145
|
+
}
|
|
146
|
+
return BYMAX_INTERNAL_ERROR;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// src/envelope/error-envelope.ts
|
|
150
|
+
function buildErrorEnvelope(input) {
|
|
151
|
+
const base = {
|
|
152
|
+
statusCode: input.statusCode,
|
|
153
|
+
code: input.code,
|
|
154
|
+
message: input.message,
|
|
155
|
+
timestamp: input.now().toISOString(),
|
|
156
|
+
path: input.path
|
|
157
|
+
};
|
|
158
|
+
return {
|
|
159
|
+
...base,
|
|
160
|
+
...input.details !== void 0 ? { details: input.details } : {},
|
|
161
|
+
...input.correlationId !== void 0 ? { correlationId: input.correlationId } : {}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/envelope/exception.filter.ts
|
|
166
|
+
var INTERNAL_ERROR_STATUS = 500;
|
|
167
|
+
var INTERNAL_ERROR_MESSAGE = "Internal server error";
|
|
168
|
+
var VALIDATION_FAILED_MESSAGE = "Validation failed";
|
|
169
|
+
function extractExplicitCode(response) {
|
|
170
|
+
if (typeof response !== "object" || response === null || !("code" in response)) {
|
|
171
|
+
return void 0;
|
|
172
|
+
}
|
|
173
|
+
const code = response.code;
|
|
174
|
+
return typeof code === "string" ? code : void 0;
|
|
175
|
+
}
|
|
176
|
+
function isValidationResponse(response) {
|
|
177
|
+
return typeof response === "object" && response !== null && "message" in response && Array.isArray(response.message);
|
|
178
|
+
}
|
|
179
|
+
function toValidationDetails(violations) {
|
|
180
|
+
return violations.map(
|
|
181
|
+
(violation) => typeof violation === "string" ? { issue: violation } : violation
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
function extractHttpMessage(response, exception) {
|
|
185
|
+
if (typeof response === "string") {
|
|
186
|
+
return response;
|
|
187
|
+
}
|
|
188
|
+
if (typeof response === "object" && response !== null && "message" in response) {
|
|
189
|
+
const message = response.message;
|
|
190
|
+
if (typeof message === "string") {
|
|
191
|
+
return message;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return exception.message;
|
|
195
|
+
}
|
|
196
|
+
function buildInternalDetails(exception) {
|
|
197
|
+
if (exception instanceof Error) {
|
|
198
|
+
return exception.stack !== void 0 ? { message: exception.message, stack: exception.stack } : { message: exception.message };
|
|
199
|
+
}
|
|
200
|
+
return { message: String(exception) };
|
|
201
|
+
}
|
|
202
|
+
var BymaxExceptionFilter = class {
|
|
203
|
+
/**
|
|
204
|
+
* @param options - Resolved core options; drives the `exposeInternals` switch.
|
|
205
|
+
* @param correlation - Provider resolving the current request's correlation id.
|
|
206
|
+
* Injected with `@Optional()`: `BymaxCoreModule` binds no local default for
|
|
207
|
+
* this token, so a consumer's own `BYMAX_CORRELATION_PROVIDER` binding
|
|
208
|
+
* (from their own, globally-visible module) is not shadowed by one; when
|
|
209
|
+
* nothing is bound, this falls back to a no-op that omits `correlationId`.
|
|
210
|
+
* @param adapterHost - Host of the live HTTP adapter, resolved lazily per catch.
|
|
211
|
+
*/
|
|
212
|
+
constructor(options, correlation, adapterHost) {
|
|
213
|
+
this.options = options;
|
|
214
|
+
this.adapterHost = adapterHost;
|
|
215
|
+
/** Clock used to stamp the envelope timestamp; overridable in tests via fake timers. */
|
|
216
|
+
this.now = () => /* @__PURE__ */ new Date();
|
|
217
|
+
this.correlation = correlation ?? new NoopCorrelationIdProvider();
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Format the exception into the stable envelope and reply with it.
|
|
221
|
+
*
|
|
222
|
+
* Non-HTTP execution contexts (GraphQL, RPC) are out of scope and are
|
|
223
|
+
* rethrown untouched so their own error handling applies.
|
|
224
|
+
*
|
|
225
|
+
* @param exception - The error that escaped the handler.
|
|
226
|
+
* @param host - The arguments host for the current execution context.
|
|
227
|
+
*/
|
|
228
|
+
catch(exception, host) {
|
|
229
|
+
if (host.getType() !== "http") {
|
|
230
|
+
throw exception;
|
|
231
|
+
}
|
|
232
|
+
const { httpAdapter } = this.adapterHost;
|
|
233
|
+
const ctx = host.switchToHttp();
|
|
234
|
+
const request = ctx.getRequest();
|
|
235
|
+
const response = ctx.getResponse();
|
|
236
|
+
const correlationId = this.correlation.getCorrelationId();
|
|
237
|
+
const context = {
|
|
238
|
+
method: String(httpAdapter.getRequestMethod(request)),
|
|
239
|
+
path: String(httpAdapter.getRequestUrl(request)),
|
|
240
|
+
...correlationId !== void 0 ? { correlationId } : {}
|
|
241
|
+
};
|
|
242
|
+
const envelope = this.buildEnvelope(exception, context);
|
|
243
|
+
httpAdapter.reply(response, envelope, envelope.statusCode);
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Select the mapping rule for the exception and build its envelope. An
|
|
247
|
+
* unknown error is handed to the observability seam before it collapses, so
|
|
248
|
+
* an integration can record the original error with the request context.
|
|
249
|
+
*
|
|
250
|
+
* @param exception - The error that escaped the handler.
|
|
251
|
+
* @param context - The neutral request context.
|
|
252
|
+
* @returns The formatted envelope.
|
|
253
|
+
*/
|
|
254
|
+
buildEnvelope(exception, context) {
|
|
255
|
+
if (exception instanceof HttpException) {
|
|
256
|
+
return this.mapHttpException(exception, context);
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
this.onUnexpectedError(exception, context);
|
|
260
|
+
} catch {
|
|
261
|
+
}
|
|
262
|
+
return this.mapUnknown(exception, context);
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Map an `HttpException` to the envelope. Explicit domain codes pass through;
|
|
266
|
+
* the validation shape becomes `BYMAX_VALIDATION_FAILED` with structured
|
|
267
|
+
* details; everything else derives its code from the status.
|
|
268
|
+
*
|
|
269
|
+
* @param exception - The HTTP exception to format.
|
|
270
|
+
* @param context - The neutral request context.
|
|
271
|
+
* @returns The formatted envelope.
|
|
272
|
+
*/
|
|
273
|
+
mapHttpException(exception, context) {
|
|
274
|
+
const status = exception.getStatus();
|
|
275
|
+
const response = exception.getResponse();
|
|
276
|
+
const explicitCode = extractExplicitCode(response);
|
|
277
|
+
if (explicitCode !== void 0) {
|
|
278
|
+
return this.toEnvelope(status, explicitCode, extractHttpMessage(response, exception), context);
|
|
279
|
+
}
|
|
280
|
+
if (isValidationResponse(response)) {
|
|
281
|
+
return this.toEnvelope(
|
|
282
|
+
status,
|
|
283
|
+
BYMAX_VALIDATION_FAILED,
|
|
284
|
+
VALIDATION_FAILED_MESSAGE,
|
|
285
|
+
context,
|
|
286
|
+
toValidationDetails(response.message)
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
return this.toEnvelope(
|
|
290
|
+
status,
|
|
291
|
+
codeForStatus(status),
|
|
292
|
+
extractHttpMessage(response, exception),
|
|
293
|
+
context
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Collapse an unknown error to the fixed, production-safe 500. The original
|
|
298
|
+
* error is never serialized unless `exposeInternals` is on, in which case its
|
|
299
|
+
* message and stack are attached to `details` (development only).
|
|
300
|
+
*
|
|
301
|
+
* @param exception - The original thrown value.
|
|
302
|
+
* @param context - The neutral request context.
|
|
303
|
+
* @returns The generic internal-error envelope.
|
|
304
|
+
*/
|
|
305
|
+
mapUnknown(exception, context) {
|
|
306
|
+
const details = this.options.envelope.exposeInternals ? buildInternalDetails(exception) : void 0;
|
|
307
|
+
return this.toEnvelope(
|
|
308
|
+
INTERNAL_ERROR_STATUS,
|
|
309
|
+
BYMAX_INTERNAL_ERROR,
|
|
310
|
+
INTERNAL_ERROR_MESSAGE,
|
|
311
|
+
context,
|
|
312
|
+
details
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Assemble the envelope through the pure builder, threading the shared clock
|
|
317
|
+
* and omitting absent optional details.
|
|
318
|
+
*
|
|
319
|
+
* @param statusCode - HTTP status for the envelope.
|
|
320
|
+
* @param code - Stable machine-readable code.
|
|
321
|
+
* @param message - Human-readable, end-user-safe message.
|
|
322
|
+
* @param context - The neutral request context.
|
|
323
|
+
* @param details - Optional structured context; omitted when absent.
|
|
324
|
+
* @returns The formatted envelope.
|
|
325
|
+
*/
|
|
326
|
+
toEnvelope(statusCode, code, message, context, details) {
|
|
327
|
+
return buildErrorEnvelope({
|
|
328
|
+
statusCode,
|
|
329
|
+
code,
|
|
330
|
+
message,
|
|
331
|
+
path: context.path,
|
|
332
|
+
now: this.now,
|
|
333
|
+
...details !== void 0 ? { details } : {},
|
|
334
|
+
...context.correlationId !== void 0 ? { correlationId: context.correlationId } : {}
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Observability seam invoked for every unexpected (non-`HttpException`) error
|
|
339
|
+
* before it collapses to the generic 500. The base implementation is a no-op:
|
|
340
|
+
* this library owns no logger. An integration (for example
|
|
341
|
+
* `@bymax-one/nest-logger`) subclasses the filter and overrides this to record
|
|
342
|
+
* the original error with the current request context and correlation id.
|
|
343
|
+
* Overrides must never throw and must never write to the response.
|
|
344
|
+
*
|
|
345
|
+
* @param _error - The original thrown value.
|
|
346
|
+
* @param _context - The neutral request context.
|
|
347
|
+
*/
|
|
348
|
+
onUnexpectedError(_error, _context) {
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
BymaxExceptionFilter = __decorateClass([
|
|
352
|
+
Catch(),
|
|
353
|
+
__decorateParam(0, Inject(BYMAX_CORE_OPTIONS)),
|
|
354
|
+
__decorateParam(1, Optional()),
|
|
355
|
+
__decorateParam(1, Inject(BYMAX_CORRELATION_PROVIDER)),
|
|
356
|
+
__decorateParam(2, Inject(HttpAdapterHost))
|
|
357
|
+
], BymaxExceptionFilter);
|
|
358
|
+
|
|
359
|
+
// src/timing/request-info.accessor.ts
|
|
360
|
+
function stripQueryString(rawUrl) {
|
|
361
|
+
const queryIndex = rawUrl.indexOf("?");
|
|
362
|
+
return queryIndex === -1 ? rawUrl : rawUrl.slice(0, queryIndex);
|
|
363
|
+
}
|
|
364
|
+
function readExpressTemplate(request) {
|
|
365
|
+
const path = request.route?.path;
|
|
366
|
+
return path === void 0 ? void 0 : `${request.baseUrl ?? ""}${path}`;
|
|
367
|
+
}
|
|
368
|
+
function readFastifyTemplate(request) {
|
|
369
|
+
return request.routeOptions?.url;
|
|
370
|
+
}
|
|
371
|
+
function extractRequestInfo(context) {
|
|
372
|
+
const request = context.switchToHttp().getRequest();
|
|
373
|
+
const template = readExpressTemplate(request) ?? readFastifyTemplate(request);
|
|
374
|
+
const rawUrl = request.originalUrl ?? request.url ?? "";
|
|
375
|
+
return { method: request.method ?? "", route: template ?? stripQueryString(rawUrl) };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// src/timing/timing.interceptor.ts
|
|
379
|
+
var DEFAULT_SUCCESS_STATUS = 200;
|
|
380
|
+
var UNKNOWN_ERROR_STATUS = 500;
|
|
381
|
+
var TimingInterceptor = class {
|
|
382
|
+
/**
|
|
383
|
+
* @param options - Resolved core options; supplies `slowRequestThresholdMs`.
|
|
384
|
+
* @param sink - The bound timing sink; its `record` failures are swallowed.
|
|
385
|
+
* Injected with `@Optional()`: `BymaxCoreModule` binds no local default for
|
|
386
|
+
* this token on the sync path when the metrics bridge is not registered, so
|
|
387
|
+
* a consumer's own `BYMAX_TIMING_SINK` binding is not shadowed by one; when
|
|
388
|
+
* nothing resolves, this falls back to a no-op sink.
|
|
389
|
+
* @param clock - Monotonic clock seam; defaults to `performance.now()`, and
|
|
390
|
+
* is bound explicitly through {@link BYMAX_TIMING_CLOCK} so tests inject a
|
|
391
|
+
* stub advancing by controlled amounts.
|
|
392
|
+
*/
|
|
393
|
+
constructor(options, sink, clock = DEFAULT_MONOTONIC_CLOCK) {
|
|
394
|
+
this.options = options;
|
|
395
|
+
this.clock = clock;
|
|
396
|
+
this.sink = sink ?? new NoopTimingSink();
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Measure the handler chain and record exactly one sample per completed
|
|
400
|
+
* request, on the success path and on the error path alike.
|
|
401
|
+
*
|
|
402
|
+
* @param context - The execution context of the current request.
|
|
403
|
+
* @param next - The next handler in the chain.
|
|
404
|
+
* @returns The downstream response stream, unmodified beyond the measurement.
|
|
405
|
+
*/
|
|
406
|
+
intercept(context, next) {
|
|
407
|
+
if (context.getType() !== "http") {
|
|
408
|
+
return next.handle();
|
|
409
|
+
}
|
|
410
|
+
const start = this.clock.now();
|
|
411
|
+
const { method, route } = extractRequestInfo(context);
|
|
412
|
+
return next.handle().pipe(
|
|
413
|
+
tap({
|
|
414
|
+
complete: () => {
|
|
415
|
+
this.recordSample(method, route, this.readSuccessStatus(context), start);
|
|
416
|
+
}
|
|
417
|
+
}),
|
|
418
|
+
catchError((error) => {
|
|
419
|
+
this.recordSample(method, route, this.readErrorStatus(error), start);
|
|
420
|
+
return throwError(() => error);
|
|
421
|
+
})
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Read the final status code from the response object on the success path.
|
|
426
|
+
*
|
|
427
|
+
* @param context - The execution context of the current request.
|
|
428
|
+
* @returns The response's status code, or the default success status when absent.
|
|
429
|
+
*/
|
|
430
|
+
readSuccessStatus(context) {
|
|
431
|
+
const response = context.switchToHttp().getResponse();
|
|
432
|
+
return response.statusCode ?? DEFAULT_SUCCESS_STATUS;
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Derive the final status code for an error that escaped the handler.
|
|
436
|
+
*
|
|
437
|
+
* @param error - The error propagated by the handler chain.
|
|
438
|
+
* @returns The `HttpException` status, or the generic 500 for anything else.
|
|
439
|
+
*/
|
|
440
|
+
readErrorStatus(error) {
|
|
441
|
+
return error instanceof HttpException ? error.getStatus() : UNKNOWN_ERROR_STATUS;
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Build the sample, compute the slow flag, and deliver it to the sink inside
|
|
445
|
+
* a try/catch that silences any failure: a throwing sink must never affect
|
|
446
|
+
* the request it is observing.
|
|
447
|
+
*
|
|
448
|
+
* @param method - HTTP method of the request.
|
|
449
|
+
* @param route - Route template of the request.
|
|
450
|
+
* @param statusCode - Final status code, success or error.
|
|
451
|
+
* @param start - Monotonic start timestamp captured before the handler ran.
|
|
452
|
+
*/
|
|
453
|
+
recordSample(method, route, statusCode, start) {
|
|
454
|
+
const durationMs = this.clock.now() - start;
|
|
455
|
+
const threshold = this.options.timing.slowRequestThresholdMs;
|
|
456
|
+
const slow = threshold !== void 0 && durationMs > threshold;
|
|
457
|
+
const sample = { method, route, statusCode, durationMs, slow };
|
|
458
|
+
try {
|
|
459
|
+
this.sink.record(sample);
|
|
460
|
+
} catch {
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
TimingInterceptor = __decorateClass([
|
|
465
|
+
Injectable(),
|
|
466
|
+
__decorateParam(0, Inject(BYMAX_CORE_OPTIONS)),
|
|
467
|
+
__decorateParam(1, Optional()),
|
|
468
|
+
__decorateParam(1, Inject(BYMAX_TIMING_SINK)),
|
|
469
|
+
__decorateParam(2, Inject(BYMAX_TIMING_CLOCK))
|
|
470
|
+
], TimingInterceptor);
|
|
471
|
+
|
|
472
|
+
// src/passthrough.providers.ts
|
|
473
|
+
var PassThroughExceptionFilter = class {
|
|
474
|
+
constructor(adapterHost) {
|
|
475
|
+
this.adapterHost = adapterHost;
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Format the exception exactly as Nest's default handler would.
|
|
479
|
+
*
|
|
480
|
+
* @param exception - The exception that escaped the handler.
|
|
481
|
+
* @param host - The arguments host for the current request.
|
|
482
|
+
*/
|
|
483
|
+
catch(exception, host) {
|
|
484
|
+
this.delegate ??= new BaseExceptionFilter(this.adapterHost.httpAdapter);
|
|
485
|
+
this.delegate.catch(exception, host);
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
PassThroughExceptionFilter = __decorateClass([
|
|
489
|
+
Catch()
|
|
490
|
+
], PassThroughExceptionFilter);
|
|
491
|
+
var PassThroughInterceptor = class {
|
|
492
|
+
/**
|
|
493
|
+
* Forward the request to the next handler unchanged.
|
|
494
|
+
*
|
|
495
|
+
* @param _context - The execution context; unused by a transparent forwarder.
|
|
496
|
+
* @param next - The next handler in the chain.
|
|
497
|
+
* @returns The downstream response stream, unmodified.
|
|
498
|
+
*/
|
|
499
|
+
intercept(_context, next) {
|
|
500
|
+
return next.handle();
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
function assertAsyncFeatureEnabled(feature, enabled) {
|
|
504
|
+
if (!enabled) {
|
|
505
|
+
throw new Error(
|
|
506
|
+
`[BymaxCoreModule] The "${feature}" controller was reached while the feature is disabled. On the forRootAsync path this controller is always registered because options resolve after the module is defined; enable "${feature}" in the resolved options, or do not expose this controller while the feature is disabled.`
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
function selectAsyncExceptionFilter(options, correlation, adapterHost) {
|
|
511
|
+
return options.envelope.enabled ? new BymaxExceptionFilter(options, correlation, adapterHost) : new PassThroughExceptionFilter(adapterHost);
|
|
512
|
+
}
|
|
513
|
+
function selectAsyncTimingInterceptor(options, sink, clock) {
|
|
514
|
+
return options.timing.enabled ? new TimingInterceptor(options, sink, clock) : new PassThroughInterceptor();
|
|
515
|
+
}
|
|
516
|
+
var MAX_ERROR_MESSAGE_LENGTH = 300;
|
|
517
|
+
var TRUNCATION_ELLIPSIS = "...";
|
|
518
|
+
function summarizeRejection(reason) {
|
|
519
|
+
let message;
|
|
520
|
+
try {
|
|
521
|
+
message = reason instanceof Error ? reason.message : String(reason);
|
|
522
|
+
} catch {
|
|
523
|
+
message = "Unknown error";
|
|
524
|
+
}
|
|
525
|
+
if (message.length <= MAX_ERROR_MESSAGE_LENGTH) {
|
|
526
|
+
return message;
|
|
527
|
+
}
|
|
528
|
+
return `${message.slice(0, MAX_ERROR_MESSAGE_LENGTH - TRUNCATION_ELLIPSIS.length)}${TRUNCATION_ELLIPSIS}`;
|
|
529
|
+
}
|
|
530
|
+
async function runIndicator(indicator, timeoutMs) {
|
|
531
|
+
let timer;
|
|
532
|
+
const timedOut = new Promise((resolve) => {
|
|
533
|
+
timer = setTimeout(() => {
|
|
534
|
+
resolve({ name: indicator.name, status: "down", details: { timedOutAfterMs: timeoutMs } });
|
|
535
|
+
}, timeoutMs);
|
|
536
|
+
});
|
|
537
|
+
const checked = Promise.resolve().then(() => indicator.check()).then((result) => ({ ...result, name: indicator.name })).catch((reason) => ({
|
|
538
|
+
name: indicator.name,
|
|
539
|
+
status: "down",
|
|
540
|
+
details: { error: summarizeRejection(reason) }
|
|
541
|
+
}));
|
|
542
|
+
try {
|
|
543
|
+
return await Promise.race([checked, timedOut]);
|
|
544
|
+
} finally {
|
|
545
|
+
clearTimeout(timer);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
var HealthService = class {
|
|
549
|
+
/**
|
|
550
|
+
* @param indicators - Every registered indicator; empty when none resolve.
|
|
551
|
+
* Injected with `@Optional()`: `BymaxCoreModule` binds no local default for
|
|
552
|
+
* this token, so a consumer's own `BYMAX_HEALTH_INDICATORS` binding (from
|
|
553
|
+
* their own, globally-visible module) is not shadowed by one; when nothing
|
|
554
|
+
* is bound, this defaults to an empty array.
|
|
555
|
+
* @param options - Resolved core options; supplies `indicatorTimeoutMs`.
|
|
556
|
+
*/
|
|
557
|
+
constructor(indicators = [], options) {
|
|
558
|
+
this.indicators = indicators;
|
|
559
|
+
this.options = options;
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Liveness check: the process is up and able to respond. Runs no
|
|
563
|
+
* indicators, so it never depends on the health of anything else.
|
|
564
|
+
*
|
|
565
|
+
* @returns The documented liveness shape: `{ status: 'ok', checks: [] }`.
|
|
566
|
+
*/
|
|
567
|
+
checkLiveness() {
|
|
568
|
+
return { status: "ok", checks: [] };
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Readiness check: run every registered indicator concurrently and
|
|
572
|
+
* aggregate the results. `status` is `'ok'` only when every indicator
|
|
573
|
+
* reports `up`; an empty indicator list is vacuously `'ok'`.
|
|
574
|
+
*
|
|
575
|
+
* @returns The aggregated health response.
|
|
576
|
+
*/
|
|
577
|
+
async checkReadiness() {
|
|
578
|
+
const timeoutMs = this.options.health.indicatorTimeoutMs;
|
|
579
|
+
const checks = await Promise.all(
|
|
580
|
+
this.indicators.map((indicator) => runIndicator(indicator, timeoutMs))
|
|
581
|
+
);
|
|
582
|
+
const status = checks.every((check) => check.status === "up") ? "ok" : "error";
|
|
583
|
+
return { status, checks };
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
HealthService = __decorateClass([
|
|
587
|
+
Injectable(),
|
|
588
|
+
__decorateParam(0, Optional()),
|
|
589
|
+
__decorateParam(0, Inject(BYMAX_HEALTH_INDICATORS)),
|
|
590
|
+
__decorateParam(1, Inject(BYMAX_CORE_OPTIONS))
|
|
591
|
+
], HealthService);
|
|
592
|
+
|
|
593
|
+
// src/health/health.controller.ts
|
|
594
|
+
function assertControllerMatchesOptions(options, registeredPath) {
|
|
595
|
+
assertAsyncFeatureEnabled("health", options.health.enabled);
|
|
596
|
+
if (options.health.path !== registeredPath) {
|
|
597
|
+
throw new Error(
|
|
598
|
+
`[BymaxCoreModule] The "health" controller is registered at "${registeredPath}" but the resolved options request "${options.health.path}". Route metadata is fixed before forRootAsync's options resolve, so a custom "health.path" is only honored through forRoot(); register synchronously, or keep the default "${DEFAULT_HEALTH_PATH}" prefix on the async path.`
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
function createHealthController(registeredPath) {
|
|
603
|
+
let HealthController = class {
|
|
604
|
+
/**
|
|
605
|
+
* @param healthService - The readiness and liveness aggregator.
|
|
606
|
+
* @param options - Resolved core options, used to guard consistency at request time.
|
|
607
|
+
* @param adapterHost - The live HTTP adapter, used to reply with a dynamic status.
|
|
608
|
+
*/
|
|
609
|
+
constructor(healthService, options, adapterHost) {
|
|
610
|
+
this.healthService = healthService;
|
|
611
|
+
this.options = options;
|
|
612
|
+
this.adapterHost = adapterHost;
|
|
613
|
+
}
|
|
614
|
+
live() {
|
|
615
|
+
assertControllerMatchesOptions(this.options, registeredPath);
|
|
616
|
+
return this.healthService.checkLiveness();
|
|
617
|
+
}
|
|
618
|
+
async ready(response) {
|
|
619
|
+
assertControllerMatchesOptions(this.options, registeredPath);
|
|
620
|
+
const result = await this.healthService.checkReadiness();
|
|
621
|
+
const status = result.status === "ok" ? HttpStatus.OK : HttpStatus.SERVICE_UNAVAILABLE;
|
|
622
|
+
this.adapterHost.httpAdapter.reply(response, result, status);
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
__decorateClass([
|
|
626
|
+
Get("live")
|
|
627
|
+
], HealthController.prototype, "live", 1);
|
|
628
|
+
__decorateClass([
|
|
629
|
+
Get("ready"),
|
|
630
|
+
__decorateParam(0, Res())
|
|
631
|
+
], HealthController.prototype, "ready", 1);
|
|
632
|
+
HealthController = __decorateClass([
|
|
633
|
+
Controller(registeredPath),
|
|
634
|
+
__decorateParam(0, Inject(HealthService)),
|
|
635
|
+
__decorateParam(1, Inject(BYMAX_CORE_OPTIONS)),
|
|
636
|
+
__decorateParam(2, Inject(HttpAdapterHost))
|
|
637
|
+
], HealthController);
|
|
638
|
+
return HealthController;
|
|
639
|
+
}
|
|
640
|
+
function assertControllerMatchesOptions2(options, registeredPath) {
|
|
641
|
+
assertAsyncFeatureEnabled("metrics", options.metrics.enabled);
|
|
642
|
+
if (options.metrics.path !== registeredPath) {
|
|
643
|
+
throw new Error(
|
|
644
|
+
`[BymaxCoreModule] The "metrics" controller is registered at "${registeredPath}" but the resolved options request "${options.metrics.path}". Route metadata is fixed before forRootAsync's options resolve, so a custom "metrics.path" is only honored through forRoot(); register synchronously, or keep the default "metrics" route on the async path.`
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
function createMetricsController(registeredPath) {
|
|
649
|
+
let MetricsController = class {
|
|
650
|
+
/**
|
|
651
|
+
* @param registry - The dedicated `prom-client` registry to scrape.
|
|
652
|
+
* @param options - Resolved core options, used to guard consistency at request time.
|
|
653
|
+
* @param adapterHost - The live HTTP adapter, used to reply with the correct content type.
|
|
654
|
+
*/
|
|
655
|
+
constructor(registry, options, adapterHost) {
|
|
656
|
+
this.registry = registry;
|
|
657
|
+
this.options = options;
|
|
658
|
+
this.adapterHost = adapterHost;
|
|
659
|
+
}
|
|
660
|
+
async scrape(response) {
|
|
661
|
+
assertControllerMatchesOptions2(this.options, registeredPath);
|
|
662
|
+
const body = await this.registry.metrics();
|
|
663
|
+
this.adapterHost.httpAdapter.setHeader(response, "Content-Type", this.registry.contentType);
|
|
664
|
+
this.adapterHost.httpAdapter.reply(response, body, HttpStatus.OK);
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
__decorateClass([
|
|
668
|
+
Get(),
|
|
669
|
+
__decorateParam(0, Res())
|
|
670
|
+
], MetricsController.prototype, "scrape", 1);
|
|
671
|
+
MetricsController = __decorateClass([
|
|
672
|
+
Controller(registeredPath),
|
|
673
|
+
__decorateParam(0, Inject(BYMAX_METRICS_REGISTRY)),
|
|
674
|
+
__decorateParam(1, Inject(BYMAX_CORE_OPTIONS)),
|
|
675
|
+
__decorateParam(2, Inject(HttpAdapterHost))
|
|
676
|
+
], MetricsController);
|
|
677
|
+
return MetricsController;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// src/metrics/metrics.registry.ts
|
|
681
|
+
var MISSING_PEER_MESSAGE = "metrics.enabled is true but the optional peer prom-client is not installed. Run: pnpm add prom-client";
|
|
682
|
+
function isMissingModuleError(cause) {
|
|
683
|
+
const code = cause.code;
|
|
684
|
+
return code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND";
|
|
685
|
+
}
|
|
686
|
+
async function loadPromClient() {
|
|
687
|
+
try {
|
|
688
|
+
return await import('prom-client');
|
|
689
|
+
} catch (cause) {
|
|
690
|
+
if (isMissingModuleError(cause)) {
|
|
691
|
+
throw new Error(MISSING_PEER_MESSAGE, { cause });
|
|
692
|
+
}
|
|
693
|
+
throw cause;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
async function createMetricsRegistry(options) {
|
|
697
|
+
const promClient = await loadPromClient();
|
|
698
|
+
const registry = new promClient.Registry();
|
|
699
|
+
registry.setDefaultLabels(options.metrics.defaultLabels);
|
|
700
|
+
if (options.metrics.collectDefaultMetrics) {
|
|
701
|
+
promClient.collectDefaultMetrics({ register: registry });
|
|
702
|
+
}
|
|
703
|
+
return registry;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// src/metrics/timing-metrics.sink.ts
|
|
707
|
+
var REQUESTS_TOTAL = "http_requests_total";
|
|
708
|
+
var REQUEST_DURATION_SECONDS = "http_request_duration_seconds";
|
|
709
|
+
var HTTP_METRIC_LABELS = ["method", "route", "status_code"];
|
|
710
|
+
var MILLISECONDS_PER_SECOND = 1e3;
|
|
711
|
+
function getOrCreateCounter(promClient, registry) {
|
|
712
|
+
const existing = registry.getSingleMetric(REQUESTS_TOTAL);
|
|
713
|
+
if (existing !== void 0) {
|
|
714
|
+
if (!(existing instanceof promClient.Counter)) {
|
|
715
|
+
throw new Error(
|
|
716
|
+
`A metric named "${REQUESTS_TOTAL}" is already registered on the metrics registry with a different type; the HTTP metrics bridge requires it to be a Counter.`
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
return existing;
|
|
720
|
+
}
|
|
721
|
+
return new promClient.Counter({
|
|
722
|
+
name: REQUESTS_TOTAL,
|
|
723
|
+
help: "Total number of completed HTTP requests, labeled by method, route, and status_code.",
|
|
724
|
+
labelNames: [...HTTP_METRIC_LABELS],
|
|
725
|
+
registers: [registry]
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
function getOrCreateHistogram(promClient, registry) {
|
|
729
|
+
const existing = registry.getSingleMetric(REQUEST_DURATION_SECONDS);
|
|
730
|
+
if (existing !== void 0) {
|
|
731
|
+
if (!(existing instanceof promClient.Histogram)) {
|
|
732
|
+
throw new Error(
|
|
733
|
+
`A metric named "${REQUEST_DURATION_SECONDS}" is already registered on the metrics registry with a different type; the HTTP metrics bridge requires it to be a Histogram.`
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
return existing;
|
|
737
|
+
}
|
|
738
|
+
return new promClient.Histogram({
|
|
739
|
+
name: REQUEST_DURATION_SECONDS,
|
|
740
|
+
help: "HTTP request duration in seconds, labeled by method, route, and status_code.",
|
|
741
|
+
labelNames: [...HTTP_METRIC_LABELS],
|
|
742
|
+
registers: [registry]
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
var TimingMetricsSink = class {
|
|
746
|
+
/**
|
|
747
|
+
* @param registry - The dedicated metrics registry the samples feed.
|
|
748
|
+
* @param promClient - The lazily loaded `prom-client` module supplying the
|
|
749
|
+
* `Counter` and `Histogram` constructors, passed in so this bridge never
|
|
750
|
+
* imports the optional peer at the top level.
|
|
751
|
+
*/
|
|
752
|
+
constructor(registry, promClient) {
|
|
753
|
+
this.counter = getOrCreateCounter(promClient, registry);
|
|
754
|
+
this.histogram = getOrCreateHistogram(promClient, registry);
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* Record one completed request: increment the counter once and observe the
|
|
758
|
+
* duration in seconds, both under the bounded label set. Any failure is
|
|
759
|
+
* swallowed so a metrics backend problem can never break the request being
|
|
760
|
+
* observed.
|
|
761
|
+
*
|
|
762
|
+
* @param sample - The timing sample for a completed request.
|
|
763
|
+
*/
|
|
764
|
+
record(sample) {
|
|
765
|
+
const labels = {
|
|
766
|
+
method: sample.method,
|
|
767
|
+
route: sample.route,
|
|
768
|
+
status_code: String(sample.statusCode)
|
|
769
|
+
};
|
|
770
|
+
try {
|
|
771
|
+
this.counter.inc(labels);
|
|
772
|
+
this.histogram.observe(labels, sample.durationMs / MILLISECONDS_PER_SECOND);
|
|
773
|
+
} catch {
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
};
|
|
777
|
+
|
|
778
|
+
// src/metrics/metrics.providers.ts
|
|
779
|
+
function createDisabledRegistryPlaceholder() {
|
|
780
|
+
const throwDisabled = () => {
|
|
781
|
+
throw new Error(
|
|
782
|
+
'[BymaxCoreModule] The metrics registry was accessed while metrics are disabled. Enable "metrics" in the resolved options before injecting the registry.'
|
|
783
|
+
);
|
|
784
|
+
};
|
|
785
|
+
return { metrics: throwDisabled };
|
|
786
|
+
}
|
|
787
|
+
async function resolveMetricsRegistry(options) {
|
|
788
|
+
if (!options.metrics.enabled) {
|
|
789
|
+
return createDisabledRegistryPlaceholder();
|
|
790
|
+
}
|
|
791
|
+
return createMetricsRegistry(options);
|
|
792
|
+
}
|
|
793
|
+
async function resolveTimingSink(options, registry) {
|
|
794
|
+
if (options.metrics.enabled && options.timing.enabled) {
|
|
795
|
+
return new TimingMetricsSink(registry, await loadPromClient());
|
|
796
|
+
}
|
|
797
|
+
return new NoopTimingSink();
|
|
798
|
+
}
|
|
799
|
+
function buildMetricsRegistryProvider() {
|
|
800
|
+
return {
|
|
801
|
+
provide: BYMAX_METRICS_REGISTRY,
|
|
802
|
+
useFactory: (options) => resolveMetricsRegistry(options),
|
|
803
|
+
inject: [BYMAX_CORE_OPTIONS]
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
function buildMetricsTimingSinkProvider() {
|
|
807
|
+
return {
|
|
808
|
+
provide: BYMAX_TIMING_SINK,
|
|
809
|
+
useFactory: (options, registry) => resolveTimingSink(options, registry),
|
|
810
|
+
inject: [BYMAX_CORE_OPTIONS, BYMAX_METRICS_REGISTRY]
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// src/core.module.ts
|
|
815
|
+
var {
|
|
816
|
+
ConfigurableModuleClass: BymaxCoreModuleBase,
|
|
817
|
+
MODULE_OPTIONS_TOKEN: BUILDER_OPTIONS_TOKEN,
|
|
818
|
+
OPTIONS_TYPE,
|
|
819
|
+
ASYNC_OPTIONS_TYPE
|
|
820
|
+
} = new ConfigurableModuleBuilder().setClassMethodName("forRoot").setExtras({ isGlobal: true }, (definition, extras) => ({
|
|
821
|
+
...definition,
|
|
822
|
+
// `setExtras` merges the `{ isGlobal: true }` default first, so `isGlobal`
|
|
823
|
+
// is always defined; `!== false` keeps "global unless explicitly disabled".
|
|
824
|
+
global: extras.isGlobal !== false
|
|
825
|
+
})).build();
|
|
826
|
+
function buildSyncProviders(resolved) {
|
|
827
|
+
const providers = [];
|
|
828
|
+
if (resolved.envelope.enabled) {
|
|
829
|
+
providers.push({ provide: APP_FILTER, useClass: BymaxExceptionFilter });
|
|
830
|
+
}
|
|
831
|
+
if (resolved.timing.enabled) {
|
|
832
|
+
providers.push({ provide: APP_INTERCEPTOR, useClass: TimingInterceptor });
|
|
833
|
+
}
|
|
834
|
+
if (resolved.health.enabled) {
|
|
835
|
+
providers.push(HealthService);
|
|
836
|
+
}
|
|
837
|
+
if (resolved.metrics.enabled) {
|
|
838
|
+
providers.push(buildMetricsRegistryProvider());
|
|
839
|
+
if (resolved.timing.enabled) {
|
|
840
|
+
providers.push(buildMetricsTimingSinkProvider());
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
return providers;
|
|
844
|
+
}
|
|
845
|
+
function buildControllers(resolved) {
|
|
846
|
+
const controllers = [];
|
|
847
|
+
if (resolved.health.enabled) {
|
|
848
|
+
controllers.push(createHealthController(resolved.health.path));
|
|
849
|
+
}
|
|
850
|
+
if (resolved.metrics.enabled) {
|
|
851
|
+
controllers.push(createMetricsController(resolved.metrics.path));
|
|
852
|
+
}
|
|
853
|
+
return controllers;
|
|
854
|
+
}
|
|
855
|
+
function buildAsyncSlots() {
|
|
856
|
+
return [
|
|
857
|
+
{
|
|
858
|
+
provide: APP_FILTER,
|
|
859
|
+
useFactory: (options, correlation, adapterHost) => selectAsyncExceptionFilter(options, correlation, adapterHost),
|
|
860
|
+
inject: [
|
|
861
|
+
BYMAX_CORE_OPTIONS,
|
|
862
|
+
{ token: BYMAX_CORRELATION_PROVIDER, optional: true },
|
|
863
|
+
HttpAdapterHost
|
|
864
|
+
]
|
|
865
|
+
},
|
|
866
|
+
{
|
|
867
|
+
provide: APP_INTERCEPTOR,
|
|
868
|
+
useFactory: (options, sink, clock) => selectAsyncTimingInterceptor(options, sink, clock),
|
|
869
|
+
inject: [BYMAX_CORE_OPTIONS, BYMAX_TIMING_SINK, BYMAX_TIMING_CLOCK]
|
|
870
|
+
}
|
|
871
|
+
];
|
|
872
|
+
}
|
|
873
|
+
function augmentModule(base, providers, controllers, exportTokens = [BYMAX_CORE_OPTIONS]) {
|
|
874
|
+
return {
|
|
875
|
+
...base,
|
|
876
|
+
module: BymaxCoreModule,
|
|
877
|
+
providers: [...base.providers ?? [], ...providers],
|
|
878
|
+
controllers: [...base.controllers ?? [], ...controllers],
|
|
879
|
+
exports: [...base.exports ?? [], ...exportTokens]
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
var BymaxCoreModule = class extends BymaxCoreModuleBase {
|
|
883
|
+
/**
|
|
884
|
+
* Register the module synchronously. Options are known now, so disabled
|
|
885
|
+
* features are omitted from the providers and controllers arrays and the
|
|
886
|
+
* resolved snapshot is provided under {@link BYMAX_CORE_OPTIONS}.
|
|
887
|
+
*
|
|
888
|
+
* @param options - Core options plus the optional `isGlobal` extra. Omit for
|
|
889
|
+
* all documented defaults.
|
|
890
|
+
* @returns The configured `DynamicModule`.
|
|
891
|
+
* @example
|
|
892
|
+
* BymaxCoreModule.forRoot({ metrics: { enabled: true } })
|
|
893
|
+
*/
|
|
894
|
+
static forRoot(options = {}) {
|
|
895
|
+
const resolved = normalizeCoreOptions(options);
|
|
896
|
+
const providers = [
|
|
897
|
+
{ provide: BYMAX_CORE_OPTIONS, useValue: resolved },
|
|
898
|
+
...buildDefaultProviders(),
|
|
899
|
+
...buildSyncProviders(resolved)
|
|
900
|
+
];
|
|
901
|
+
const exportTokens = [BYMAX_CORE_OPTIONS];
|
|
902
|
+
if (resolved.metrics.enabled) {
|
|
903
|
+
exportTokens.push(BYMAX_METRICS_REGISTRY);
|
|
904
|
+
if (resolved.timing.enabled) {
|
|
905
|
+
exportTokens.push(BYMAX_TIMING_SINK);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
return augmentModule(
|
|
909
|
+
super.forRoot(options),
|
|
910
|
+
providers,
|
|
911
|
+
buildControllers(resolved),
|
|
912
|
+
exportTokens
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* Register the module asynchronously. The resolved options are produced by
|
|
917
|
+
* the consumer's factory and normalized under {@link BYMAX_CORE_OPTIONS}.
|
|
918
|
+
* Because those options are unknown when the module is defined, the pipeline
|
|
919
|
+
* slots register unconditionally and gate at runtime with transparent
|
|
920
|
+
* pass-throughs. The health controller cannot register conditionally either,
|
|
921
|
+
* since its route metadata is fixed before the async options resolve: it is
|
|
922
|
+
* always registered at the default health path, and its handlers guard
|
|
923
|
+
* every request against the resolved options being disabled or requesting a
|
|
924
|
+
* different path, throwing a descriptive configuration error in either case.
|
|
925
|
+
* The metrics controller follows the same mechanism at the default metrics
|
|
926
|
+
* path; the `BYMAX_METRICS_REGISTRY` factory gates on the resolved options and
|
|
927
|
+
* resolves to a guarded placeholder when metrics are disabled, so the optional
|
|
928
|
+
* peer `prom-client` is never loaded unless metrics are actually enabled.
|
|
929
|
+
*
|
|
930
|
+
* @param options - Async options (factory + inject + imports, or class).
|
|
931
|
+
* @returns The configured `DynamicModule`.
|
|
932
|
+
* @example
|
|
933
|
+
* BymaxCoreModule.forRootAsync({ inject: [Config], useFactory: (c) => ({ ... }) })
|
|
934
|
+
*/
|
|
935
|
+
static forRootAsync(options) {
|
|
936
|
+
const providers = [
|
|
937
|
+
{
|
|
938
|
+
provide: BYMAX_CORE_OPTIONS,
|
|
939
|
+
useFactory: (raw) => normalizeCoreOptions(raw),
|
|
940
|
+
inject: [BUILDER_OPTIONS_TOKEN]
|
|
941
|
+
},
|
|
942
|
+
...buildDefaultProviders(),
|
|
943
|
+
...buildAsyncSlots(),
|
|
944
|
+
HealthService,
|
|
945
|
+
buildMetricsRegistryProvider(),
|
|
946
|
+
buildMetricsTimingSinkProvider()
|
|
947
|
+
];
|
|
948
|
+
return augmentModule(
|
|
949
|
+
super.forRootAsync(options),
|
|
950
|
+
providers,
|
|
951
|
+
[createHealthController(DEFAULT_HEALTH_PATH), createMetricsController(DEFAULT_METRICS_PATH)],
|
|
952
|
+
[BYMAX_CORE_OPTIONS, BYMAX_TIMING_SINK, BYMAX_METRICS_REGISTRY]
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
};
|
|
956
|
+
BymaxCoreModule = __decorateClass([
|
|
957
|
+
Module({})
|
|
958
|
+
], BymaxCoreModule);
|
|
959
|
+
|
|
960
|
+
export { BYMAX_BAD_GATEWAY, BYMAX_BAD_REQUEST, BYMAX_CLIENT_ERROR, BYMAX_CONFLICT, BYMAX_CORE_OPTIONS, BYMAX_CORRELATION_PROVIDER, BYMAX_FORBIDDEN, BYMAX_GATEWAY_TIMEOUT, BYMAX_HEALTH_INDICATORS, BYMAX_INTERNAL_ERROR, BYMAX_METRICS_REGISTRY, BYMAX_NOT_FOUND, BYMAX_NOT_IMPLEMENTED, BYMAX_PAYLOAD_TOO_LARGE, BYMAX_SERVICE_UNAVAILABLE, BYMAX_TIMING_SINK, BYMAX_TOO_MANY_REQUESTS, BYMAX_UNAUTHORIZED, BYMAX_UNPROCESSABLE_ENTITY, BYMAX_UNSUPPORTED_MEDIA_TYPE, BYMAX_VALIDATION_FAILED, BymaxCoreModule, BymaxExceptionFilter, TimingInterceptor, buildErrorEnvelope, codeForStatus };
|