@bymax-one/nest-core 1.0.0 → 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.0",
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,8 +53,51 @@
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
  },
78
+ "scripts": {
79
+ "build": "pnpm clean && tsup",
80
+ "check:exports": "attw --pack . --profile strict",
81
+ "check:published": "node scripts/check-published-surface.mjs",
82
+ "check:runtime": "node scripts/check-consumer-runtime.mjs",
83
+ "clean": "node -e \"const fs=require('node:fs');for(const d of ['dist','coverage'])fs.rmSync(d,{recursive:true,force:true})\"",
84
+ "dogfood": "node scripts/dogfood-smoke-test.mjs",
85
+ "lint": "eslint --no-error-on-unmatched-pattern src scripts test",
86
+ "lint:fix": "eslint --no-error-on-unmatched-pattern src scripts test --fix",
87
+ "mutation": "stryker run",
88
+ "mutation:dry-run": "stryker run --dryRunOnly",
89
+ "mutation:incremental": "stryker run --incremental",
90
+ "prepare": "husky",
91
+ "prepublishOnly": "pnpm clean && pnpm typecheck && pnpm lint && pnpm test:cov:all && pnpm build && pnpm size && pnpm check:published",
92
+ "release": "npm publish --provenance --access public",
93
+ "size": "node scripts/check-size.mjs",
94
+ "test": "jest",
95
+ "test:cov": "jest --coverage",
96
+ "test:cov:all": "jest --config jest.coverage.config.ts --coverage",
97
+ "test:e2e": "jest --config jest.e2e.config.ts",
98
+ "test:watch": "jest --watch",
99
+ "typecheck": "tsc --noEmit"
100
+ },
58
101
  "lint-staged": {
59
102
  "*.{ts,tsx,js,mjs,cjs}": [
60
103
  "eslint --fix",
@@ -64,15 +107,22 @@
64
107
  "prettier --write"
65
108
  ]
66
109
  },
67
- "dependencies": {},
68
110
  "peerDependencies": {
69
111
  "@nestjs/common": "^11.0.16",
70
112
  "@nestjs/core": "^11.1.18",
113
+ "@nestjs/swagger": "^11.0.0",
114
+ "@opentelemetry/api": "^1.9.0",
115
+ "prom-client": "^15.0.0",
71
116
  "reflect-metadata": "^0.2.0",
72
- "rxjs": "^7.0.0",
73
- "prom-client": "^15.0.0"
117
+ "rxjs": "^7.0.0"
74
118
  },
75
119
  "peerDependenciesMeta": {
120
+ "@nestjs/swagger": {
121
+ "optional": true
122
+ },
123
+ "@opentelemetry/api": {
124
+ "optional": true
125
+ },
76
126
  "prom-client": {
77
127
  "optional": true
78
128
  }
@@ -85,7 +135,10 @@
85
135
  "@nestjs/common": "^11.1.20",
86
136
  "@nestjs/core": "^11.1.20",
87
137
  "@nestjs/platform-express": "^11.1.20",
138
+ "@nestjs/swagger": "^11.4.6",
88
139
  "@nestjs/testing": "^11.1.20",
140
+ "@opentelemetry/api": "^1.9.1",
141
+ "@opentelemetry/context-async-hooks": "^2.10.0",
89
142
  "@stryker-mutator/core": "^9",
90
143
  "@stryker-mutator/jest-runner": "^9",
91
144
  "@stryker-mutator/typescript-checker": "^9",
@@ -124,8 +177,13 @@
124
177
  "health-check",
125
178
  "prometheus",
126
179
  "metrics",
180
+ "openapi",
181
+ "swagger",
182
+ "opentelemetry",
183
+ "tracing",
127
184
  "typescript"
128
185
  ],
186
+ "packageManager": "pnpm@11.20.0",
129
187
  "engines": {
130
188
  "node": ">=24.0.0"
131
189
  },
@@ -142,28 +200,13 @@
142
200
  ],
143
201
  "health": [
144
202
  "./dist/health/index.d.cts"
203
+ ],
204
+ "openapi": [
205
+ "./dist/openapi/index.d.cts"
206
+ ],
207
+ "metrics": [
208
+ "./dist/metrics/index.d.cts"
145
209
  ]
146
210
  }
147
- },
148
- "scripts": {
149
- "build": "pnpm clean && tsup",
150
- "check:exports": "attw --pack . --profile strict",
151
- "check:published": "node scripts/check-published-surface.mjs",
152
- "check:runtime": "node scripts/check-consumer-runtime.mjs",
153
- "clean": "node -e \"const fs=require('node:fs');for(const d of ['dist','coverage'])fs.rmSync(d,{recursive:true,force:true})\"",
154
- "dogfood": "node scripts/dogfood-smoke-test.mjs",
155
- "lint": "eslint --no-error-on-unmatched-pattern src scripts test",
156
- "lint:fix": "eslint --no-error-on-unmatched-pattern src scripts test --fix",
157
- "mutation": "stryker run",
158
- "mutation:dry-run": "stryker run --dryRunOnly",
159
- "mutation:incremental": "stryker run --incremental",
160
- "release": "npm publish --provenance --access public",
161
- "size": "node scripts/check-size.mjs",
162
- "test": "jest",
163
- "test:cov": "jest --coverage",
164
- "test:cov:all": "jest --config jest.coverage.config.ts --coverage",
165
- "test:e2e": "jest --config jest.e2e.config.ts",
166
- "test:watch": "jest --watch",
167
- "typecheck": "tsc --noEmit"
168
211
  }
169
- }
212
+ }