@bymax-one/nest-core 1.0.1 → 1.1.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.
@@ -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 };
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.0",
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,6 +53,26 @@
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": {
@@ -87,15 +107,22 @@
87
107
  "prettier --write"
88
108
  ]
89
109
  },
90
- "dependencies": {},
91
110
  "peerDependencies": {
92
111
  "@nestjs/common": "^11.0.16",
93
112
  "@nestjs/core": "^11.1.18",
113
+ "@nestjs/swagger": "^11.0.0",
114
+ "@opentelemetry/api": "^1.9.0",
115
+ "prom-client": "^15.0.0",
94
116
  "reflect-metadata": "^0.2.0",
95
- "rxjs": "^7.0.0",
96
- "prom-client": "^15.0.0"
117
+ "rxjs": "^7.0.0"
97
118
  },
98
119
  "peerDependenciesMeta": {
120
+ "@nestjs/swagger": {
121
+ "optional": true
122
+ },
123
+ "@opentelemetry/api": {
124
+ "optional": true
125
+ },
99
126
  "prom-client": {
100
127
  "optional": true
101
128
  }
@@ -108,7 +135,10 @@
108
135
  "@nestjs/common": "^11.1.20",
109
136
  "@nestjs/core": "^11.1.20",
110
137
  "@nestjs/platform-express": "^11.1.20",
138
+ "@nestjs/swagger": "^11.4.6",
111
139
  "@nestjs/testing": "^11.1.20",
140
+ "@opentelemetry/api": "^1.9.1",
141
+ "@opentelemetry/context-async-hooks": "^2.10.0",
112
142
  "@stryker-mutator/core": "^9",
113
143
  "@stryker-mutator/jest-runner": "^9",
114
144
  "@stryker-mutator/typescript-checker": "^9",
@@ -147,9 +177,13 @@
147
177
  "health-check",
148
178
  "prometheus",
149
179
  "metrics",
180
+ "openapi",
181
+ "swagger",
182
+ "opentelemetry",
183
+ "tracing",
150
184
  "typescript"
151
185
  ],
152
- "packageManager": "pnpm@10.8.1",
186
+ "packageManager": "pnpm@11.20.0",
153
187
  "engines": {
154
188
  "node": ">=24.0.0"
155
189
  },
@@ -158,16 +192,6 @@
158
192
  "provenance": true,
159
193
  "registry": "https://registry.npmjs.org/"
160
194
  },
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
195
  "module": "./dist/index.mjs",
172
196
  "typesVersions": {
173
197
  "*": {
@@ -176,6 +200,12 @@
176
200
  ],
177
201
  "health": [
178
202
  "./dist/health/index.d.cts"
203
+ ],
204
+ "openapi": [
205
+ "./dist/openapi/index.d.cts"
206
+ ],
207
+ "metrics": [
208
+ "./dist/metrics/index.d.cts"
179
209
  ]
180
210
  }
181
211
  }