@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 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
@@ -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;AAsED;;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"}
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"}
@@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#x27;');
26
30
  }
27
- function createSwaggerUiHtml(title) {
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="${SWAGGER_UI_CSS_URL}" />
95
+ <link rel="stylesheet" href="${escapeHtml(assets.cssUrl)}" />
35
96
  </head>
36
97
  <body>
37
98
  <div id="swagger-ui"></div>
38
- <script src="${SWAGGER_UI_BUNDLE_JS_URL}" crossorigin></script>
99
+ <script src="${escapeHtml(assets.jsBundleUrl)}" crossorigin></script>
39
100
  <script>
40
101
  const specUrl = window.location.pathname.replace(/\/docs\/?$/, '/openapi.json');
41
- window.ui = SwaggerUIBundle({
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.4",
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.2",
40
- "@fluojs/validation": "^1.0.0-beta.1",
41
- "@fluojs/http": "^1.0.0-beta.4",
42
- "@fluojs/runtime": "^1.0.0-beta.5"
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"