@fluojs/openapi 1.0.0-beta.4 → 1.0.0-beta.5
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/README.ko.md +6 -1
- package/README.md +6 -1
- package/dist/openapi-module.d.ts +8 -0
- package/dist/openapi-module.d.ts.map +1 -1
- package/dist/openapi-module.js +69 -7
- package/package.json +5 -5
package/README.ko.md
CHANGED
|
@@ -89,7 +89,10 @@ HTTP 핸들러가 `@fluojs/http`의 `@Produces(...)`를 선언하면, 생성된
|
|
|
89
89
|
같은 scheme에 대해 여러 `@ApiSecurity()` 데코레이터를 쌓으면, 해당 scheme의 scope가 하나의 누적 OpenAPI security requirement로 병합됩니다. 따라서 라우트가 `['reports:read']`와 `['reports:write', 'reports:read']`처럼 겹치는 scope를 선언해도 OAuth 스타일 요구사항은 결정적으로 유지되며, 서로 다른 scheme은 별도 requirement로 남습니다.
|
|
90
90
|
|
|
91
91
|
### 결정적인 Swagger UI 자산
|
|
92
|
-
`ui: true`를 활성화하면 생성되는 `/docs` 페이지는 정확한 `swagger-ui-dist` 버전의 자산을 참조하여 패키지 릴리스마다 동일한 동작을 유지합니다.
|
|
92
|
+
`ui: true`를 활성화하면 생성되는 `/docs` 페이지는 정확한 `swagger-ui-dist` 버전의 자산을 참조하여 패키지 릴리스마다 동일한 동작을 유지합니다. 오프라인 또는 CSP 제어 환경에서 자체 호스팅 자산이 필요하면 `swaggerUiAssets.cssUrl`과 `swaggerUiAssets.jsBundleUrl`을 설정하세요. 생성된 HTML은 해당 URL을 이스케이프하며 Swagger UI 인스턴스를 `window.ui`에 노출하지 않습니다.
|
|
93
|
+
|
|
94
|
+
### 모듈 옵션 결정성
|
|
95
|
+
`OpenApiModule.forRoot(...)`는 등록 시점에 옵션을 스냅샷하고 freeze합니다. 등록 후 원본 options 객체, `sources`, `descriptors`, `securitySchemes`, `extraModels`, `swaggerUiAssets`를 변경해도 제공되는 OpenAPI 문서나 `/docs` HTML은 바뀌지 않습니다. `OpenApiModule.forRootAsync(...)`도 async factory가 resolve된 뒤 같은 스냅샷을 적용하며, factory 실패는 bootstrap 중 전파됩니다.
|
|
93
96
|
|
|
94
97
|
## 공개 API
|
|
95
98
|
|
|
@@ -99,6 +102,8 @@ HTTP 핸들러가 `@fluojs/http`의 `@Produces(...)`를 선언하면, 생성된
|
|
|
99
102
|
- `ApiBearerAuth`, `ApiSecurity`: 보안 요구사항 데코레이터.
|
|
100
103
|
- `ApiExcludeEndpoint`: 특정 핸들러를 문서화에서 제외.
|
|
101
104
|
- `buildOpenApiDocument`: 프로그래밍 방식의 문서 빌더 (저수준).
|
|
105
|
+
- `OpenApiHandlerRegistry`: 고급 통합에서 문서 생성 전에 handler descriptor를 스냅샷하는 mutable descriptor registry.
|
|
106
|
+
- `getControllerTags`, `getMethodApiMetadata`: 고급 테스트와 통합 tooling을 위한 metadata reader.
|
|
102
107
|
- `OpenApiSchemaObject`: 명시적 `@ApiBody(...)` 및 `@ApiResponse(...)` 스키마를 위한 타입화된 스키마 표면입니다. OpenAPI 3.1 조합(`allOf`, `oneOf`, `anyOf`), 객체/배열 제약, examples/defaults, 읽기/쓰기/Deprecated 주석을 포함합니다.
|
|
103
108
|
|
|
104
109
|
## 관련 패키지
|
package/README.md
CHANGED
|
@@ -89,7 +89,10 @@ Easily document authentication requirements like Bearer tokens or API keys using
|
|
|
89
89
|
Stacking multiple `@ApiSecurity()` decorators for the same scheme merges scopes into one cumulative OpenAPI security requirement for that scheme. This keeps OAuth-style requirements deterministic when a route declares overlapping scopes such as `['reports:read']` and `['reports:write', 'reports:read']`, while different schemes remain separate requirements.
|
|
90
90
|
|
|
91
91
|
### Deterministic Swagger UI Assets
|
|
92
|
-
When `ui: true` is enabled, the generated `/docs` page references an exact `swagger-ui-dist` asset version so release behavior stays deterministic across package updates.
|
|
92
|
+
When `ui: true` is enabled, the generated `/docs` page references an exact `swagger-ui-dist` asset version so release behavior stays deterministic across package updates. If your deployment requires self-hosted assets for offline or CSP-controlled environments, set `swaggerUiAssets.cssUrl` and `swaggerUiAssets.jsBundleUrl`; the generated HTML escapes those URLs and does not expose the Swagger UI instance on `window.ui`.
|
|
93
|
+
|
|
94
|
+
### Module Option Determinism
|
|
95
|
+
`OpenApiModule.forRoot(...)` snapshots and freezes its options at registration time. Mutating the original options object, `sources`, `descriptors`, `securitySchemes`, `extraModels`, or `swaggerUiAssets` after registration does not alter the served OpenAPI document or `/docs` HTML. `OpenApiModule.forRootAsync(...)` applies the same snapshot once the async factory resolves, and factory failures propagate during bootstrap.
|
|
93
96
|
|
|
94
97
|
## Public API
|
|
95
98
|
|
|
@@ -99,6 +102,8 @@ When `ui: true` is enabled, the generated `/docs` page references an exact `swag
|
|
|
99
102
|
- `ApiBearerAuth`, `ApiSecurity`: Security requirement decorators.
|
|
100
103
|
- `ApiExcludeEndpoint`: Omit specific handlers from documentation.
|
|
101
104
|
- `buildOpenApiDocument`: Programmatic document builder (low-level).
|
|
105
|
+
- `OpenApiHandlerRegistry`: Mutable descriptor registry used by advanced integrations to snapshot handler descriptors before document generation.
|
|
106
|
+
- `getControllerTags`, `getMethodApiMetadata`: Metadata readers for advanced tests and integration tooling.
|
|
102
107
|
- `OpenApiSchemaObject`: Typed schema surface for explicit `@ApiBody(...)` and `@ApiResponse(...)` schemas, including OpenAPI 3.1 composition (`allOf`, `oneOf`, `anyOf`), object/array constraints, examples/defaults, and read/write/deprecated annotations.
|
|
103
108
|
|
|
104
109
|
## Related Packages
|
package/dist/openapi-module.d.ts
CHANGED
|
@@ -2,6 +2,13 @@ import { type HandlerDescriptor, type HandlerSource } from '@fluojs/http';
|
|
|
2
2
|
import { type AsyncModuleOptions, type Constructor } from '@fluojs/core';
|
|
3
3
|
import { type ModuleType } from '@fluojs/runtime';
|
|
4
4
|
import { type DefaultErrorResponsesPolicy, type OpenApiDocument, type OpenApiSecuritySchemeObject } from './schema-builder.js';
|
|
5
|
+
/**
|
|
6
|
+
* Asset URLs used by the generated Swagger UI HTML page.
|
|
7
|
+
*/
|
|
8
|
+
export interface OpenApiSwaggerUiAssetsOptions {
|
|
9
|
+
cssUrl?: string;
|
|
10
|
+
jsBundleUrl?: string;
|
|
11
|
+
}
|
|
5
12
|
/**
|
|
6
13
|
* Public options for `OpenApiModule.forRoot(...)` and `OpenApiModule.forRootAsync(...)`.
|
|
7
14
|
*
|
|
@@ -17,6 +24,7 @@ export interface OpenApiModuleOptions {
|
|
|
17
24
|
descriptors?: readonly HandlerDescriptor[];
|
|
18
25
|
sources?: readonly HandlerSource[];
|
|
19
26
|
securitySchemes?: Record<string, OpenApiSecuritySchemeObject>;
|
|
27
|
+
swaggerUiAssets?: OpenApiSwaggerUiAssetsOptions;
|
|
20
28
|
extraModels?: Constructor[];
|
|
21
29
|
documentTransform?: (document: OpenApiDocument) => OpenApiDocument;
|
|
22
30
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openapi-module.d.ts","sourceRoot":"","sources":["../src/openapi-module.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAEnB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAU,KAAK,kBAAkB,EAAE,KAAK,WAAW,EAAiC,MAAM,cAAc,CAAC;AAChH,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAGhE,OAAO,EAEL,KAAK,2BAA2B,EAChC,KAAK,eAAe,EACpB,KAAK,2BAA2B,EACjC,MAAM,qBAAqB,CAAC;AAO7B;;;;;;GAMG;AACH,MAAM,WAAW,oBAAoB;IACnC,2BAA2B,CAAC,EAAE,2BAA2B,CAAC;IAC1D,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,WAAW,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAC3C,OAAO,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;IACnC,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,2BAA2B,CAAC,CAAC;IAC9D,WAAW,CAAC,EAAE,WAAW,EAAE,CAAC;IAC5B,iBAAiB,CAAC,EAAE,CAAC,QAAQ,EAAE,eAAe,KAAK,eAAe,CAAC;CACpE;
|
|
1
|
+
{"version":3,"file":"openapi-module.d.ts","sourceRoot":"","sources":["../src/openapi-module.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAEnB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAU,KAAK,kBAAkB,EAAE,KAAK,WAAW,EAAiC,MAAM,cAAc,CAAC;AAChH,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAGhE,OAAO,EAEL,KAAK,2BAA2B,EAChC,KAAK,eAAe,EACpB,KAAK,2BAA2B,EACjC,MAAM,qBAAqB,CAAC;AAO7B;;GAEG;AACH,MAAM,WAAW,6BAA6B;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,oBAAoB;IACnC,2BAA2B,CAAC,EAAE,2BAA2B,CAAC;IAC1D,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,WAAW,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAC3C,OAAO,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;IACnC,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,2BAA2B,CAAC,CAAC;IAC9D,eAAe,CAAC,EAAE,6BAA6B,CAAC;IAChD,WAAW,CAAC,EAAE,WAAW,EAAE,CAAC;IAC5B,iBAAiB,CAAC,EAAE,CAAC,QAAQ,EAAE,eAAe,KAAK,eAAe,CAAC;CACpE;AA6ID;;GAEG;AACH,qBAAa,aAAa;IACxB;;;;;;;;;;;;;;OAcG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,oBAAoB,GAAG,UAAU;IAOzD;;;;;;;;;;;;;;;;OAgBG;IACH,MAAM,CAAC,YAAY,CAAC,OAAO,EAAE,kBAAkB,CAAC,oBAAoB,CAAC,GAAG,UAAU;IAQlF,OAAO,CAAC,MAAM,CAAC,YAAY;CAqE5B"}
|
package/dist/openapi-module.js
CHANGED
|
@@ -13,6 +13,10 @@ const SWAGGER_UI_DIST_BASE_URL = `https://unpkg.com/swagger-ui-dist@${SWAGGER_UI
|
|
|
13
13
|
const SWAGGER_UI_CSS_URL = `${SWAGGER_UI_DIST_BASE_URL}/swagger-ui.css`;
|
|
14
14
|
const SWAGGER_UI_BUNDLE_JS_URL = `${SWAGGER_UI_DIST_BASE_URL}/swagger-ui-bundle.js`;
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Asset URLs used by the generated Swagger UI HTML page.
|
|
18
|
+
*/
|
|
19
|
+
|
|
16
20
|
/**
|
|
17
21
|
* Public options for `OpenApiModule.forRoot(...)` and `OpenApiModule.forRootAsync(...)`.
|
|
18
22
|
*
|
|
@@ -24,24 +28,82 @@ const SWAGGER_UI_BUNDLE_JS_URL = `${SWAGGER_UI_DIST_BASE_URL}/swagger-ui-bundle.
|
|
|
24
28
|
function escapeHtml(value) {
|
|
25
29
|
return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
26
30
|
}
|
|
27
|
-
function
|
|
31
|
+
function cloneRecord(record) {
|
|
32
|
+
if (!record) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
const clone = {};
|
|
36
|
+
for (const [key, value] of Object.entries(record)) {
|
|
37
|
+
clone[key] = cloneSnapshotValue(value);
|
|
38
|
+
}
|
|
39
|
+
return clone;
|
|
40
|
+
}
|
|
41
|
+
function cloneSnapshotValue(value) {
|
|
42
|
+
if (value === null || value === undefined) {
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
if (Array.isArray(value)) {
|
|
46
|
+
return value.map(entry => cloneSnapshotValue(entry));
|
|
47
|
+
}
|
|
48
|
+
if (typeof value !== 'object') {
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
const clone = {};
|
|
52
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
53
|
+
clone[key] = cloneSnapshotValue(value[key]);
|
|
54
|
+
}
|
|
55
|
+
return clone;
|
|
56
|
+
}
|
|
57
|
+
function deepFreeze(value) {
|
|
58
|
+
if (value === null || typeof value !== 'object') {
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
62
|
+
deepFreeze(value[key]);
|
|
63
|
+
}
|
|
64
|
+
return Object.freeze(value);
|
|
65
|
+
}
|
|
66
|
+
function snapshotOpenApiModuleOptions(options) {
|
|
67
|
+
return deepFreeze({
|
|
68
|
+
defaultErrorResponsesPolicy: options.defaultErrorResponsesPolicy,
|
|
69
|
+
descriptors: options.descriptors ? cloneSnapshotValue(options.descriptors) : undefined,
|
|
70
|
+
documentTransform: options.documentTransform,
|
|
71
|
+
extraModels: options.extraModels ? [...options.extraModels] : undefined,
|
|
72
|
+
securitySchemes: cloneRecord(options.securitySchemes),
|
|
73
|
+
sources: options.sources ? cloneSnapshotValue(options.sources) : undefined,
|
|
74
|
+
swaggerUiAssets: options.swaggerUiAssets ? {
|
|
75
|
+
...options.swaggerUiAssets
|
|
76
|
+
} : undefined,
|
|
77
|
+
title: options.title,
|
|
78
|
+
ui: options.ui,
|
|
79
|
+
version: options.version
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function resolveSwaggerUiAssets(options) {
|
|
83
|
+
return {
|
|
84
|
+
cssUrl: options.swaggerUiAssets?.cssUrl ?? SWAGGER_UI_CSS_URL,
|
|
85
|
+
jsBundleUrl: options.swaggerUiAssets?.jsBundleUrl ?? SWAGGER_UI_BUNDLE_JS_URL
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function createSwaggerUiHtml(title, assets) {
|
|
28
89
|
return `<!doctype html>
|
|
29
90
|
<html lang="en">
|
|
30
91
|
<head>
|
|
31
92
|
<meta charset="utf-8" />
|
|
32
93
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
33
94
|
<title>${escapeHtml(title)}</title>
|
|
34
|
-
<link rel="stylesheet" href="${
|
|
95
|
+
<link rel="stylesheet" href="${escapeHtml(assets.cssUrl)}" />
|
|
35
96
|
</head>
|
|
36
97
|
<body>
|
|
37
98
|
<div id="swagger-ui"></div>
|
|
38
|
-
<script src="${
|
|
99
|
+
<script src="${escapeHtml(assets.jsBundleUrl)}" crossorigin></script>
|
|
39
100
|
<script>
|
|
40
101
|
const specUrl = window.location.pathname.replace(/\/docs\/?$/, '/openapi.json');
|
|
41
|
-
|
|
102
|
+
const swaggerUi = SwaggerUIBundle({
|
|
42
103
|
url: specUrl,
|
|
43
104
|
dom_id: '#swagger-ui'
|
|
44
105
|
});
|
|
106
|
+
void swaggerUi;
|
|
45
107
|
</script>
|
|
46
108
|
</body>
|
|
47
109
|
</html>`;
|
|
@@ -87,7 +149,7 @@ export class OpenApiModule {
|
|
|
87
149
|
static forRoot(options) {
|
|
88
150
|
return this.createModule({
|
|
89
151
|
scope: 'singleton',
|
|
90
|
-
useValue: options
|
|
152
|
+
useValue: snapshotOpenApiModuleOptions(options)
|
|
91
153
|
});
|
|
92
154
|
}
|
|
93
155
|
|
|
@@ -112,7 +174,7 @@ export class OpenApiModule {
|
|
|
112
174
|
return this.createModule({
|
|
113
175
|
inject: options.inject,
|
|
114
176
|
scope: 'singleton',
|
|
115
|
-
useFactory: options.useFactory
|
|
177
|
+
useFactory: async (...deps) => snapshotOpenApiModuleOptions(await options.useFactory(...deps))
|
|
116
178
|
});
|
|
117
179
|
}
|
|
118
180
|
static createModule(optionsProvider) {
|
|
@@ -140,7 +202,7 @@ export class OpenApiModule {
|
|
|
140
202
|
throw new NotFoundException('Swagger UI is disabled.');
|
|
141
203
|
}
|
|
142
204
|
context.response.setHeader('content-type', 'text/html; charset=utf-8');
|
|
143
|
-
return createSwaggerUiHtml(this.options.title);
|
|
205
|
+
return createSwaggerUiHtml(this.options.title, resolveSwaggerUiAssets(this.options));
|
|
144
206
|
}
|
|
145
207
|
static {
|
|
146
208
|
_initClass();
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"documentation",
|
|
10
10
|
"rest"
|
|
11
11
|
],
|
|
12
|
-
"version": "1.0.0-beta.
|
|
12
|
+
"version": "1.0.0-beta.5",
|
|
13
13
|
"private": false,
|
|
14
14
|
"license": "MIT",
|
|
15
15
|
"repository": {
|
|
@@ -36,10 +36,10 @@
|
|
|
36
36
|
"dist"
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@fluojs/core": "^1.0.0-beta.
|
|
40
|
-
"@fluojs/validation": "^1.0.0-beta.
|
|
41
|
-
"@fluojs/http": "^1.0.0-beta.
|
|
42
|
-
"@fluojs/runtime": "^1.0.0-beta.
|
|
39
|
+
"@fluojs/core": "^1.0.0-beta.3",
|
|
40
|
+
"@fluojs/validation": "^1.0.0-beta.2",
|
|
41
|
+
"@fluojs/http": "^1.0.0-beta.9",
|
|
42
|
+
"@fluojs/runtime": "^1.0.0-beta.9"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"vitest": "^3.2.4"
|