@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.cjs
CHANGED
|
@@ -134,6 +134,82 @@ function collectContributions(discovery, reflector, handlers) {
|
|
|
134
134
|
});
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
// src/openapi/openapi.shape.ts
|
|
138
|
+
var OPERATION_METHODS = [
|
|
139
|
+
"get",
|
|
140
|
+
"post",
|
|
141
|
+
"put",
|
|
142
|
+
"patch",
|
|
143
|
+
"delete",
|
|
144
|
+
"head",
|
|
145
|
+
"options",
|
|
146
|
+
"trace"
|
|
147
|
+
];
|
|
148
|
+
function asRecord(value) {
|
|
149
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
150
|
+
return {};
|
|
151
|
+
}
|
|
152
|
+
return value;
|
|
153
|
+
}
|
|
154
|
+
function operationsOf(item) {
|
|
155
|
+
return Object.entries(asRecord(item)).filter(([key]) => OPERATION_METHODS.includes(key));
|
|
156
|
+
}
|
|
157
|
+
function operationKey(method, path) {
|
|
158
|
+
return `${method.toUpperCase()} ${path}`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/openapi/openapi.components.ts
|
|
162
|
+
function isEmptyRequired(key, value) {
|
|
163
|
+
return key === "required" && Array.isArray(value) && value.length === 0;
|
|
164
|
+
}
|
|
165
|
+
var IGNORABLE_KEYS = /* @__PURE__ */ new Set(["type", "properties", "description"]);
|
|
166
|
+
function describesNoShape(entry) {
|
|
167
|
+
const type = entry["type"];
|
|
168
|
+
if (type !== void 0 && type !== "object") {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
const properties = entry["properties"];
|
|
172
|
+
if (!Object.hasOwn(entry, "properties") || asRecord(properties) !== properties) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
if (Object.keys(asRecord(properties)).length > 0) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
return Object.entries(entry).every(
|
|
179
|
+
([key, value]) => IGNORABLE_KEYS.has(key) || isEmptyRequired(key, value)
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
function placeComponentEntries(existing, member, offered, onKept) {
|
|
183
|
+
const next = new Map(Object.entries(asRecord(existing)));
|
|
184
|
+
for (const [name, contributed] of Object.entries(asRecord(offered))) {
|
|
185
|
+
const declared = next.get(name);
|
|
186
|
+
const held = asRecord(declared);
|
|
187
|
+
const replaceable = member === "schemas" && describesNoShape(held);
|
|
188
|
+
if (declared !== void 0 && !replaceable) {
|
|
189
|
+
onKept(name);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const described = held["description"];
|
|
193
|
+
next.set(
|
|
194
|
+
name,
|
|
195
|
+
described === void 0 || described === "" ? contributed : { ...asRecord(contributed), description: described }
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
return Object.fromEntries(next);
|
|
199
|
+
}
|
|
200
|
+
function mergeContributedComponents(merged, contributions) {
|
|
201
|
+
const discarded = [];
|
|
202
|
+
for (const contribution of contributions) {
|
|
203
|
+
for (const [member, entries] of Object.entries(contribution.components)) {
|
|
204
|
+
const placed = placeComponentEntries(merged.get(member), member, entries, (name) => {
|
|
205
|
+
discarded.push({ contributor: contribution.label, member, name });
|
|
206
|
+
});
|
|
207
|
+
merged.set(member, placed);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return discarded;
|
|
211
|
+
}
|
|
212
|
+
|
|
137
213
|
// src/route-defaults.ts
|
|
138
214
|
var DEFAULT_HEALTH_PATH = "health";
|
|
139
215
|
var DEFAULT_METRICS_PATH = "metrics";
|
|
@@ -326,30 +402,6 @@ var CORE_PARAMETERS = {
|
|
|
326
402
|
}
|
|
327
403
|
};
|
|
328
404
|
|
|
329
|
-
// src/openapi/openapi.shape.ts
|
|
330
|
-
var OPERATION_METHODS = [
|
|
331
|
-
"get",
|
|
332
|
-
"post",
|
|
333
|
-
"put",
|
|
334
|
-
"patch",
|
|
335
|
-
"delete",
|
|
336
|
-
"head",
|
|
337
|
-
"options",
|
|
338
|
-
"trace"
|
|
339
|
-
];
|
|
340
|
-
function asRecord(value) {
|
|
341
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
342
|
-
return {};
|
|
343
|
-
}
|
|
344
|
-
return value;
|
|
345
|
-
}
|
|
346
|
-
function operationsOf(item) {
|
|
347
|
-
return Object.entries(asRecord(item)).filter(([key]) => OPERATION_METHODS.includes(key));
|
|
348
|
-
}
|
|
349
|
-
function operationKey(method, path) {
|
|
350
|
-
return `${method.toUpperCase()} ${path}`;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
405
|
// src/openapi/openapi.document.ts
|
|
354
406
|
var METRICS_SCHEME_NAME = "BymaxMetricsAuth";
|
|
355
407
|
var ERROR_ENVELOPE_SCHEMA = "BymaxErrorEnvelope";
|
|
@@ -541,8 +593,14 @@ function augmentDocument(document, options, pathPrefixes = [""], contributions =
|
|
|
541
593
|
const components = asRecord(document.components);
|
|
542
594
|
const merged = new Map(Object.entries(components));
|
|
543
595
|
if (openapi.includeCoreSchemas) {
|
|
544
|
-
|
|
545
|
-
|
|
596
|
+
const keep = () => void 0;
|
|
597
|
+
const catalogue = [
|
|
598
|
+
["schemas", CORE_SCHEMAS],
|
|
599
|
+
["parameters", CORE_PARAMETERS]
|
|
600
|
+
];
|
|
601
|
+
for (const [member, entries] of catalogue) {
|
|
602
|
+
merged.set(member, placeComponentEntries(merged.get(member), member, entries, keep));
|
|
603
|
+
}
|
|
546
604
|
}
|
|
547
605
|
const scrapeScheme = options.metrics.authToken === void 0 ? {} : {
|
|
548
606
|
[METRICS_SCHEME_NAME]: {
|
|
@@ -559,11 +617,7 @@ function augmentDocument(document, options, pathPrefixes = [""], contributions =
|
|
|
559
617
|
if (Object.keys(declaredSchemes).length > 0) {
|
|
560
618
|
merged.set("securitySchemes", declaredSchemes);
|
|
561
619
|
}
|
|
562
|
-
|
|
563
|
-
for (const [member, entries] of Object.entries(contribution.components)) {
|
|
564
|
-
merged.set(member, mergeAbsent(asRecord(merged.get(member)), entries));
|
|
565
|
-
}
|
|
566
|
-
}
|
|
620
|
+
const discarded = mergeContributedComponents(merged, contributions);
|
|
567
621
|
assertSchemesDeclared(openapi, asRecord(merged.get("securitySchemes")));
|
|
568
622
|
const routes = indexOwnRoutes(options, pathPrefixes);
|
|
569
623
|
const served = withoutDisabledRoutes(asRecord(document.paths), options, routes);
|
|
@@ -578,7 +632,10 @@ function augmentDocument(document, options, pathPrefixes = [""], contributions =
|
|
|
578
632
|
)
|
|
579
633
|
};
|
|
580
634
|
const security = openapi.security.length > 0 && document.security === void 0 ? { security: openapi.security } : {};
|
|
581
|
-
return {
|
|
635
|
+
return {
|
|
636
|
+
document: { ...document, components: Object.fromEntries(merged), ...paths, ...security },
|
|
637
|
+
discarded
|
|
638
|
+
};
|
|
582
639
|
}
|
|
583
640
|
|
|
584
641
|
// src/optional-peer.ts
|
|
@@ -673,6 +730,16 @@ function warnUnsecuredOperations(logger, keys) {
|
|
|
673
730
|
`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.`
|
|
674
731
|
);
|
|
675
732
|
}
|
|
733
|
+
function warnDiscardedComponents(logger, discarded) {
|
|
734
|
+
if (discarded.length === 0) {
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
const elided = discarded.length - MAX_WARNED_OPERATIONS;
|
|
738
|
+
const listed = discarded.slice(0, MAX_WARNED_OPERATIONS).map(({ contributor, member, name }) => `${contributor} -> ${member}.${name}`).join(", ");
|
|
739
|
+
logger.warn(
|
|
740
|
+
`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.`
|
|
741
|
+
);
|
|
742
|
+
}
|
|
676
743
|
function buildConfig(builder, options) {
|
|
677
744
|
builder.setTitle(options.title).setDescription(options.description).setVersion(options.version);
|
|
678
745
|
for (const server of options.servers) {
|
|
@@ -702,13 +769,19 @@ async function applyBymaxOpenApi(app) {
|
|
|
702
769
|
operationIdFactory: recordingOperationIdFactory(handlers, options.operationIdFactory)
|
|
703
770
|
});
|
|
704
771
|
const prefixes = readPathPrefixes(app);
|
|
705
|
-
const document = augmentDocument(
|
|
772
|
+
const { document, discarded } = augmentDocument(
|
|
773
|
+
generated,
|
|
774
|
+
resolved,
|
|
775
|
+
prefixes,
|
|
776
|
+
readContributions(app, handlers)
|
|
777
|
+
);
|
|
706
778
|
warnUnsecuredOperations(logger, unsecuredOperations(document, resolved, prefixes));
|
|
779
|
+
warnDiscardedComponents(logger, discarded);
|
|
707
780
|
swagger.SwaggerModule.setup(options.path, app, document, {
|
|
708
781
|
jsonDocumentUrl: options.jsonPath
|
|
709
782
|
});
|
|
710
783
|
logger.log(`OpenAPI document served at "/${options.path}" (JSON at "/${options.jsonPath}")`);
|
|
711
|
-
return { mounted: true, path: options.path };
|
|
784
|
+
return { mounted: true, path: options.path, discarded };
|
|
712
785
|
}
|
|
713
786
|
|
|
714
787
|
exports.BYMAX_OPENAPI_CONTRACT_VERSION = BYMAX_OPENAPI_CONTRACT_VERSION;
|
package/dist/openapi/index.d.cts
CHANGED
|
@@ -1,55 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
/** Why the document was not mounted. */
|
|
4
|
-
type OpenApiSkipReason =
|
|
5
|
-
/** The consumer never enabled the feature. */
|
|
6
|
-
'disabled'
|
|
7
|
-
/** The runtime is production, where the document is never served. */
|
|
8
|
-
| 'production';
|
|
9
|
-
/** What {@link applyBymaxOpenApi} did, so a caller can assert on it. */
|
|
10
|
-
interface OpenApiMountOutcome {
|
|
11
|
-
/** Whether the document and its UI were mounted. */
|
|
12
|
-
mounted: boolean;
|
|
13
|
-
/** Present only when `mounted` is `false`. */
|
|
14
|
-
reason?: OpenApiSkipReason;
|
|
15
|
-
/** The route the UI was mounted at. Present only when `mounted` is `true`. */
|
|
16
|
-
path?: string;
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Build and mount the OpenAPI document and its interactive UI, when the
|
|
20
|
-
* configuration and the runtime both allow it.
|
|
21
|
-
*
|
|
22
|
-
* Call it once, after `NestFactory.create` and BEFORE the application starts
|
|
23
|
-
* listening. The ordering is not a style preference: mounting the document
|
|
24
|
-
* re-registers routes on the HTTP adapter, and on Express 5 doing that against
|
|
25
|
-
* an already-initialized application replaces the router — every route the
|
|
26
|
-
* application had, including its own controllers and this package's health
|
|
27
|
-
* endpoints, stops resolving. `app.listen()` performs that initialization, so
|
|
28
|
-
* "before listening" is the whole rule.
|
|
29
|
-
*
|
|
30
|
-
* It is safe to call unconditionally: with the feature disabled, or in
|
|
31
|
-
* production, it mounts nothing, loads no optional peer, and returns why.
|
|
32
|
-
*
|
|
33
|
-
* Testing this under Jest needs one flag. `@nestjs/swagger` is loaded through a
|
|
34
|
-
* dynamic `import()`, which is what keeps the peer optional for everyone who
|
|
35
|
-
* never enables the document — and Jest's module registry cannot service a
|
|
36
|
-
* dynamic import without `NODE_OPTIONS=--experimental-vm-modules`. Without it,
|
|
37
|
-
* only the *enabled* path fails, with `dynamic import callback invoked without
|
|
38
|
-
* --experimental-vm-modules`; the disabled and production paths never reach the
|
|
39
|
-
* loader and pass either way, which is what makes the omission confusing.
|
|
40
|
-
*
|
|
41
|
-
* @param app - The created Nest application, not yet listening.
|
|
42
|
-
* @returns What happened: mounted, or skipped with a reason.
|
|
43
|
-
* @throws Error When `BymaxCoreModule` is not registered, when the feature is
|
|
44
|
-
* enabled and the optional peer `@nestjs/swagger` is not installed, or when
|
|
45
|
-
* `openapi.operationSecurity` addresses an operation the generated document
|
|
46
|
-
* does not contain.
|
|
47
|
-
* @example
|
|
48
|
-
* const app = await NestFactory.create(AppModule)
|
|
49
|
-
* await applyBymaxOpenApi(app)
|
|
50
|
-
* await app.listen(3000)
|
|
51
|
-
*/
|
|
52
|
-
declare function applyBymaxOpenApi(app: INestApplication): Promise<OpenApiMountOutcome>;
|
|
1
|
+
import { CustomDecorator, INestApplication } from '@nestjs/common';
|
|
53
2
|
|
|
54
3
|
/**
|
|
55
4
|
* Addresses one route handler, as `'<ControllerClassName>.<methodName>'`.
|
|
@@ -196,4 +145,99 @@ declare const BYMAX_OPENAPI_CONTRIBUTOR_METADATA = "bymax-one:openapi-contributo
|
|
|
196
145
|
*/
|
|
197
146
|
declare function BymaxOpenApiContributor(): CustomDecorator<string>;
|
|
198
147
|
|
|
199
|
-
|
|
148
|
+
/**
|
|
149
|
+
* @fileoverview Placing offered entries — this package's catalogue and every
|
|
150
|
+
* library contribution — into a document's `components`, and reporting what a
|
|
151
|
+
* contributor could not place.
|
|
152
|
+
*
|
|
153
|
+
* A module of its own because the rule here is not the document's rule: everywhere else the merge is "the existing side wins", and this is the
|
|
154
|
+
* one place that asks whether the existing side says anything at all. Keeping
|
|
155
|
+
* the question and its narrow carve-out together makes the exception legible as
|
|
156
|
+
* an exception.
|
|
157
|
+
* @layer internal
|
|
158
|
+
*/
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* One component entry a contributor offered and the document already declared.
|
|
162
|
+
*
|
|
163
|
+
* Carries the contributor's label because a collision is only actionable if you
|
|
164
|
+
* know who lost it: an application with several contributing libraries would
|
|
165
|
+
* otherwise be told a name was dropped without being told whose it was.
|
|
166
|
+
*/
|
|
167
|
+
interface DiscardedComponent {
|
|
168
|
+
/** The contributing class, as {@link ResolvedContribution.label} names it. */
|
|
169
|
+
readonly contributor: string;
|
|
170
|
+
/** The component member, such as `schemas` or `parameters`. */
|
|
171
|
+
readonly member: string;
|
|
172
|
+
/** The entry's name within that member. */
|
|
173
|
+
readonly name: string;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Why the document was not mounted. */
|
|
177
|
+
type OpenApiSkipReason =
|
|
178
|
+
/** The consumer never enabled the feature. */
|
|
179
|
+
'disabled'
|
|
180
|
+
/** The runtime is production, where the document is never served. */
|
|
181
|
+
| 'production';
|
|
182
|
+
/** What {@link applyBymaxOpenApi} did, so a caller can assert on it. */
|
|
183
|
+
interface OpenApiMountOutcome {
|
|
184
|
+
/** Whether the document and its UI were mounted. */
|
|
185
|
+
mounted: boolean;
|
|
186
|
+
/** Present only when `mounted` is `false`. */
|
|
187
|
+
reason?: OpenApiSkipReason;
|
|
188
|
+
/** The route the UI was mounted at. Present only when `mounted` is `true`. */
|
|
189
|
+
path?: string;
|
|
190
|
+
/**
|
|
191
|
+
* Component entries a contributor offered that the served document already
|
|
192
|
+
* defined with a shape of its own. Present only when `mounted` is `true`;
|
|
193
|
+
* empty when every contribution was placed.
|
|
194
|
+
*
|
|
195
|
+
* The merge prefers whatever holds the name already, so a collision is not an
|
|
196
|
+
* error and does not stop the boot. The holder is usually the application's
|
|
197
|
+
* own declaration, but it may be a core schema this package contributes under
|
|
198
|
+
* `openapi.includeCoreSchemas` or an earlier contributor's entry — the report
|
|
199
|
+
* names what was dropped and by whom, not who owns the winning definition.
|
|
200
|
+
*
|
|
201
|
+
* It is reported because it is otherwise unobservable from either side: the
|
|
202
|
+
* contributing library cannot tell its entry was dropped, and the application
|
|
203
|
+
* cannot tell one was offered.
|
|
204
|
+
*/
|
|
205
|
+
discarded?: readonly DiscardedComponent[];
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Build and mount the OpenAPI document and its interactive UI, when the
|
|
209
|
+
* configuration and the runtime both allow it.
|
|
210
|
+
*
|
|
211
|
+
* Call it once, after `NestFactory.create` and BEFORE the application starts
|
|
212
|
+
* listening. The ordering is not a style preference: mounting the document
|
|
213
|
+
* re-registers routes on the HTTP adapter, and on Express 5 doing that against
|
|
214
|
+
* an already-initialized application replaces the router — every route the
|
|
215
|
+
* application had, including its own controllers and this package's health
|
|
216
|
+
* endpoints, stops resolving. `app.listen()` performs that initialization, so
|
|
217
|
+
* "before listening" is the whole rule.
|
|
218
|
+
*
|
|
219
|
+
* It is safe to call unconditionally: with the feature disabled, or in
|
|
220
|
+
* production, it mounts nothing, loads no optional peer, and returns why.
|
|
221
|
+
*
|
|
222
|
+
* Testing this under Jest needs one flag. `@nestjs/swagger` is loaded through a
|
|
223
|
+
* dynamic `import()`, which is what keeps the peer optional for everyone who
|
|
224
|
+
* never enables the document — and Jest's module registry cannot service a
|
|
225
|
+
* dynamic import without `NODE_OPTIONS=--experimental-vm-modules`. Without it,
|
|
226
|
+
* only the *enabled* path fails, with `dynamic import callback invoked without
|
|
227
|
+
* --experimental-vm-modules`; the disabled and production paths never reach the
|
|
228
|
+
* loader and pass either way, which is what makes the omission confusing.
|
|
229
|
+
*
|
|
230
|
+
* @param app - The created Nest application, not yet listening.
|
|
231
|
+
* @returns What happened: mounted, or skipped with a reason.
|
|
232
|
+
* @throws Error When `BymaxCoreModule` is not registered, when the feature is
|
|
233
|
+
* enabled and the optional peer `@nestjs/swagger` is not installed, or when
|
|
234
|
+
* `openapi.operationSecurity` addresses an operation the generated document
|
|
235
|
+
* does not contain.
|
|
236
|
+
* @example
|
|
237
|
+
* const app = await NestFactory.create(AppModule)
|
|
238
|
+
* await applyBymaxOpenApi(app)
|
|
239
|
+
* await app.listen(3000)
|
|
240
|
+
*/
|
|
241
|
+
declare function applyBymaxOpenApi(app: INestApplication): Promise<OpenApiMountOutcome>;
|
|
242
|
+
|
|
243
|
+
export { BYMAX_OPENAPI_CONTRACT_VERSION, BYMAX_OPENAPI_CONTRIBUTOR_METADATA, BymaxOpenApiContributor, type DiscardedComponent, type IOpenApiContributor, type OpenApiFragment, type OpenApiFragmentObject, type OpenApiHandlerKey, type OpenApiMountOutcome, type OpenApiSkipReason, applyBymaxOpenApi };
|
package/dist/openapi/index.d.ts
CHANGED
|
@@ -1,55 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
/** Why the document was not mounted. */
|
|
4
|
-
type OpenApiSkipReason =
|
|
5
|
-
/** The consumer never enabled the feature. */
|
|
6
|
-
'disabled'
|
|
7
|
-
/** The runtime is production, where the document is never served. */
|
|
8
|
-
| 'production';
|
|
9
|
-
/** What {@link applyBymaxOpenApi} did, so a caller can assert on it. */
|
|
10
|
-
interface OpenApiMountOutcome {
|
|
11
|
-
/** Whether the document and its UI were mounted. */
|
|
12
|
-
mounted: boolean;
|
|
13
|
-
/** Present only when `mounted` is `false`. */
|
|
14
|
-
reason?: OpenApiSkipReason;
|
|
15
|
-
/** The route the UI was mounted at. Present only when `mounted` is `true`. */
|
|
16
|
-
path?: string;
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Build and mount the OpenAPI document and its interactive UI, when the
|
|
20
|
-
* configuration and the runtime both allow it.
|
|
21
|
-
*
|
|
22
|
-
* Call it once, after `NestFactory.create` and BEFORE the application starts
|
|
23
|
-
* listening. The ordering is not a style preference: mounting the document
|
|
24
|
-
* re-registers routes on the HTTP adapter, and on Express 5 doing that against
|
|
25
|
-
* an already-initialized application replaces the router — every route the
|
|
26
|
-
* application had, including its own controllers and this package's health
|
|
27
|
-
* endpoints, stops resolving. `app.listen()` performs that initialization, so
|
|
28
|
-
* "before listening" is the whole rule.
|
|
29
|
-
*
|
|
30
|
-
* It is safe to call unconditionally: with the feature disabled, or in
|
|
31
|
-
* production, it mounts nothing, loads no optional peer, and returns why.
|
|
32
|
-
*
|
|
33
|
-
* Testing this under Jest needs one flag. `@nestjs/swagger` is loaded through a
|
|
34
|
-
* dynamic `import()`, which is what keeps the peer optional for everyone who
|
|
35
|
-
* never enables the document — and Jest's module registry cannot service a
|
|
36
|
-
* dynamic import without `NODE_OPTIONS=--experimental-vm-modules`. Without it,
|
|
37
|
-
* only the *enabled* path fails, with `dynamic import callback invoked without
|
|
38
|
-
* --experimental-vm-modules`; the disabled and production paths never reach the
|
|
39
|
-
* loader and pass either way, which is what makes the omission confusing.
|
|
40
|
-
*
|
|
41
|
-
* @param app - The created Nest application, not yet listening.
|
|
42
|
-
* @returns What happened: mounted, or skipped with a reason.
|
|
43
|
-
* @throws Error When `BymaxCoreModule` is not registered, when the feature is
|
|
44
|
-
* enabled and the optional peer `@nestjs/swagger` is not installed, or when
|
|
45
|
-
* `openapi.operationSecurity` addresses an operation the generated document
|
|
46
|
-
* does not contain.
|
|
47
|
-
* @example
|
|
48
|
-
* const app = await NestFactory.create(AppModule)
|
|
49
|
-
* await applyBymaxOpenApi(app)
|
|
50
|
-
* await app.listen(3000)
|
|
51
|
-
*/
|
|
52
|
-
declare function applyBymaxOpenApi(app: INestApplication): Promise<OpenApiMountOutcome>;
|
|
1
|
+
import { CustomDecorator, INestApplication } from '@nestjs/common';
|
|
53
2
|
|
|
54
3
|
/**
|
|
55
4
|
* Addresses one route handler, as `'<ControllerClassName>.<methodName>'`.
|
|
@@ -196,4 +145,99 @@ declare const BYMAX_OPENAPI_CONTRIBUTOR_METADATA = "bymax-one:openapi-contributo
|
|
|
196
145
|
*/
|
|
197
146
|
declare function BymaxOpenApiContributor(): CustomDecorator<string>;
|
|
198
147
|
|
|
199
|
-
|
|
148
|
+
/**
|
|
149
|
+
* @fileoverview Placing offered entries — this package's catalogue and every
|
|
150
|
+
* library contribution — into a document's `components`, and reporting what a
|
|
151
|
+
* contributor could not place.
|
|
152
|
+
*
|
|
153
|
+
* A module of its own because the rule here is not the document's rule: everywhere else the merge is "the existing side wins", and this is the
|
|
154
|
+
* one place that asks whether the existing side says anything at all. Keeping
|
|
155
|
+
* the question and its narrow carve-out together makes the exception legible as
|
|
156
|
+
* an exception.
|
|
157
|
+
* @layer internal
|
|
158
|
+
*/
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* One component entry a contributor offered and the document already declared.
|
|
162
|
+
*
|
|
163
|
+
* Carries the contributor's label because a collision is only actionable if you
|
|
164
|
+
* know who lost it: an application with several contributing libraries would
|
|
165
|
+
* otherwise be told a name was dropped without being told whose it was.
|
|
166
|
+
*/
|
|
167
|
+
interface DiscardedComponent {
|
|
168
|
+
/** The contributing class, as {@link ResolvedContribution.label} names it. */
|
|
169
|
+
readonly contributor: string;
|
|
170
|
+
/** The component member, such as `schemas` or `parameters`. */
|
|
171
|
+
readonly member: string;
|
|
172
|
+
/** The entry's name within that member. */
|
|
173
|
+
readonly name: string;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Why the document was not mounted. */
|
|
177
|
+
type OpenApiSkipReason =
|
|
178
|
+
/** The consumer never enabled the feature. */
|
|
179
|
+
'disabled'
|
|
180
|
+
/** The runtime is production, where the document is never served. */
|
|
181
|
+
| 'production';
|
|
182
|
+
/** What {@link applyBymaxOpenApi} did, so a caller can assert on it. */
|
|
183
|
+
interface OpenApiMountOutcome {
|
|
184
|
+
/** Whether the document and its UI were mounted. */
|
|
185
|
+
mounted: boolean;
|
|
186
|
+
/** Present only when `mounted` is `false`. */
|
|
187
|
+
reason?: OpenApiSkipReason;
|
|
188
|
+
/** The route the UI was mounted at. Present only when `mounted` is `true`. */
|
|
189
|
+
path?: string;
|
|
190
|
+
/**
|
|
191
|
+
* Component entries a contributor offered that the served document already
|
|
192
|
+
* defined with a shape of its own. Present only when `mounted` is `true`;
|
|
193
|
+
* empty when every contribution was placed.
|
|
194
|
+
*
|
|
195
|
+
* The merge prefers whatever holds the name already, so a collision is not an
|
|
196
|
+
* error and does not stop the boot. The holder is usually the application's
|
|
197
|
+
* own declaration, but it may be a core schema this package contributes under
|
|
198
|
+
* `openapi.includeCoreSchemas` or an earlier contributor's entry — the report
|
|
199
|
+
* names what was dropped and by whom, not who owns the winning definition.
|
|
200
|
+
*
|
|
201
|
+
* It is reported because it is otherwise unobservable from either side: the
|
|
202
|
+
* contributing library cannot tell its entry was dropped, and the application
|
|
203
|
+
* cannot tell one was offered.
|
|
204
|
+
*/
|
|
205
|
+
discarded?: readonly DiscardedComponent[];
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Build and mount the OpenAPI document and its interactive UI, when the
|
|
209
|
+
* configuration and the runtime both allow it.
|
|
210
|
+
*
|
|
211
|
+
* Call it once, after `NestFactory.create` and BEFORE the application starts
|
|
212
|
+
* listening. The ordering is not a style preference: mounting the document
|
|
213
|
+
* re-registers routes on the HTTP adapter, and on Express 5 doing that against
|
|
214
|
+
* an already-initialized application replaces the router — every route the
|
|
215
|
+
* application had, including its own controllers and this package's health
|
|
216
|
+
* endpoints, stops resolving. `app.listen()` performs that initialization, so
|
|
217
|
+
* "before listening" is the whole rule.
|
|
218
|
+
*
|
|
219
|
+
* It is safe to call unconditionally: with the feature disabled, or in
|
|
220
|
+
* production, it mounts nothing, loads no optional peer, and returns why.
|
|
221
|
+
*
|
|
222
|
+
* Testing this under Jest needs one flag. `@nestjs/swagger` is loaded through a
|
|
223
|
+
* dynamic `import()`, which is what keeps the peer optional for everyone who
|
|
224
|
+
* never enables the document — and Jest's module registry cannot service a
|
|
225
|
+
* dynamic import without `NODE_OPTIONS=--experimental-vm-modules`. Without it,
|
|
226
|
+
* only the *enabled* path fails, with `dynamic import callback invoked without
|
|
227
|
+
* --experimental-vm-modules`; the disabled and production paths never reach the
|
|
228
|
+
* loader and pass either way, which is what makes the omission confusing.
|
|
229
|
+
*
|
|
230
|
+
* @param app - The created Nest application, not yet listening.
|
|
231
|
+
* @returns What happened: mounted, or skipped with a reason.
|
|
232
|
+
* @throws Error When `BymaxCoreModule` is not registered, when the feature is
|
|
233
|
+
* enabled and the optional peer `@nestjs/swagger` is not installed, or when
|
|
234
|
+
* `openapi.operationSecurity` addresses an operation the generated document
|
|
235
|
+
* does not contain.
|
|
236
|
+
* @example
|
|
237
|
+
* const app = await NestFactory.create(AppModule)
|
|
238
|
+
* await applyBymaxOpenApi(app)
|
|
239
|
+
* await app.listen(3000)
|
|
240
|
+
*/
|
|
241
|
+
declare function applyBymaxOpenApi(app: INestApplication): Promise<OpenApiMountOutcome>;
|
|
242
|
+
|
|
243
|
+
export { BYMAX_OPENAPI_CONTRACT_VERSION, BYMAX_OPENAPI_CONTRIBUTOR_METADATA, BymaxOpenApiContributor, type DiscardedComponent, type IOpenApiContributor, type OpenApiFragment, type OpenApiFragmentObject, type OpenApiHandlerKey, type OpenApiMountOutcome, type OpenApiSkipReason, applyBymaxOpenApi };
|