@bymax-one/nest-core 1.5.3 → 1.6.1
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 +213 -1
- package/README.md +127 -13
- package/dist/health/index.d.cts +1 -0
- package/dist/health/index.d.ts +1 -0
- package/dist/health.transition-B_IgXJTz.d.cts +119 -0
- package/dist/health.transition-B_IgXJTz.d.ts +119 -0
- package/dist/index.cjs +212 -24
- package/dist/index.d.cts +23 -8
- package/dist/index.d.ts +23 -8
- package/dist/index.mjs +212 -25
- package/dist/openapi/index.cjs +108 -34
- package/dist/openapi/index.d.cts +97 -53
- package/dist/openapi/index.d.ts +97 -53
- package/dist/openapi/index.mjs +108 -34
- package/package.json +3 -3
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 };
|
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,15 @@ 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
|
-
merged.set(
|
|
594
|
+
const keep = () => void 0;
|
|
595
|
+
merged.set(
|
|
596
|
+
"schemas",
|
|
597
|
+
placeComponentEntries(components["schemas"], "schemas", CORE_SCHEMAS, keep)
|
|
598
|
+
);
|
|
599
|
+
merged.set(
|
|
600
|
+
"parameters",
|
|
601
|
+
placeComponentEntries(components["parameters"], "parameters", CORE_PARAMETERS, keep)
|
|
602
|
+
);
|
|
544
603
|
}
|
|
545
604
|
const scrapeScheme = options.metrics.authToken === void 0 ? {} : {
|
|
546
605
|
[METRICS_SCHEME_NAME]: {
|
|
@@ -557,11 +616,7 @@ function augmentDocument(document, options, pathPrefixes = [""], contributions =
|
|
|
557
616
|
if (Object.keys(declaredSchemes).length > 0) {
|
|
558
617
|
merged.set("securitySchemes", declaredSchemes);
|
|
559
618
|
}
|
|
560
|
-
|
|
561
|
-
for (const [member, entries] of Object.entries(contribution.components)) {
|
|
562
|
-
merged.set(member, mergeAbsent(asRecord(merged.get(member)), entries));
|
|
563
|
-
}
|
|
564
|
-
}
|
|
619
|
+
const discarded = mergeContributedComponents(merged, contributions);
|
|
565
620
|
assertSchemesDeclared(openapi, asRecord(merged.get("securitySchemes")));
|
|
566
621
|
const routes = indexOwnRoutes(options, pathPrefixes);
|
|
567
622
|
const served = withoutDisabledRoutes(asRecord(document.paths), options, routes);
|
|
@@ -576,7 +631,10 @@ function augmentDocument(document, options, pathPrefixes = [""], contributions =
|
|
|
576
631
|
)
|
|
577
632
|
};
|
|
578
633
|
const security = openapi.security.length > 0 && document.security === void 0 ? { security: openapi.security } : {};
|
|
579
|
-
return {
|
|
634
|
+
return {
|
|
635
|
+
document: { ...document, components: Object.fromEntries(merged), ...paths, ...security },
|
|
636
|
+
discarded
|
|
637
|
+
};
|
|
580
638
|
}
|
|
581
639
|
|
|
582
640
|
// src/optional-peer.ts
|
|
@@ -671,6 +729,16 @@ function warnUnsecuredOperations(logger, keys) {
|
|
|
671
729
|
`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
730
|
);
|
|
673
731
|
}
|
|
732
|
+
function warnDiscardedComponents(logger, discarded) {
|
|
733
|
+
if (discarded.length === 0) {
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
const elided = discarded.length - MAX_WARNED_OPERATIONS;
|
|
737
|
+
const listed = discarded.slice(0, MAX_WARNED_OPERATIONS).map(({ contributor, member, name }) => `${contributor} -> ${member}.${name}`).join(", ");
|
|
738
|
+
logger.warn(
|
|
739
|
+
`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.`
|
|
740
|
+
);
|
|
741
|
+
}
|
|
674
742
|
function buildConfig(builder, options) {
|
|
675
743
|
builder.setTitle(options.title).setDescription(options.description).setVersion(options.version);
|
|
676
744
|
for (const server of options.servers) {
|
|
@@ -700,13 +768,19 @@ async function applyBymaxOpenApi(app) {
|
|
|
700
768
|
operationIdFactory: recordingOperationIdFactory(handlers, options.operationIdFactory)
|
|
701
769
|
});
|
|
702
770
|
const prefixes = readPathPrefixes(app);
|
|
703
|
-
const document = augmentDocument(
|
|
771
|
+
const { document, discarded } = augmentDocument(
|
|
772
|
+
generated,
|
|
773
|
+
resolved,
|
|
774
|
+
prefixes,
|
|
775
|
+
readContributions(app, handlers)
|
|
776
|
+
);
|
|
704
777
|
warnUnsecuredOperations(logger, unsecuredOperations(document, resolved, prefixes));
|
|
778
|
+
warnDiscardedComponents(logger, discarded);
|
|
705
779
|
swagger.SwaggerModule.setup(options.path, app, document, {
|
|
706
780
|
jsonDocumentUrl: options.jsonPath
|
|
707
781
|
});
|
|
708
782
|
logger.log(`OpenAPI document served at "/${options.path}" (JSON at "/${options.jsonPath}")`);
|
|
709
|
-
return { mounted: true, path: options.path };
|
|
783
|
+
return { mounted: true, path: options.path, discarded };
|
|
710
784
|
}
|
|
711
785
|
|
|
712
786
|
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.
|
|
3
|
+
"version": "1.6.1",
|
|
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",
|