@bymax-one/nest-core 1.0.1 → 1.1.1

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.
@@ -0,0 +1,266 @@
1
+ import { Logger } from '@nestjs/common';
2
+
3
+ // src/openapi/openapi.bootstrap.ts
4
+
5
+ // src/core.tokens.ts
6
+ var BYMAX_CORE_OPTIONS = /* @__PURE__ */ Symbol("BYMAX_CORE_OPTIONS");
7
+
8
+ // src/runtime.environment.ts
9
+ var NON_PRODUCTION_ENVIRONMENTS = /* @__PURE__ */ new Set(["development", "test"]);
10
+ function isProductionRuntime(value = process.env["NODE_ENV"]) {
11
+ if (value === void 0) {
12
+ return true;
13
+ }
14
+ return !NON_PRODUCTION_ENVIRONMENTS.has(value.trim().toLowerCase());
15
+ }
16
+
17
+ // src/envelope/error-codes.ts
18
+ var BYMAX_BAD_REQUEST = "BYMAX_BAD_REQUEST";
19
+ var BYMAX_VALIDATION_FAILED = "BYMAX_VALIDATION_FAILED";
20
+ var BYMAX_UNAUTHORIZED = "BYMAX_UNAUTHORIZED";
21
+ var BYMAX_FORBIDDEN = "BYMAX_FORBIDDEN";
22
+ var BYMAX_NOT_FOUND = "BYMAX_NOT_FOUND";
23
+ var BYMAX_CONFLICT = "BYMAX_CONFLICT";
24
+ var BYMAX_PAYLOAD_TOO_LARGE = "BYMAX_PAYLOAD_TOO_LARGE";
25
+ var BYMAX_UNSUPPORTED_MEDIA_TYPE = "BYMAX_UNSUPPORTED_MEDIA_TYPE";
26
+ var BYMAX_UNPROCESSABLE_ENTITY = "BYMAX_UNPROCESSABLE_ENTITY";
27
+ var BYMAX_TOO_MANY_REQUESTS = "BYMAX_TOO_MANY_REQUESTS";
28
+ var BYMAX_CLIENT_ERROR = "BYMAX_CLIENT_ERROR";
29
+ var BYMAX_INTERNAL_ERROR = "BYMAX_INTERNAL_ERROR";
30
+ var BYMAX_NOT_IMPLEMENTED = "BYMAX_NOT_IMPLEMENTED";
31
+ var BYMAX_BAD_GATEWAY = "BYMAX_BAD_GATEWAY";
32
+ var BYMAX_SERVICE_UNAVAILABLE = "BYMAX_SERVICE_UNAVAILABLE";
33
+ var BYMAX_GATEWAY_TIMEOUT = "BYMAX_GATEWAY_TIMEOUT";
34
+
35
+ // src/openapi/openapi.schemas.ts
36
+ var ERROR_CODES = [
37
+ BYMAX_BAD_REQUEST,
38
+ BYMAX_VALIDATION_FAILED,
39
+ BYMAX_UNAUTHORIZED,
40
+ BYMAX_FORBIDDEN,
41
+ BYMAX_NOT_FOUND,
42
+ BYMAX_CONFLICT,
43
+ BYMAX_PAYLOAD_TOO_LARGE,
44
+ BYMAX_UNSUPPORTED_MEDIA_TYPE,
45
+ BYMAX_UNPROCESSABLE_ENTITY,
46
+ BYMAX_TOO_MANY_REQUESTS,
47
+ BYMAX_CLIENT_ERROR,
48
+ BYMAX_INTERNAL_ERROR,
49
+ BYMAX_NOT_IMPLEMENTED,
50
+ BYMAX_BAD_GATEWAY,
51
+ BYMAX_SERVICE_UNAVAILABLE,
52
+ BYMAX_GATEWAY_TIMEOUT
53
+ ];
54
+ var CORE_SCHEMAS = {
55
+ BymaxErrorCode: {
56
+ type: "string",
57
+ enum: ERROR_CODES,
58
+ description: "Stable machine-readable error codes emitted by this package. A domain error may pass through its own code, so a response is not restricted to this catalogue."
59
+ },
60
+ BymaxErrorDetails: {
61
+ description: "Structured error context. The array form carries one entry per validation violation; the object form carries the development-only internals dump.",
62
+ oneOf: [
63
+ { type: "array", items: {} },
64
+ { type: "object", additionalProperties: true }
65
+ ]
66
+ },
67
+ BymaxErrorEnvelope: {
68
+ type: "object",
69
+ description: "The shape of every error response served by this application.",
70
+ required: ["statusCode", "code", "message", "timestamp", "path"],
71
+ properties: {
72
+ statusCode: { type: "integer", example: 404 },
73
+ code: {
74
+ type: "string",
75
+ example: BYMAX_NOT_FOUND,
76
+ description: "A code from BymaxErrorCode, or a domain code passed through unchanged."
77
+ },
78
+ message: { type: "string", description: "Human-readable and safe to show end users." },
79
+ details: { $ref: "#/components/schemas/BymaxErrorDetails" },
80
+ correlationId: {
81
+ type: "string",
82
+ description: "Present only when a correlation provider resolves an id."
83
+ },
84
+ timestamp: { type: "string", format: "date-time" },
85
+ path: { type: "string", example: "/invoices/42" }
86
+ }
87
+ },
88
+ BymaxHealthCheckEntry: {
89
+ type: "object",
90
+ description: "One indicator's result within a readiness response.",
91
+ required: ["name", "status"],
92
+ properties: {
93
+ name: { type: "string", example: "redis" },
94
+ status: { type: "string", enum: ["up", "down"] },
95
+ details: {
96
+ type: "object",
97
+ additionalProperties: true,
98
+ description: "Safe diagnostic context. Never carries secrets or connection strings."
99
+ }
100
+ }
101
+ },
102
+ BymaxHealthResponse: {
103
+ type: "object",
104
+ description: "The body served by the liveness and readiness endpoints.",
105
+ required: ["status", "checks"],
106
+ properties: {
107
+ status: {
108
+ type: "string",
109
+ enum: ["ok", "error"],
110
+ description: "'ok' only when every check is up. Liveness is always 'ok'."
111
+ },
112
+ checks: {
113
+ type: "array",
114
+ items: { $ref: "#/components/schemas/BymaxHealthCheckEntry" },
115
+ description: "Empty for the liveness endpoint."
116
+ }
117
+ }
118
+ },
119
+ BymaxPageMeta: {
120
+ type: "object",
121
+ description: "Offset-pagination metadata.",
122
+ required: ["page", "limit", "totalItems", "totalPages"],
123
+ properties: {
124
+ page: { type: "integer", minimum: 1, example: 1 },
125
+ limit: { type: "integer", minimum: 1, example: 20 },
126
+ totalItems: { type: "integer", minimum: 0, example: 137 },
127
+ totalPages: { type: "integer", minimum: 0, example: 7 }
128
+ }
129
+ },
130
+ BymaxPageResult: {
131
+ type: "object",
132
+ description: "An offset-paginated page. Compose it with a concrete item schema by overriding `items`.",
133
+ required: ["items", "meta"],
134
+ properties: {
135
+ items: { type: "array", items: {} },
136
+ meta: { $ref: "#/components/schemas/BymaxPageMeta" }
137
+ }
138
+ },
139
+ BymaxCursorResult: {
140
+ type: "object",
141
+ description: "A cursor-paginated page. Compose it with a concrete item schema by overriding `items`.",
142
+ required: ["items", "nextCursor"],
143
+ properties: {
144
+ items: { type: "array", items: {} },
145
+ nextCursor: {
146
+ type: "string",
147
+ nullable: true,
148
+ description: "Opaque cursor for the next page, or null when the last page was reached."
149
+ }
150
+ }
151
+ }
152
+ };
153
+ var CORE_PARAMETERS = {
154
+ BymaxPageQueryPage: {
155
+ name: "page",
156
+ in: "query",
157
+ required: false,
158
+ description: "1-based page number. Out-of-range and non-numeric input is clamped.",
159
+ schema: { type: "integer", minimum: 1, default: 1 }
160
+ },
161
+ BymaxPageQueryLimit: {
162
+ name: "limit",
163
+ in: "query",
164
+ required: false,
165
+ description: "Items per page. Clamped to the range this application configures.",
166
+ schema: { type: "integer", minimum: 1, default: 20 }
167
+ },
168
+ BymaxCursorQueryCursor: {
169
+ name: "cursor",
170
+ in: "query",
171
+ required: false,
172
+ description: "Opaque cursor from a previous response. Omit to request the first page.",
173
+ schema: { type: "string" }
174
+ }
175
+ };
176
+
177
+ // src/openapi/openapi.document.ts
178
+ function asRecord(value) {
179
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
180
+ return {};
181
+ }
182
+ return value;
183
+ }
184
+ function mergeAbsent(existing, additions) {
185
+ return { ...additions, ...existing };
186
+ }
187
+ function augmentDocument(document, options) {
188
+ const components = asRecord(document.components);
189
+ const merged = { ...components };
190
+ if (options.includeCoreSchemas) {
191
+ merged["schemas"] = mergeAbsent(asRecord(components["schemas"]), CORE_SCHEMAS);
192
+ merged["parameters"] = mergeAbsent(asRecord(components["parameters"]), CORE_PARAMETERS);
193
+ }
194
+ const securitySchemeNames = Object.keys(options.securitySchemes);
195
+ if (securitySchemeNames.length > 0) {
196
+ merged["securitySchemes"] = mergeAbsent(
197
+ asRecord(components["securitySchemes"]),
198
+ options.securitySchemes
199
+ );
200
+ }
201
+ return { ...document, components: merged };
202
+ }
203
+
204
+ // src/optional-peer.ts
205
+ function isMissingModuleError(cause) {
206
+ const code = cause.code;
207
+ return code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND";
208
+ }
209
+ function missingPeerMessage(option, peer) {
210
+ return `${option} is true but the optional peer ${peer} is not installed. Run: pnpm add ${peer}`;
211
+ }
212
+
213
+ // src/openapi/openapi.loader.ts
214
+ var MISSING_PEER_MESSAGE = missingPeerMessage("openapi.enabled", "@nestjs/swagger");
215
+ async function loadSwagger() {
216
+ try {
217
+ return await import('@nestjs/swagger');
218
+ } catch (cause) {
219
+ if (isMissingModuleError(cause)) {
220
+ throw new Error(MISSING_PEER_MESSAGE, { cause });
221
+ }
222
+ throw cause;
223
+ }
224
+ }
225
+
226
+ // src/openapi/openapi.bootstrap.ts
227
+ var OPTIONS_UNRESOLVED_MESSAGE = "[BymaxCoreModule] applyBymaxOpenApi could not resolve BYMAX_CORE_OPTIONS from the application. Register BymaxCoreModule (forRoot or forRootAsync) before calling it, and keep the module global or import it into the module you bootstrap.";
228
+ function resolveCoreOptions(app) {
229
+ try {
230
+ return app.get(BYMAX_CORE_OPTIONS);
231
+ } catch (cause) {
232
+ throw new Error(OPTIONS_UNRESOLVED_MESSAGE, { cause });
233
+ }
234
+ }
235
+ function buildConfig(builder, options) {
236
+ builder.setTitle(options.title).setDescription(options.description).setVersion(options.version);
237
+ for (const server of options.servers) {
238
+ builder.addServer(server.url, server.description);
239
+ }
240
+ return builder.build();
241
+ }
242
+ async function applyBymaxOpenApi(app) {
243
+ const logger = new Logger("BymaxCoreModule");
244
+ const options = resolveCoreOptions(app).openapi;
245
+ if (isProductionRuntime()) {
246
+ if (options.suppressedInProduction || options.enabled) {
247
+ logger.warn(
248
+ 'openapi.enabled was requested but the OpenAPI document is never served in production. Set NODE_ENV to "development" or "test" to serve it.'
249
+ );
250
+ }
251
+ return { mounted: false, reason: "production" };
252
+ }
253
+ if (!options.enabled) {
254
+ return { mounted: false, reason: "disabled" };
255
+ }
256
+ const swagger = await loadSwagger();
257
+ const config = buildConfig(new swagger.DocumentBuilder(), options);
258
+ const document = augmentDocument(swagger.SwaggerModule.createDocument(app, config), options);
259
+ swagger.SwaggerModule.setup(options.path, app, document, {
260
+ jsonDocumentUrl: options.jsonPath
261
+ });
262
+ logger.log(`OpenAPI document served at "/${options.path}" (JSON at "/${options.jsonPath}")`);
263
+ return { mounted: true, path: options.path };
264
+ }
265
+
266
+ export { applyBymaxOpenApi };
@@ -57,6 +57,7 @@ function isOrderingKeyRecord(value) {
57
57
  return false;
58
58
  }
59
59
  return Object.values(value).every(
60
+ // Stryker disable next-line ConditionalExpression: equivalent — replacing the `typeof entry === 'number'` guard with `true` leaves `Number.isFinite(entry)`, which never coerces and so already returns `false` for every non-number. The guard is redundant and the predicate is unchanged for every value.
60
61
  (entry) => typeof entry === "string" || typeof entry === "number" && Number.isFinite(entry)
61
62
  );
62
63
  }
@@ -55,6 +55,7 @@ function isOrderingKeyRecord(value) {
55
55
  return false;
56
56
  }
57
57
  return Object.values(value).every(
58
+ // Stryker disable next-line ConditionalExpression: equivalent — replacing the `typeof entry === 'number'` guard with `true` leaves `Number.isFinite(entry)`, which never coerces and so already returns `false` for every non-number. The guard is redundant and the predicate is unchanged for every value.
58
59
  (entry) => typeof entry === "string" || typeof entry === "number" && Number.isFinite(entry)
59
60
  );
60
61
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bymax-one/nest-core",
3
- "version": "1.0.1",
4
- "description": "Zero-dependency NestJS 11 application foundation kit: error-envelope exception filter, request-timing interceptor, pagination helpers, health endpoints, and an optional Prometheus metrics endpoint.",
3
+ "version": "1.1.1",
4
+ "description": "Zero-dependency NestJS 11 application foundation kit: error-envelope exception filter, request-timing interceptor, pagination helpers, health endpoints with indicator discovery, an optional Prometheus metrics endpoint with a contribution contract, OpenAPI documents in development, and OpenTelemetry trace correlation.",
5
5
  "author": "Bymax One <support@bymax.one>",
6
6
  "license": "MIT",
7
7
  "homepage": "https://github.com/bymaxone/nest-core#readme",
@@ -53,11 +53,32 @@
53
53
  "default": "./dist/health/index.cjs"
54
54
  }
55
55
  },
56
+ "./metrics": {
57
+ "import": {
58
+ "types": "./dist/metrics/index.d.ts",
59
+ "default": "./dist/metrics/index.mjs"
60
+ },
61
+ "require": {
62
+ "types": "./dist/metrics/index.d.cts",
63
+ "default": "./dist/metrics/index.cjs"
64
+ }
65
+ },
66
+ "./openapi": {
67
+ "import": {
68
+ "types": "./dist/openapi/index.d.ts",
69
+ "default": "./dist/openapi/index.mjs"
70
+ },
71
+ "require": {
72
+ "types": "./dist/openapi/index.d.cts",
73
+ "default": "./dist/openapi/index.cjs"
74
+ }
75
+ },
56
76
  "./package.json": "./package.json"
57
77
  },
58
78
  "scripts": {
59
79
  "build": "pnpm clean && tsup",
60
80
  "check:exports": "attw --pack . --profile strict",
81
+ "check:mutants": "node scripts/check-mutation-directives.mjs",
61
82
  "check:published": "node scripts/check-published-surface.mjs",
62
83
  "check:runtime": "node scripts/check-consumer-runtime.mjs",
63
84
  "clean": "node -e \"const fs=require('node:fs');for(const d of ['dist','coverage'])fs.rmSync(d,{recursive:true,force:true})\"",
@@ -68,7 +89,7 @@
68
89
  "mutation:dry-run": "stryker run --dryRunOnly",
69
90
  "mutation:incremental": "stryker run --incremental",
70
91
  "prepare": "husky",
71
- "prepublishOnly": "pnpm clean && pnpm typecheck && pnpm lint && pnpm test:cov:all && pnpm build && pnpm size && pnpm check:published",
92
+ "prepublishOnly": "pnpm clean && pnpm typecheck && pnpm lint && pnpm check:mutants && pnpm test:cov:all && pnpm build && pnpm size && pnpm check:published",
72
93
  "release": "npm publish --provenance --access public",
73
94
  "size": "node scripts/check-size.mjs",
74
95
  "test": "jest",
@@ -87,15 +108,22 @@
87
108
  "prettier --write"
88
109
  ]
89
110
  },
90
- "dependencies": {},
91
111
  "peerDependencies": {
92
112
  "@nestjs/common": "^11.0.16",
93
113
  "@nestjs/core": "^11.1.18",
114
+ "@nestjs/swagger": "^11.0.0",
115
+ "@opentelemetry/api": "^1.9.0",
116
+ "prom-client": "^15.0.0",
94
117
  "reflect-metadata": "^0.2.0",
95
- "rxjs": "^7.0.0",
96
- "prom-client": "^15.0.0"
118
+ "rxjs": "^7.0.0"
97
119
  },
98
120
  "peerDependenciesMeta": {
121
+ "@nestjs/swagger": {
122
+ "optional": true
123
+ },
124
+ "@opentelemetry/api": {
125
+ "optional": true
126
+ },
99
127
  "prom-client": {
100
128
  "optional": true
101
129
  }
@@ -108,7 +136,10 @@
108
136
  "@nestjs/common": "^11.1.20",
109
137
  "@nestjs/core": "^11.1.20",
110
138
  "@nestjs/platform-express": "^11.1.20",
139
+ "@nestjs/swagger": "^11.4.6",
111
140
  "@nestjs/testing": "^11.1.20",
141
+ "@opentelemetry/api": "^1.9.1",
142
+ "@opentelemetry/context-async-hooks": "^2.10.0",
112
143
  "@stryker-mutator/core": "^9",
113
144
  "@stryker-mutator/jest-runner": "^9",
114
145
  "@stryker-mutator/typescript-checker": "^9",
@@ -147,9 +178,13 @@
147
178
  "health-check",
148
179
  "prometheus",
149
180
  "metrics",
181
+ "openapi",
182
+ "swagger",
183
+ "opentelemetry",
184
+ "tracing",
150
185
  "typescript"
151
186
  ],
152
- "packageManager": "pnpm@10.8.1",
187
+ "packageManager": "pnpm@11.20.0",
153
188
  "engines": {
154
189
  "node": ">=24.0.0"
155
190
  },
@@ -158,16 +193,6 @@
158
193
  "provenance": true,
159
194
  "registry": "https://registry.npmjs.org/"
160
195
  },
161
- "pnpm": {
162
- "overrides": {
163
- "brace-expansion@1": "^1.1.18",
164
- "brace-expansion@2": "^2.1.4",
165
- "brace-expansion@5": "^5.0.9",
166
- "esbuild": "^0.28.1",
167
- "fast-uri": "^3.1.5",
168
- "qs": "^6.15.2"
169
- }
170
- },
171
196
  "module": "./dist/index.mjs",
172
197
  "typesVersions": {
173
198
  "*": {
@@ -176,6 +201,12 @@
176
201
  ],
177
202
  "health": [
178
203
  "./dist/health/index.d.cts"
204
+ ],
205
+ "openapi": [
206
+ "./dist/openapi/index.d.cts"
207
+ ],
208
+ "metrics": [
209
+ "./dist/metrics/index.d.cts"
179
210
  ]
180
211
  }
181
212
  }