@bymax-one/nest-core 1.0.1 → 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.
- package/CHANGELOG.md +121 -1
- package/README.md +354 -54
- package/dist/health/index.cjs +10 -0
- package/dist/health/index.d.cts +33 -1
- package/dist/health/index.d.ts +33 -1
- package/dist/health/index.mjs +8 -0
- package/dist/index.cjs +406 -31
- package/dist/index.d.cts +224 -11
- package/dist/index.d.ts +224 -11
- package/dist/index.mjs +407 -33
- package/dist/metrics/index.cjs +12 -0
- package/dist/metrics/index.d.cts +57 -0
- package/dist/metrics/index.d.ts +57 -0
- package/dist/metrics/index.mjs +9 -0
- package/dist/openapi/index.cjs +268 -0
- package/dist/openapi/index.d.cts +44 -0
- package/dist/openapi/index.d.ts +44 -0
- package/dist/openapi/index.mjs +266 -0
- package/package.json +46 -16
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { CustomDecorator } from '@nestjs/common';
|
|
2
|
+
import * as PromClient from 'prom-client';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The dedicated registry backing the application's scrape endpoint. Collectors
|
|
6
|
+
* registered against it appear at `GET /metrics`; nothing else does.
|
|
7
|
+
*/
|
|
8
|
+
type MetricsRegistry = PromClient.Registry;
|
|
9
|
+
/**
|
|
10
|
+
* A component that publishes its own metrics.
|
|
11
|
+
*
|
|
12
|
+
* Implement it on a provider, mark the class, and the module calls it once at
|
|
13
|
+
* bootstrap with the registry the scrape endpoint serves. Registration failures
|
|
14
|
+
* — most often a metric name another component already claimed — fail the boot
|
|
15
|
+
* with the contributor named, rather than surfacing at the first scrape.
|
|
16
|
+
*/
|
|
17
|
+
interface IMetricsContributor {
|
|
18
|
+
/**
|
|
19
|
+
* Register this component's collectors against the shared registry.
|
|
20
|
+
*
|
|
21
|
+
* Called exactly once, during application bootstrap, and only when the metrics
|
|
22
|
+
* feature is enabled. Construct collectors with `registers: [registry]` (or
|
|
23
|
+
* call `registry.registerMetric`); do not create metrics on the global default
|
|
24
|
+
* registry, which this package never scrapes.
|
|
25
|
+
*
|
|
26
|
+
* @param registry - The registry the scrape endpoint serves.
|
|
27
|
+
*/
|
|
28
|
+
registerMetrics(registry: MetricsRegistry): void;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Reflect metadata key carrying the contributor marker. Namespaced so it cannot
|
|
32
|
+
* collide with a consumer's own metadata, and exported so a conformance test can
|
|
33
|
+
* assert a class is marked without depending on how the decorator is built.
|
|
34
|
+
*/
|
|
35
|
+
declare const BYMAX_METRICS_CONTRIBUTOR_METADATA = "bymax-one:metrics-contributor";
|
|
36
|
+
/**
|
|
37
|
+
* Mark a provider class as a metrics contributor, so `BymaxCoreModule` calls it
|
|
38
|
+
* at bootstrap without the application wiring anything.
|
|
39
|
+
*
|
|
40
|
+
* The class must implement {@link IMetricsContributor}; a marked provider that
|
|
41
|
+
* does not fails at bootstrap with a message naming it. Contributors run only
|
|
42
|
+
* when the metrics feature is enabled — marking a class in an application that
|
|
43
|
+
* leaves metrics off costs one metadata entry and changes nothing.
|
|
44
|
+
*
|
|
45
|
+
* @returns The class decorator carrying the marker.
|
|
46
|
+
* @example
|
|
47
|
+
* \@BymaxMetricsContributor()
|
|
48
|
+
* \@Injectable()
|
|
49
|
+
* export class QueueMetrics implements IMetricsContributor {
|
|
50
|
+
* registerMetrics(registry: MetricsRegistry): void {
|
|
51
|
+
* new Gauge({ name: 'bymax_queue_depth', help: 'Jobs waiting', registers: [registry] })
|
|
52
|
+
* }
|
|
53
|
+
* }
|
|
54
|
+
*/
|
|
55
|
+
declare function BymaxMetricsContributor(): CustomDecorator<string>;
|
|
56
|
+
|
|
57
|
+
export { BYMAX_METRICS_CONTRIBUTOR_METADATA, BymaxMetricsContributor, type IMetricsContributor, type MetricsRegistry };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { CustomDecorator } from '@nestjs/common';
|
|
2
|
+
import * as PromClient from 'prom-client';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The dedicated registry backing the application's scrape endpoint. Collectors
|
|
6
|
+
* registered against it appear at `GET /metrics`; nothing else does.
|
|
7
|
+
*/
|
|
8
|
+
type MetricsRegistry = PromClient.Registry;
|
|
9
|
+
/**
|
|
10
|
+
* A component that publishes its own metrics.
|
|
11
|
+
*
|
|
12
|
+
* Implement it on a provider, mark the class, and the module calls it once at
|
|
13
|
+
* bootstrap with the registry the scrape endpoint serves. Registration failures
|
|
14
|
+
* — most often a metric name another component already claimed — fail the boot
|
|
15
|
+
* with the contributor named, rather than surfacing at the first scrape.
|
|
16
|
+
*/
|
|
17
|
+
interface IMetricsContributor {
|
|
18
|
+
/**
|
|
19
|
+
* Register this component's collectors against the shared registry.
|
|
20
|
+
*
|
|
21
|
+
* Called exactly once, during application bootstrap, and only when the metrics
|
|
22
|
+
* feature is enabled. Construct collectors with `registers: [registry]` (or
|
|
23
|
+
* call `registry.registerMetric`); do not create metrics on the global default
|
|
24
|
+
* registry, which this package never scrapes.
|
|
25
|
+
*
|
|
26
|
+
* @param registry - The registry the scrape endpoint serves.
|
|
27
|
+
*/
|
|
28
|
+
registerMetrics(registry: MetricsRegistry): void;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Reflect metadata key carrying the contributor marker. Namespaced so it cannot
|
|
32
|
+
* collide with a consumer's own metadata, and exported so a conformance test can
|
|
33
|
+
* assert a class is marked without depending on how the decorator is built.
|
|
34
|
+
*/
|
|
35
|
+
declare const BYMAX_METRICS_CONTRIBUTOR_METADATA = "bymax-one:metrics-contributor";
|
|
36
|
+
/**
|
|
37
|
+
* Mark a provider class as a metrics contributor, so `BymaxCoreModule` calls it
|
|
38
|
+
* at bootstrap without the application wiring anything.
|
|
39
|
+
*
|
|
40
|
+
* The class must implement {@link IMetricsContributor}; a marked provider that
|
|
41
|
+
* does not fails at bootstrap with a message naming it. Contributors run only
|
|
42
|
+
* when the metrics feature is enabled — marking a class in an application that
|
|
43
|
+
* leaves metrics off costs one metadata entry and changes nothing.
|
|
44
|
+
*
|
|
45
|
+
* @returns The class decorator carrying the marker.
|
|
46
|
+
* @example
|
|
47
|
+
* \@BymaxMetricsContributor()
|
|
48
|
+
* \@Injectable()
|
|
49
|
+
* export class QueueMetrics implements IMetricsContributor {
|
|
50
|
+
* registerMetrics(registry: MetricsRegistry): void {
|
|
51
|
+
* new Gauge({ name: 'bymax_queue_depth', help: 'Jobs waiting', registers: [registry] })
|
|
52
|
+
* }
|
|
53
|
+
* }
|
|
54
|
+
*/
|
|
55
|
+
declare function BymaxMetricsContributor(): CustomDecorator<string>;
|
|
56
|
+
|
|
57
|
+
export { BYMAX_METRICS_CONTRIBUTOR_METADATA, BymaxMetricsContributor, type IMetricsContributor, type MetricsRegistry };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { SetMetadata } from '@nestjs/common';
|
|
2
|
+
|
|
3
|
+
// src/metrics/metrics.contract.ts
|
|
4
|
+
var BYMAX_METRICS_CONTRIBUTOR_METADATA = "bymax-one:metrics-contributor";
|
|
5
|
+
function BymaxMetricsContributor() {
|
|
6
|
+
return SetMetadata(BYMAX_METRICS_CONTRIBUTOR_METADATA, true);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export { BYMAX_METRICS_CONTRIBUTOR_METADATA, BymaxMetricsContributor };
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var common = require('@nestjs/common');
|
|
4
|
+
|
|
5
|
+
// src/openapi/openapi.bootstrap.ts
|
|
6
|
+
|
|
7
|
+
// src/core.tokens.ts
|
|
8
|
+
var BYMAX_CORE_OPTIONS = /* @__PURE__ */ Symbol("BYMAX_CORE_OPTIONS");
|
|
9
|
+
|
|
10
|
+
// src/runtime.environment.ts
|
|
11
|
+
var NON_PRODUCTION_ENVIRONMENTS = /* @__PURE__ */ new Set(["development", "test"]);
|
|
12
|
+
function isProductionRuntime(value = process.env["NODE_ENV"]) {
|
|
13
|
+
if (value === void 0) {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
return !NON_PRODUCTION_ENVIRONMENTS.has(value.trim().toLowerCase());
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// src/envelope/error-codes.ts
|
|
20
|
+
var BYMAX_BAD_REQUEST = "BYMAX_BAD_REQUEST";
|
|
21
|
+
var BYMAX_VALIDATION_FAILED = "BYMAX_VALIDATION_FAILED";
|
|
22
|
+
var BYMAX_UNAUTHORIZED = "BYMAX_UNAUTHORIZED";
|
|
23
|
+
var BYMAX_FORBIDDEN = "BYMAX_FORBIDDEN";
|
|
24
|
+
var BYMAX_NOT_FOUND = "BYMAX_NOT_FOUND";
|
|
25
|
+
var BYMAX_CONFLICT = "BYMAX_CONFLICT";
|
|
26
|
+
var BYMAX_PAYLOAD_TOO_LARGE = "BYMAX_PAYLOAD_TOO_LARGE";
|
|
27
|
+
var BYMAX_UNSUPPORTED_MEDIA_TYPE = "BYMAX_UNSUPPORTED_MEDIA_TYPE";
|
|
28
|
+
var BYMAX_UNPROCESSABLE_ENTITY = "BYMAX_UNPROCESSABLE_ENTITY";
|
|
29
|
+
var BYMAX_TOO_MANY_REQUESTS = "BYMAX_TOO_MANY_REQUESTS";
|
|
30
|
+
var BYMAX_CLIENT_ERROR = "BYMAX_CLIENT_ERROR";
|
|
31
|
+
var BYMAX_INTERNAL_ERROR = "BYMAX_INTERNAL_ERROR";
|
|
32
|
+
var BYMAX_NOT_IMPLEMENTED = "BYMAX_NOT_IMPLEMENTED";
|
|
33
|
+
var BYMAX_BAD_GATEWAY = "BYMAX_BAD_GATEWAY";
|
|
34
|
+
var BYMAX_SERVICE_UNAVAILABLE = "BYMAX_SERVICE_UNAVAILABLE";
|
|
35
|
+
var BYMAX_GATEWAY_TIMEOUT = "BYMAX_GATEWAY_TIMEOUT";
|
|
36
|
+
|
|
37
|
+
// src/openapi/openapi.schemas.ts
|
|
38
|
+
var ERROR_CODES = [
|
|
39
|
+
BYMAX_BAD_REQUEST,
|
|
40
|
+
BYMAX_VALIDATION_FAILED,
|
|
41
|
+
BYMAX_UNAUTHORIZED,
|
|
42
|
+
BYMAX_FORBIDDEN,
|
|
43
|
+
BYMAX_NOT_FOUND,
|
|
44
|
+
BYMAX_CONFLICT,
|
|
45
|
+
BYMAX_PAYLOAD_TOO_LARGE,
|
|
46
|
+
BYMAX_UNSUPPORTED_MEDIA_TYPE,
|
|
47
|
+
BYMAX_UNPROCESSABLE_ENTITY,
|
|
48
|
+
BYMAX_TOO_MANY_REQUESTS,
|
|
49
|
+
BYMAX_CLIENT_ERROR,
|
|
50
|
+
BYMAX_INTERNAL_ERROR,
|
|
51
|
+
BYMAX_NOT_IMPLEMENTED,
|
|
52
|
+
BYMAX_BAD_GATEWAY,
|
|
53
|
+
BYMAX_SERVICE_UNAVAILABLE,
|
|
54
|
+
BYMAX_GATEWAY_TIMEOUT
|
|
55
|
+
];
|
|
56
|
+
var CORE_SCHEMAS = {
|
|
57
|
+
BymaxErrorCode: {
|
|
58
|
+
type: "string",
|
|
59
|
+
enum: ERROR_CODES,
|
|
60
|
+
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."
|
|
61
|
+
},
|
|
62
|
+
BymaxErrorDetails: {
|
|
63
|
+
description: "Structured error context. The array form carries one entry per validation violation; the object form carries the development-only internals dump.",
|
|
64
|
+
oneOf: [
|
|
65
|
+
{ type: "array", items: {} },
|
|
66
|
+
{ type: "object", additionalProperties: true }
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
BymaxErrorEnvelope: {
|
|
70
|
+
type: "object",
|
|
71
|
+
description: "The shape of every error response served by this application.",
|
|
72
|
+
required: ["statusCode", "code", "message", "timestamp", "path"],
|
|
73
|
+
properties: {
|
|
74
|
+
statusCode: { type: "integer", example: 404 },
|
|
75
|
+
code: {
|
|
76
|
+
type: "string",
|
|
77
|
+
example: BYMAX_NOT_FOUND,
|
|
78
|
+
description: "A code from BymaxErrorCode, or a domain code passed through unchanged."
|
|
79
|
+
},
|
|
80
|
+
message: { type: "string", description: "Human-readable and safe to show end users." },
|
|
81
|
+
details: { $ref: "#/components/schemas/BymaxErrorDetails" },
|
|
82
|
+
correlationId: {
|
|
83
|
+
type: "string",
|
|
84
|
+
description: "Present only when a correlation provider resolves an id."
|
|
85
|
+
},
|
|
86
|
+
timestamp: { type: "string", format: "date-time" },
|
|
87
|
+
path: { type: "string", example: "/invoices/42" }
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
BymaxHealthCheckEntry: {
|
|
91
|
+
type: "object",
|
|
92
|
+
description: "One indicator's result within a readiness response.",
|
|
93
|
+
required: ["name", "status"],
|
|
94
|
+
properties: {
|
|
95
|
+
name: { type: "string", example: "redis" },
|
|
96
|
+
status: { type: "string", enum: ["up", "down"] },
|
|
97
|
+
details: {
|
|
98
|
+
type: "object",
|
|
99
|
+
additionalProperties: true,
|
|
100
|
+
description: "Safe diagnostic context. Never carries secrets or connection strings."
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
BymaxHealthResponse: {
|
|
105
|
+
type: "object",
|
|
106
|
+
description: "The body served by the liveness and readiness endpoints.",
|
|
107
|
+
required: ["status", "checks"],
|
|
108
|
+
properties: {
|
|
109
|
+
status: {
|
|
110
|
+
type: "string",
|
|
111
|
+
enum: ["ok", "error"],
|
|
112
|
+
description: "'ok' only when every check is up. Liveness is always 'ok'."
|
|
113
|
+
},
|
|
114
|
+
checks: {
|
|
115
|
+
type: "array",
|
|
116
|
+
items: { $ref: "#/components/schemas/BymaxHealthCheckEntry" },
|
|
117
|
+
description: "Empty for the liveness endpoint."
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
BymaxPageMeta: {
|
|
122
|
+
type: "object",
|
|
123
|
+
description: "Offset-pagination metadata.",
|
|
124
|
+
required: ["page", "limit", "totalItems", "totalPages"],
|
|
125
|
+
properties: {
|
|
126
|
+
page: { type: "integer", minimum: 1, example: 1 },
|
|
127
|
+
limit: { type: "integer", minimum: 1, example: 20 },
|
|
128
|
+
totalItems: { type: "integer", minimum: 0, example: 137 },
|
|
129
|
+
totalPages: { type: "integer", minimum: 0, example: 7 }
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
BymaxPageResult: {
|
|
133
|
+
type: "object",
|
|
134
|
+
description: "An offset-paginated page. Compose it with a concrete item schema by overriding `items`.",
|
|
135
|
+
required: ["items", "meta"],
|
|
136
|
+
properties: {
|
|
137
|
+
items: { type: "array", items: {} },
|
|
138
|
+
meta: { $ref: "#/components/schemas/BymaxPageMeta" }
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
BymaxCursorResult: {
|
|
142
|
+
type: "object",
|
|
143
|
+
description: "A cursor-paginated page. Compose it with a concrete item schema by overriding `items`.",
|
|
144
|
+
required: ["items", "nextCursor"],
|
|
145
|
+
properties: {
|
|
146
|
+
items: { type: "array", items: {} },
|
|
147
|
+
nextCursor: {
|
|
148
|
+
type: "string",
|
|
149
|
+
nullable: true,
|
|
150
|
+
description: "Opaque cursor for the next page, or null when the last page was reached."
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
var CORE_PARAMETERS = {
|
|
156
|
+
BymaxPageQueryPage: {
|
|
157
|
+
name: "page",
|
|
158
|
+
in: "query",
|
|
159
|
+
required: false,
|
|
160
|
+
description: "1-based page number. Out-of-range and non-numeric input is clamped.",
|
|
161
|
+
schema: { type: "integer", minimum: 1, default: 1 }
|
|
162
|
+
},
|
|
163
|
+
BymaxPageQueryLimit: {
|
|
164
|
+
name: "limit",
|
|
165
|
+
in: "query",
|
|
166
|
+
required: false,
|
|
167
|
+
description: "Items per page. Clamped to the range this application configures.",
|
|
168
|
+
schema: { type: "integer", minimum: 1, default: 20 }
|
|
169
|
+
},
|
|
170
|
+
BymaxCursorQueryCursor: {
|
|
171
|
+
name: "cursor",
|
|
172
|
+
in: "query",
|
|
173
|
+
required: false,
|
|
174
|
+
description: "Opaque cursor from a previous response. Omit to request the first page.",
|
|
175
|
+
schema: { type: "string" }
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// src/openapi/openapi.document.ts
|
|
180
|
+
function asRecord(value) {
|
|
181
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
182
|
+
return {};
|
|
183
|
+
}
|
|
184
|
+
return value;
|
|
185
|
+
}
|
|
186
|
+
function mergeAbsent(existing, additions) {
|
|
187
|
+
return { ...additions, ...existing };
|
|
188
|
+
}
|
|
189
|
+
function augmentDocument(document, options) {
|
|
190
|
+
const components = asRecord(document.components);
|
|
191
|
+
const merged = { ...components };
|
|
192
|
+
if (options.includeCoreSchemas) {
|
|
193
|
+
merged["schemas"] = mergeAbsent(asRecord(components["schemas"]), CORE_SCHEMAS);
|
|
194
|
+
merged["parameters"] = mergeAbsent(asRecord(components["parameters"]), CORE_PARAMETERS);
|
|
195
|
+
}
|
|
196
|
+
const securitySchemeNames = Object.keys(options.securitySchemes);
|
|
197
|
+
if (securitySchemeNames.length > 0) {
|
|
198
|
+
merged["securitySchemes"] = mergeAbsent(
|
|
199
|
+
asRecord(components["securitySchemes"]),
|
|
200
|
+
options.securitySchemes
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
return { ...document, components: merged };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/optional-peer.ts
|
|
207
|
+
function isMissingModuleError(cause) {
|
|
208
|
+
const code = cause.code;
|
|
209
|
+
return code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND";
|
|
210
|
+
}
|
|
211
|
+
function missingPeerMessage(option, peer) {
|
|
212
|
+
return `${option} is true but the optional peer ${peer} is not installed. Run: pnpm add ${peer}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// src/openapi/openapi.loader.ts
|
|
216
|
+
var MISSING_PEER_MESSAGE = missingPeerMessage("openapi.enabled", "@nestjs/swagger");
|
|
217
|
+
async function loadSwagger() {
|
|
218
|
+
try {
|
|
219
|
+
return await import('@nestjs/swagger');
|
|
220
|
+
} catch (cause) {
|
|
221
|
+
if (isMissingModuleError(cause)) {
|
|
222
|
+
throw new Error(MISSING_PEER_MESSAGE, { cause });
|
|
223
|
+
}
|
|
224
|
+
throw cause;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/openapi/openapi.bootstrap.ts
|
|
229
|
+
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.";
|
|
230
|
+
function resolveCoreOptions(app) {
|
|
231
|
+
try {
|
|
232
|
+
return app.get(BYMAX_CORE_OPTIONS);
|
|
233
|
+
} catch (cause) {
|
|
234
|
+
throw new Error(OPTIONS_UNRESOLVED_MESSAGE, { cause });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function buildConfig(builder, options) {
|
|
238
|
+
builder.setTitle(options.title).setDescription(options.description).setVersion(options.version);
|
|
239
|
+
for (const server of options.servers) {
|
|
240
|
+
builder.addServer(server.url, server.description);
|
|
241
|
+
}
|
|
242
|
+
return builder.build();
|
|
243
|
+
}
|
|
244
|
+
async function applyBymaxOpenApi(app) {
|
|
245
|
+
const logger = new common.Logger("BymaxCoreModule");
|
|
246
|
+
const options = resolveCoreOptions(app).openapi;
|
|
247
|
+
if (isProductionRuntime()) {
|
|
248
|
+
if (options.suppressedInProduction || options.enabled) {
|
|
249
|
+
logger.warn(
|
|
250
|
+
'openapi.enabled was requested but the OpenAPI document is never served in production. Set NODE_ENV to "development" or "test" to serve it.'
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
return { mounted: false, reason: "production" };
|
|
254
|
+
}
|
|
255
|
+
if (!options.enabled) {
|
|
256
|
+
return { mounted: false, reason: "disabled" };
|
|
257
|
+
}
|
|
258
|
+
const swagger = await loadSwagger();
|
|
259
|
+
const config = buildConfig(new swagger.DocumentBuilder(), options);
|
|
260
|
+
const document = augmentDocument(swagger.SwaggerModule.createDocument(app, config), options);
|
|
261
|
+
swagger.SwaggerModule.setup(options.path, app, document, {
|
|
262
|
+
jsonDocumentUrl: options.jsonPath
|
|
263
|
+
});
|
|
264
|
+
logger.log(`OpenAPI document served at "/${options.path}" (JSON at "/${options.jsonPath}")`);
|
|
265
|
+
return { mounted: true, path: options.path };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
exports.applyBymaxOpenApi = applyBymaxOpenApi;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { INestApplication } from '@nestjs/common';
|
|
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
|
+
* @param app - The created Nest application, not yet listening.
|
|
34
|
+
* @returns What happened: mounted, or skipped with a reason.
|
|
35
|
+
* @throws Error When `BymaxCoreModule` is not registered, or when the feature is
|
|
36
|
+
* enabled and the optional peer `@nestjs/swagger` is not installed.
|
|
37
|
+
* @example
|
|
38
|
+
* const app = await NestFactory.create(AppModule)
|
|
39
|
+
* await applyBymaxOpenApi(app)
|
|
40
|
+
* await app.listen(3000)
|
|
41
|
+
*/
|
|
42
|
+
declare function applyBymaxOpenApi(app: INestApplication): Promise<OpenApiMountOutcome>;
|
|
43
|
+
|
|
44
|
+
export { type OpenApiMountOutcome, type OpenApiSkipReason, applyBymaxOpenApi };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { INestApplication } from '@nestjs/common';
|
|
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
|
+
* @param app - The created Nest application, not yet listening.
|
|
34
|
+
* @returns What happened: mounted, or skipped with a reason.
|
|
35
|
+
* @throws Error When `BymaxCoreModule` is not registered, or when the feature is
|
|
36
|
+
* enabled and the optional peer `@nestjs/swagger` is not installed.
|
|
37
|
+
* @example
|
|
38
|
+
* const app = await NestFactory.create(AppModule)
|
|
39
|
+
* await applyBymaxOpenApi(app)
|
|
40
|
+
* await app.listen(3000)
|
|
41
|
+
*/
|
|
42
|
+
declare function applyBymaxOpenApi(app: INestApplication): Promise<OpenApiMountOutcome>;
|
|
43
|
+
|
|
44
|
+
export { type OpenApiMountOutcome, type OpenApiSkipReason, applyBymaxOpenApi };
|