@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.
- package/CHANGELOG.md +136 -1
- package/README.md +108 -23
- package/dist/index.cjs +85 -36
- package/dist/index.d.cts +119 -5
- package/dist/index.d.ts +119 -5
- package/dist/index.mjs +85 -37
- package/dist/openapi/index.cjs +107 -34
- package/dist/openapi/index.d.cts +97 -53
- package/dist/openapi/index.d.ts +97 -53
- package/dist/openapi/index.mjs +107 -34
- package/package.json +3 -3
package/dist/openapi/index.mjs
CHANGED
|
@@ -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
|
-
|
|
543
|
-
|
|
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
|
-
|
|
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 {
|
|
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(
|
|
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.
|
|
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.
|
|
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.
|
|
159
|
+
"fastify": "^5.12.5",
|
|
160
160
|
"globals": "^17.6.0",
|
|
161
161
|
"husky": "^9.1.7",
|
|
162
162
|
"jest": "^30.4.2",
|