@bymax-one/nest-core 1.6.0 → 1.6.2

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.
@@ -132,6 +132,82 @@ function collectContributions(discovery, reflector, handlers) {
132
132
  });
133
133
  }
134
134
 
135
+ // src/openapi/openapi.shape.ts
136
+ var OPERATION_METHODS = [
137
+ "get",
138
+ "post",
139
+ "put",
140
+ "patch",
141
+ "delete",
142
+ "head",
143
+ "options",
144
+ "trace"
145
+ ];
146
+ function asRecord(value) {
147
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
148
+ return {};
149
+ }
150
+ return value;
151
+ }
152
+ function operationsOf(item) {
153
+ return Object.entries(asRecord(item)).filter(([key]) => OPERATION_METHODS.includes(key));
154
+ }
155
+ function operationKey(method, path) {
156
+ return `${method.toUpperCase()} ${path}`;
157
+ }
158
+
159
+ // src/openapi/openapi.components.ts
160
+ function isEmptyRequired(key, value) {
161
+ return key === "required" && Array.isArray(value) && value.length === 0;
162
+ }
163
+ var IGNORABLE_KEYS = /* @__PURE__ */ new Set(["type", "properties", "description"]);
164
+ function describesNoShape(entry) {
165
+ const type = entry["type"];
166
+ if (type !== void 0 && type !== "object") {
167
+ return false;
168
+ }
169
+ const properties = entry["properties"];
170
+ if (!Object.hasOwn(entry, "properties") || asRecord(properties) !== properties) {
171
+ return false;
172
+ }
173
+ if (Object.keys(asRecord(properties)).length > 0) {
174
+ return false;
175
+ }
176
+ return Object.entries(entry).every(
177
+ ([key, value]) => IGNORABLE_KEYS.has(key) || isEmptyRequired(key, value)
178
+ );
179
+ }
180
+ function placeComponentEntries(existing, member, offered, onKept) {
181
+ const next = new Map(Object.entries(asRecord(existing)));
182
+ for (const [name, contributed] of Object.entries(asRecord(offered))) {
183
+ const declared = next.get(name);
184
+ const held = asRecord(declared);
185
+ const replaceable = member === "schemas" && describesNoShape(held);
186
+ if (declared !== void 0 && !replaceable) {
187
+ onKept(name);
188
+ continue;
189
+ }
190
+ const described = held["description"];
191
+ next.set(
192
+ name,
193
+ described === void 0 || described === "" ? contributed : { ...asRecord(contributed), description: described }
194
+ );
195
+ }
196
+ return Object.fromEntries(next);
197
+ }
198
+ function mergeContributedComponents(merged, contributions) {
199
+ const discarded = [];
200
+ for (const contribution of contributions) {
201
+ for (const [member, entries] of Object.entries(contribution.components)) {
202
+ const placed = placeComponentEntries(merged.get(member), member, entries, (name) => {
203
+ discarded.push({ contributor: contribution.label, member, name });
204
+ });
205
+ merged.set(member, placed);
206
+ }
207
+ }
208
+ return discarded;
209
+ }
210
+
135
211
  // src/route-defaults.ts
136
212
  var DEFAULT_HEALTH_PATH = "health";
137
213
  var DEFAULT_METRICS_PATH = "metrics";
@@ -324,30 +400,6 @@ var CORE_PARAMETERS = {
324
400
  }
325
401
  };
326
402
 
327
- // src/openapi/openapi.shape.ts
328
- var OPERATION_METHODS = [
329
- "get",
330
- "post",
331
- "put",
332
- "patch",
333
- "delete",
334
- "head",
335
- "options",
336
- "trace"
337
- ];
338
- function asRecord(value) {
339
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
340
- return {};
341
- }
342
- return value;
343
- }
344
- function operationsOf(item) {
345
- return Object.entries(asRecord(item)).filter(([key]) => OPERATION_METHODS.includes(key));
346
- }
347
- function operationKey(method, path) {
348
- return `${method.toUpperCase()} ${path}`;
349
- }
350
-
351
403
  // src/openapi/openapi.document.ts
352
404
  var METRICS_SCHEME_NAME = "BymaxMetricsAuth";
353
405
  var ERROR_ENVELOPE_SCHEMA = "BymaxErrorEnvelope";
@@ -539,8 +591,14 @@ function augmentDocument(document, options, pathPrefixes = [""], contributions =
539
591
  const components = asRecord(document.components);
540
592
  const merged = new Map(Object.entries(components));
541
593
  if (openapi.includeCoreSchemas) {
542
- merged.set("schemas", mergeAbsent(asRecord(components["schemas"]), CORE_SCHEMAS));
543
- merged.set("parameters", mergeAbsent(asRecord(components["parameters"]), CORE_PARAMETERS));
594
+ const keep = () => void 0;
595
+ const catalogue = [
596
+ ["schemas", CORE_SCHEMAS],
597
+ ["parameters", CORE_PARAMETERS]
598
+ ];
599
+ for (const [member, entries] of catalogue) {
600
+ merged.set(member, placeComponentEntries(merged.get(member), member, entries, keep));
601
+ }
544
602
  }
545
603
  const scrapeScheme = options.metrics.authToken === void 0 ? {} : {
546
604
  [METRICS_SCHEME_NAME]: {
@@ -557,11 +615,7 @@ function augmentDocument(document, options, pathPrefixes = [""], contributions =
557
615
  if (Object.keys(declaredSchemes).length > 0) {
558
616
  merged.set("securitySchemes", declaredSchemes);
559
617
  }
560
- for (const contribution of contributions) {
561
- for (const [member, entries] of Object.entries(contribution.components)) {
562
- merged.set(member, mergeAbsent(asRecord(merged.get(member)), entries));
563
- }
564
- }
618
+ const discarded = mergeContributedComponents(merged, contributions);
565
619
  assertSchemesDeclared(openapi, asRecord(merged.get("securitySchemes")));
566
620
  const routes = indexOwnRoutes(options, pathPrefixes);
567
621
  const served = withoutDisabledRoutes(asRecord(document.paths), options, routes);
@@ -576,7 +630,10 @@ function augmentDocument(document, options, pathPrefixes = [""], contributions =
576
630
  )
577
631
  };
578
632
  const security = openapi.security.length > 0 && document.security === void 0 ? { security: openapi.security } : {};
579
- return { ...document, components: Object.fromEntries(merged), ...paths, ...security };
633
+ return {
634
+ document: { ...document, components: Object.fromEntries(merged), ...paths, ...security },
635
+ discarded
636
+ };
580
637
  }
581
638
 
582
639
  // src/optional-peer.ts
@@ -671,6 +728,16 @@ function warnUnsecuredOperations(logger, keys) {
671
728
  `a client generated from the OpenAPI document will send no credentials to ${keys.length} operation(s): ${listed}${elided > 0 ? `, and ${elided} more` : ""}. They state no security requirement, the document declares no default, and other operations in it do state one \u2014 so this is more often a missing openapi.security default than a public API. Set openapi.security, or state the intent per operation with an explicit [] in openapi.operationSecurity.`
672
729
  );
673
730
  }
731
+ function warnDiscardedComponents(logger, discarded) {
732
+ if (discarded.length === 0) {
733
+ return;
734
+ }
735
+ const elided = discarded.length - MAX_WARNED_OPERATIONS;
736
+ const listed = discarded.slice(0, MAX_WARNED_OPERATIONS).map(({ contributor, member, name }) => `${contributor} -> ${member}.${name}`).join(", ");
737
+ logger.warn(
738
+ `the served OpenAPI document already defines ${discarded.length} component entry(ies) a library also contributes: ${listed}${elided > 0 ? `, and ${elided} more` : ""}. The contributed version is not served, so a client generated from this document will not match that library's own types. What holds the name may be the application's own declaration, a core schema under openapi.includeCoreSchemas, or a contributor that sorted earlier \u2014 read the document before deciding what to remove.`
739
+ );
740
+ }
674
741
  function buildConfig(builder, options) {
675
742
  builder.setTitle(options.title).setDescription(options.description).setVersion(options.version);
676
743
  for (const server of options.servers) {
@@ -700,13 +767,19 @@ async function applyBymaxOpenApi(app) {
700
767
  operationIdFactory: recordingOperationIdFactory(handlers, options.operationIdFactory)
701
768
  });
702
769
  const prefixes = readPathPrefixes(app);
703
- const document = augmentDocument(generated, resolved, prefixes, readContributions(app, handlers));
770
+ const { document, discarded } = augmentDocument(
771
+ generated,
772
+ resolved,
773
+ prefixes,
774
+ readContributions(app, handlers)
775
+ );
704
776
  warnUnsecuredOperations(logger, unsecuredOperations(document, resolved, prefixes));
777
+ warnDiscardedComponents(logger, discarded);
705
778
  swagger.SwaggerModule.setup(options.path, app, document, {
706
779
  jsonDocumentUrl: options.jsonPath
707
780
  });
708
781
  logger.log(`OpenAPI document served at "/${options.path}" (JSON at "/${options.jsonPath}")`);
709
- return { mounted: true, path: options.path };
782
+ return { mounted: true, path: options.path, discarded };
710
783
  }
711
784
 
712
785
  export { BYMAX_OPENAPI_CONTRACT_VERSION, BYMAX_OPENAPI_CONTRIBUTOR_METADATA, BymaxOpenApiContributor, applyBymaxOpenApi };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bymax-one/nest-core",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
4
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",
@@ -136,7 +136,7 @@
136
136
  "@nestjs/common": "^11.1.20",
137
137
  "@nestjs/core": "^11.1.20",
138
138
  "@nestjs/platform-express": "^11.1.20",
139
- "@nestjs/platform-fastify": "^11.1.29",
139
+ "@nestjs/platform-fastify": "^11.2.4",
140
140
  "@nestjs/swagger": "^11.4.6",
141
141
  "@nestjs/testing": "^11.1.20",
142
142
  "@opentelemetry/api": "^1.9.1",
@@ -156,7 +156,7 @@
156
156
  "eslint-plugin-import": "^2.32.0",
157
157
  "eslint-plugin-prettier": "^5.5.5",
158
158
  "eslint-plugin-security": "^4.0.0",
159
- "fastify": "^5.11.3",
159
+ "fastify": "^5.12.5",
160
160
  "globals": "^17.6.0",
161
161
  "husky": "^9.1.7",
162
162
  "jest": "^30.4.2",