@nestia/sdk 7.0.0-dev.20250607 → 7.0.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/LICENSE +21 -21
- package/README.md +92 -92
- package/assets/bundle/api/HttpError.ts +1 -1
- package/assets/bundle/api/IConnection.ts +1 -1
- package/assets/bundle/api/Primitive.ts +1 -1
- package/assets/bundle/api/Resolved.ts +1 -1
- package/assets/bundle/api/index.ts +4 -4
- package/assets/bundle/api/module.ts +6 -6
- package/assets/bundle/distribute/README.md +37 -37
- package/assets/bundle/distribute/package.json +28 -28
- package/assets/bundle/distribute/tsconfig.json +109 -109
- package/assets/bundle/e2e/index.ts +42 -42
- package/assets/config/nestia.config.ts +97 -97
- package/lib/executable/internal/NestiaConfigLoader.js +4 -4
- package/lib/executable/sdk.js +12 -12
- package/package.json +5 -5
- package/src/INestiaConfig.ts +269 -269
- package/src/NestiaSdkApplication.ts +307 -307
- package/src/NestiaSwaggerComposer.ts +138 -138
- package/src/analyses/AccessorAnalyzer.ts +67 -67
- package/src/analyses/ConfigAnalyzer.ts +155 -155
- package/src/analyses/ExceptionAnalyzer.ts +154 -154
- package/src/analyses/GenericAnalyzer.ts +49 -49
- package/src/analyses/ImportAnalyzer.ts +171 -171
- package/src/analyses/PathAnalyzer.ts +69 -69
- package/src/analyses/ReflectControllerAnalyzer.ts +105 -105
- package/src/analyses/ReflectHttpOperationAnalyzer.ts +183 -183
- package/src/analyses/ReflectHttpOperationExceptionAnalyzer.ts +71 -71
- package/src/analyses/ReflectHttpOperationParameterAnalyzer.ts +348 -348
- package/src/analyses/ReflectHttpOperationResponseAnalyzer.ts +127 -127
- package/src/analyses/ReflectMetadataAnalyzer.ts +44 -44
- package/src/analyses/ReflectWebSocketOperationAnalyzer.ts +172 -172
- package/src/analyses/SecurityAnalyzer.ts +25 -25
- package/src/analyses/TypedHttpRouteAnalyzer.ts +204 -204
- package/src/analyses/TypedWebSocketRouteAnalyzer.ts +33 -33
- package/src/decorators/OperationMetadata.ts +15 -15
- package/src/executable/internal/CommandParser.ts +15 -15
- package/src/executable/internal/NestiaConfigLoader.ts +78 -78
- package/src/executable/internal/NestiaSdkCommand.ts +103 -103
- package/src/executable/sdk.ts +75 -75
- package/src/generates/CloneGenerator.ts +66 -66
- package/src/generates/E2eGenerator.ts +32 -32
- package/src/generates/SdkGenerator.ts +160 -160
- package/src/generates/SwaggerGenerator.ts +284 -284
- package/src/generates/internal/E2eFileProgrammer.ts +205 -205
- package/src/generates/internal/FilePrinter.ts +53 -53
- package/src/generates/internal/ImportDictionary.ts +163 -163
- package/src/generates/internal/SdkAliasCollection.ts +255 -255
- package/src/generates/internal/SdkDistributionComposer.ts +103 -103
- package/src/generates/internal/SdkFileProgrammer.ts +116 -116
- package/src/generates/internal/SdkHttpCloneProgrammer.ts +124 -124
- package/src/generates/internal/SdkHttpCloneReferencer.ts +75 -75
- package/src/generates/internal/SdkHttpFunctionProgrammer.ts +276 -276
- package/src/generates/internal/SdkHttpNamespaceProgrammer.ts +500 -500
- package/src/generates/internal/SdkHttpParameterProgrammer.ts +178 -178
- package/src/generates/internal/SdkHttpRouteProgrammer.ts +107 -107
- package/src/generates/internal/SdkHttpSimulationProgrammer.ts +340 -340
- package/src/generates/internal/SdkImportWizard.ts +55 -55
- package/src/generates/internal/SdkRouteDirectory.ts +18 -18
- package/src/generates/internal/SdkTypeProgrammer.ts +384 -384
- package/src/generates/internal/SdkTypeTagProgrammer.ts +102 -102
- package/src/generates/internal/SdkWebSocketNamespaceProgrammer.ts +366 -366
- package/src/generates/internal/SdkWebSocketParameterProgrammer.ts +87 -87
- package/src/generates/internal/SdkWebSocketRouteProgrammer.ts +279 -279
- package/src/generates/internal/SwaggerDescriptionComposer.ts +64 -64
- package/src/generates/internal/SwaggerOperationComposer.ts +119 -119
- package/src/generates/internal/SwaggerOperationParameterComposer.ts +177 -177
- package/src/generates/internal/SwaggerOperationResponseComposer.ts +110 -110
- package/src/index.ts +4 -4
- package/src/module.ts +3 -3
- package/src/structures/INestiaProject.ts +13 -13
- package/src/structures/INestiaSdkInput.ts +20 -20
- package/src/structures/IReflectApplication.ts +8 -8
- package/src/structures/IReflectController.ts +15 -15
- package/src/structures/IReflectHttpOperation.ts +26 -26
- package/src/structures/IReflectHttpOperationException.ts +19 -19
- package/src/structures/IReflectHttpOperationParameter.ts +81 -81
- package/src/structures/IReflectHttpOperationSuccess.ts +22 -22
- package/src/structures/IReflectOperationError.ts +26 -26
- package/src/structures/IReflectType.ts +4 -4
- package/src/structures/IReflectTypeImport.ts +4 -4
- package/src/structures/IReflectWebSocketOperation.ts +17 -17
- package/src/structures/IReflectWebSocketOperationParameter.ts +38 -38
- package/src/structures/ITypedApplication.ts +11 -11
- package/src/structures/ITypedHttpRoute.ts +41 -41
- package/src/structures/ITypedHttpRouteException.ts +15 -15
- package/src/structures/ITypedHttpRouteParameter.ts +41 -41
- package/src/structures/ITypedHttpRouteSuccess.ts +22 -22
- package/src/structures/ITypedWebSocketRoute.ts +24 -24
- package/src/structures/ITypedWebSocketRouteParameter.ts +3 -3
- package/src/structures/MethodType.ts +5 -5
- package/src/structures/ParamCategory.ts +1 -1
- package/src/structures/TypeEntry.ts +22 -22
- package/src/transform.ts +9 -9
- package/src/transformers/IOperationMetadata.ts +44 -44
- package/src/transformers/ISdkOperationTransformerContext.ts +8 -8
- package/src/transformers/SdkOperationProgrammer.ts +209 -209
- package/src/transformers/SdkOperationTransformer.ts +253 -253
- package/src/transformers/TextPlainValidator.ts +17 -17
- package/src/typings/get-function-location.d.ts +7 -7
- package/src/utils/ArrayUtil.ts +26 -26
- package/src/utils/FileRetriever.ts +22 -22
- package/src/utils/MapUtil.ts +14 -14
- package/src/utils/MetadataUtil.ts +26 -26
- package/src/utils/PathUtil.ts +10 -10
- package/src/utils/SourceFinder.ts +66 -66
- package/src/utils/StringUtil.ts +17 -17
- package/src/utils/StripEnums.ts +5 -5
- package/src/utils/VersioningStrategy.ts +28 -28
- package/src/validators/HttpHeadersValidator.ts +34 -34
- package/src/validators/HttpQueryValidator.ts +34 -34
|
@@ -1,284 +1,284 @@
|
|
|
1
|
-
import { SwaggerCustomizer } from "@nestia/core";
|
|
2
|
-
import { OpenApi, OpenApiV3, SwaggerV2 } from "@samchon/openapi";
|
|
3
|
-
import fs from "fs";
|
|
4
|
-
import path from "path";
|
|
5
|
-
import { Singleton } from "tstl";
|
|
6
|
-
import typia, { IJsonSchemaCollection } from "typia";
|
|
7
|
-
import { JsonSchemasProgrammer } from "typia/lib/programmers/json/JsonSchemasProgrammer";
|
|
8
|
-
import { Metadata } from "typia/lib/schemas/metadata/Metadata";
|
|
9
|
-
|
|
10
|
-
import { INestiaConfig } from "../INestiaConfig";
|
|
11
|
-
import { ITypedApplication } from "../structures/ITypedApplication";
|
|
12
|
-
import { ITypedHttpRoute } from "../structures/ITypedHttpRoute";
|
|
13
|
-
import { FileRetriever } from "../utils/FileRetriever";
|
|
14
|
-
import { SdkHttpParameterProgrammer } from "./internal/SdkHttpParameterProgrammer";
|
|
15
|
-
import { SwaggerOperationComposer } from "./internal/SwaggerOperationComposer";
|
|
16
|
-
|
|
17
|
-
export namespace SwaggerGenerator {
|
|
18
|
-
export const generate = async (app: ITypedApplication): Promise<void> => {
|
|
19
|
-
// GET CONFIGURATION
|
|
20
|
-
console.log("Generating Swagger Document");
|
|
21
|
-
if (app.project.config.swagger === undefined)
|
|
22
|
-
throw new Error("Swagger configuration is not defined.");
|
|
23
|
-
const config: INestiaConfig.ISwaggerConfig = app.project.config.swagger;
|
|
24
|
-
|
|
25
|
-
// TARGET LOCATION
|
|
26
|
-
const parsed: path.ParsedPath = path.parse(config.output);
|
|
27
|
-
const directory: string = path.dirname(parsed.dir);
|
|
28
|
-
if (fs.existsSync(directory) === false)
|
|
29
|
-
try {
|
|
30
|
-
await fs.promises.mkdir(directory);
|
|
31
|
-
} catch {}
|
|
32
|
-
if (fs.existsSync(directory) === false)
|
|
33
|
-
throw new Error(
|
|
34
|
-
`Error on NestiaApplication.swagger(): failed to create output directory: ${directory}`,
|
|
35
|
-
);
|
|
36
|
-
const location: string = !!parsed.ext
|
|
37
|
-
? path.resolve(config.output)
|
|
38
|
-
: path.join(path.resolve(config.output), "swagger.json");
|
|
39
|
-
|
|
40
|
-
// COMPOSE SWAGGER DOCUMENT
|
|
41
|
-
const document: OpenApi.IDocument = compose({
|
|
42
|
-
config,
|
|
43
|
-
routes: app.routes.filter((route) => route.protocol === "http"),
|
|
44
|
-
document: await initialize(config),
|
|
45
|
-
});
|
|
46
|
-
const specified:
|
|
47
|
-
| OpenApi.IDocument
|
|
48
|
-
| SwaggerV2.IDocument
|
|
49
|
-
| OpenApiV3.IDocument =
|
|
50
|
-
config.openapi === "2.0"
|
|
51
|
-
? OpenApi.downgrade(document, config.openapi as "2.0")
|
|
52
|
-
: config.openapi === "3.0"
|
|
53
|
-
? OpenApi.downgrade(document, config.openapi as "3.0")
|
|
54
|
-
: document;
|
|
55
|
-
await fs.promises.writeFile(
|
|
56
|
-
location,
|
|
57
|
-
!config.beautify
|
|
58
|
-
? JSON.stringify(specified)
|
|
59
|
-
: JSON.stringify(
|
|
60
|
-
specified,
|
|
61
|
-
null,
|
|
62
|
-
typeof config.beautify === "number" ? config.beautify : 2,
|
|
63
|
-
),
|
|
64
|
-
"utf8",
|
|
65
|
-
);
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
export const compose = (props: {
|
|
69
|
-
config: Omit<INestiaConfig.ISwaggerConfig, "output">;
|
|
70
|
-
routes: ITypedHttpRoute[];
|
|
71
|
-
document: OpenApi.IDocument;
|
|
72
|
-
}): OpenApi.IDocument => {
|
|
73
|
-
// GATHER METADATA
|
|
74
|
-
const routes: ITypedHttpRoute[] = props.routes.filter((r) =>
|
|
75
|
-
r.jsDocTags.every(
|
|
76
|
-
(tag) => tag.name !== "internal" && tag.name !== "hidden",
|
|
77
|
-
),
|
|
78
|
-
);
|
|
79
|
-
const metadatas: Metadata[] = routes
|
|
80
|
-
.map((r) => [
|
|
81
|
-
r.success.metadata,
|
|
82
|
-
...SdkHttpParameterProgrammer.getAll(r).map((p) => p.metadata),
|
|
83
|
-
...Object.values(r.exceptions).map((e) => e.metadata),
|
|
84
|
-
])
|
|
85
|
-
.flat()
|
|
86
|
-
.filter((m) => m.size() !== 0);
|
|
87
|
-
|
|
88
|
-
// COMPOSE JSON SCHEMAS
|
|
89
|
-
const json: IJsonSchemaCollection = JsonSchemasProgrammer.write({
|
|
90
|
-
version: "3.1",
|
|
91
|
-
metadatas,
|
|
92
|
-
});
|
|
93
|
-
const dict: WeakMap<Metadata, OpenApi.IJsonSchema> = new WeakMap();
|
|
94
|
-
json.schemas.forEach((schema, i) => dict.set(metadatas[i], schema));
|
|
95
|
-
const schema = (metadata: Metadata): OpenApi.IJsonSchema | undefined =>
|
|
96
|
-
dict.get(metadata);
|
|
97
|
-
|
|
98
|
-
// COMPOSE DOCUMENT
|
|
99
|
-
const document: OpenApi.IDocument = props.document;
|
|
100
|
-
document.components.schemas ??= {};
|
|
101
|
-
Object.assign(document.components.schemas, json.components.schemas);
|
|
102
|
-
fillPaths({
|
|
103
|
-
...props,
|
|
104
|
-
routes,
|
|
105
|
-
schema,
|
|
106
|
-
document,
|
|
107
|
-
});
|
|
108
|
-
return document;
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
export const initialize = async (
|
|
112
|
-
config: Omit<INestiaConfig.ISwaggerConfig, "output">,
|
|
113
|
-
): Promise<OpenApi.IDocument> => {
|
|
114
|
-
const pack = new Singleton(
|
|
115
|
-
async (): Promise<Partial<OpenApi.IDocument.IInfo> | null> => {
|
|
116
|
-
const location: string | null = await FileRetriever.file(
|
|
117
|
-
"package.json",
|
|
118
|
-
)(process.cwd());
|
|
119
|
-
if (location === null) return null;
|
|
120
|
-
|
|
121
|
-
try {
|
|
122
|
-
const content: string = await fs.promises.readFile(location, "utf8");
|
|
123
|
-
const data = typia.json.assertParse<{
|
|
124
|
-
name?: string;
|
|
125
|
-
version?: string;
|
|
126
|
-
description?: string;
|
|
127
|
-
license?:
|
|
128
|
-
| string
|
|
129
|
-
| {
|
|
130
|
-
type: string;
|
|
131
|
-
/** @format uri */
|
|
132
|
-
url: string;
|
|
133
|
-
};
|
|
134
|
-
}>(content);
|
|
135
|
-
return {
|
|
136
|
-
title: data.name,
|
|
137
|
-
version: data.version,
|
|
138
|
-
description: data.description,
|
|
139
|
-
license: data.license
|
|
140
|
-
? typeof data.license === "string"
|
|
141
|
-
? { name: data.license }
|
|
142
|
-
: typeof data.license === "object"
|
|
143
|
-
? {
|
|
144
|
-
name: data.license.type,
|
|
145
|
-
url: data.license.url,
|
|
146
|
-
}
|
|
147
|
-
: undefined
|
|
148
|
-
: undefined,
|
|
149
|
-
};
|
|
150
|
-
} catch {
|
|
151
|
-
return null;
|
|
152
|
-
}
|
|
153
|
-
},
|
|
154
|
-
);
|
|
155
|
-
|
|
156
|
-
return {
|
|
157
|
-
openapi: "3.1.0",
|
|
158
|
-
servers: config.servers ?? [
|
|
159
|
-
{
|
|
160
|
-
url: "https://github.com/samchon/nestia",
|
|
161
|
-
description: "insert your server url",
|
|
162
|
-
},
|
|
163
|
-
],
|
|
164
|
-
info: {
|
|
165
|
-
...(config.info ?? {}),
|
|
166
|
-
version: config.info?.version ?? (await pack.get())?.version ?? "0.1.0",
|
|
167
|
-
title:
|
|
168
|
-
config.info?.title ??
|
|
169
|
-
(await pack.get())?.title ??
|
|
170
|
-
"Swagger Documents",
|
|
171
|
-
description:
|
|
172
|
-
config.info?.description ??
|
|
173
|
-
(await pack.get())?.description ??
|
|
174
|
-
"Generated by nestia - https://github.com/samchon/nestia",
|
|
175
|
-
license: config.info?.license ?? (await pack.get())?.license,
|
|
176
|
-
},
|
|
177
|
-
paths: {},
|
|
178
|
-
components: {
|
|
179
|
-
schemas: {},
|
|
180
|
-
securitySchemes: config.security,
|
|
181
|
-
},
|
|
182
|
-
tags: config.tags ?? [],
|
|
183
|
-
"x-samchon-emended-v4": true,
|
|
184
|
-
};
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
const fillPaths = (props: {
|
|
188
|
-
config: Omit<INestiaConfig.ISwaggerConfig, "output">;
|
|
189
|
-
document: OpenApi.IDocument;
|
|
190
|
-
schema: (metadata: Metadata) => OpenApi.IJsonSchema | undefined;
|
|
191
|
-
routes: ITypedHttpRoute[];
|
|
192
|
-
}): void => {
|
|
193
|
-
// SWAGGER CUSTOMIZER
|
|
194
|
-
const customizers: Array<() => void> = [];
|
|
195
|
-
const neighbor = {
|
|
196
|
-
at: new Singleton(() => {
|
|
197
|
-
const functor: Map<Function, Endpoint> = new Map();
|
|
198
|
-
for (const r of props.routes) {
|
|
199
|
-
const method: OpenApi.Method =
|
|
200
|
-
r.method.toLowerCase() as OpenApi.Method;
|
|
201
|
-
const path: string = getPath(r);
|
|
202
|
-
const operation: OpenApi.IOperation | undefined =
|
|
203
|
-
props.document.paths?.[path]?.[method];
|
|
204
|
-
if (operation === undefined) continue;
|
|
205
|
-
functor.set(r.function, {
|
|
206
|
-
method,
|
|
207
|
-
path,
|
|
208
|
-
route: operation,
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
return functor;
|
|
212
|
-
}),
|
|
213
|
-
get: new Singleton(
|
|
214
|
-
() =>
|
|
215
|
-
(key: Accessor): OpenApi.IOperation | undefined => {
|
|
216
|
-
const method: OpenApi.Method =
|
|
217
|
-
key.method.toLowerCase() as OpenApi.Method;
|
|
218
|
-
const path: string =
|
|
219
|
-
"/" +
|
|
220
|
-
key.path
|
|
221
|
-
.split("/")
|
|
222
|
-
.filter((str) => !!str.length)
|
|
223
|
-
.map((str) =>
|
|
224
|
-
str.startsWith(":") ? `{${str.substring(1)}}` : str,
|
|
225
|
-
)
|
|
226
|
-
.join("/");
|
|
227
|
-
return props.document.paths?.[path]?.[method];
|
|
228
|
-
},
|
|
229
|
-
),
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
// COMPOSE OPERATIONS
|
|
233
|
-
for (const r of props.routes) {
|
|
234
|
-
const operation: OpenApi.IOperation = SwaggerOperationComposer.compose({
|
|
235
|
-
...props,
|
|
236
|
-
route: r,
|
|
237
|
-
});
|
|
238
|
-
const path: string = getPath(r);
|
|
239
|
-
props.document.paths ??= {};
|
|
240
|
-
props.document.paths[path] ??= {};
|
|
241
|
-
props.document.paths[path][r.method.toLowerCase() as "get"] = operation;
|
|
242
|
-
|
|
243
|
-
const closure: Function | Function[] | undefined = Reflect.getMetadata(
|
|
244
|
-
"nestia/SwaggerCustomizer",
|
|
245
|
-
r.controller.class.prototype,
|
|
246
|
-
r.name,
|
|
247
|
-
);
|
|
248
|
-
if (closure !== undefined) {
|
|
249
|
-
const array: Function[] = Array.isArray(closure) ? closure : [closure];
|
|
250
|
-
customizers.push(() => {
|
|
251
|
-
for (const closure of array)
|
|
252
|
-
closure({
|
|
253
|
-
swagger: props.document,
|
|
254
|
-
method: r.method,
|
|
255
|
-
path,
|
|
256
|
-
route: operation,
|
|
257
|
-
at: (func: Function) => neighbor.at.get().get(func),
|
|
258
|
-
get: (accessor: Accessor) => neighbor.get.get()(accessor),
|
|
259
|
-
} satisfies SwaggerCustomizer.IProps);
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// DO CUSTOMIZE
|
|
265
|
-
for (const fn of customizers) fn();
|
|
266
|
-
};
|
|
267
|
-
|
|
268
|
-
const getPath = (route: ITypedHttpRoute): string => {
|
|
269
|
-
let str: string = route.path;
|
|
270
|
-
for (const param of route.pathParameters)
|
|
271
|
-
str = str.replace(`:${param.field}`, `{${param.field}}`);
|
|
272
|
-
return str;
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
interface Accessor {
|
|
277
|
-
method: string;
|
|
278
|
-
path: string;
|
|
279
|
-
}
|
|
280
|
-
interface Endpoint {
|
|
281
|
-
method: string;
|
|
282
|
-
path: string;
|
|
283
|
-
route: OpenApi.IOperation;
|
|
284
|
-
}
|
|
1
|
+
import { SwaggerCustomizer } from "@nestia/core";
|
|
2
|
+
import { OpenApi, OpenApiV3, SwaggerV2 } from "@samchon/openapi";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { Singleton } from "tstl";
|
|
6
|
+
import typia, { IJsonSchemaCollection } from "typia";
|
|
7
|
+
import { JsonSchemasProgrammer } from "typia/lib/programmers/json/JsonSchemasProgrammer";
|
|
8
|
+
import { Metadata } from "typia/lib/schemas/metadata/Metadata";
|
|
9
|
+
|
|
10
|
+
import { INestiaConfig } from "../INestiaConfig";
|
|
11
|
+
import { ITypedApplication } from "../structures/ITypedApplication";
|
|
12
|
+
import { ITypedHttpRoute } from "../structures/ITypedHttpRoute";
|
|
13
|
+
import { FileRetriever } from "../utils/FileRetriever";
|
|
14
|
+
import { SdkHttpParameterProgrammer } from "./internal/SdkHttpParameterProgrammer";
|
|
15
|
+
import { SwaggerOperationComposer } from "./internal/SwaggerOperationComposer";
|
|
16
|
+
|
|
17
|
+
export namespace SwaggerGenerator {
|
|
18
|
+
export const generate = async (app: ITypedApplication): Promise<void> => {
|
|
19
|
+
// GET CONFIGURATION
|
|
20
|
+
console.log("Generating Swagger Document");
|
|
21
|
+
if (app.project.config.swagger === undefined)
|
|
22
|
+
throw new Error("Swagger configuration is not defined.");
|
|
23
|
+
const config: INestiaConfig.ISwaggerConfig = app.project.config.swagger;
|
|
24
|
+
|
|
25
|
+
// TARGET LOCATION
|
|
26
|
+
const parsed: path.ParsedPath = path.parse(config.output);
|
|
27
|
+
const directory: string = path.dirname(parsed.dir);
|
|
28
|
+
if (fs.existsSync(directory) === false)
|
|
29
|
+
try {
|
|
30
|
+
await fs.promises.mkdir(directory);
|
|
31
|
+
} catch {}
|
|
32
|
+
if (fs.existsSync(directory) === false)
|
|
33
|
+
throw new Error(
|
|
34
|
+
`Error on NestiaApplication.swagger(): failed to create output directory: ${directory}`,
|
|
35
|
+
);
|
|
36
|
+
const location: string = !!parsed.ext
|
|
37
|
+
? path.resolve(config.output)
|
|
38
|
+
: path.join(path.resolve(config.output), "swagger.json");
|
|
39
|
+
|
|
40
|
+
// COMPOSE SWAGGER DOCUMENT
|
|
41
|
+
const document: OpenApi.IDocument = compose({
|
|
42
|
+
config,
|
|
43
|
+
routes: app.routes.filter((route) => route.protocol === "http"),
|
|
44
|
+
document: await initialize(config),
|
|
45
|
+
});
|
|
46
|
+
const specified:
|
|
47
|
+
| OpenApi.IDocument
|
|
48
|
+
| SwaggerV2.IDocument
|
|
49
|
+
| OpenApiV3.IDocument =
|
|
50
|
+
config.openapi === "2.0"
|
|
51
|
+
? OpenApi.downgrade(document, config.openapi as "2.0")
|
|
52
|
+
: config.openapi === "3.0"
|
|
53
|
+
? OpenApi.downgrade(document, config.openapi as "3.0")
|
|
54
|
+
: document;
|
|
55
|
+
await fs.promises.writeFile(
|
|
56
|
+
location,
|
|
57
|
+
!config.beautify
|
|
58
|
+
? JSON.stringify(specified)
|
|
59
|
+
: JSON.stringify(
|
|
60
|
+
specified,
|
|
61
|
+
null,
|
|
62
|
+
typeof config.beautify === "number" ? config.beautify : 2,
|
|
63
|
+
),
|
|
64
|
+
"utf8",
|
|
65
|
+
);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const compose = (props: {
|
|
69
|
+
config: Omit<INestiaConfig.ISwaggerConfig, "output">;
|
|
70
|
+
routes: ITypedHttpRoute[];
|
|
71
|
+
document: OpenApi.IDocument;
|
|
72
|
+
}): OpenApi.IDocument => {
|
|
73
|
+
// GATHER METADATA
|
|
74
|
+
const routes: ITypedHttpRoute[] = props.routes.filter((r) =>
|
|
75
|
+
r.jsDocTags.every(
|
|
76
|
+
(tag) => tag.name !== "internal" && tag.name !== "hidden",
|
|
77
|
+
),
|
|
78
|
+
);
|
|
79
|
+
const metadatas: Metadata[] = routes
|
|
80
|
+
.map((r) => [
|
|
81
|
+
r.success.metadata,
|
|
82
|
+
...SdkHttpParameterProgrammer.getAll(r).map((p) => p.metadata),
|
|
83
|
+
...Object.values(r.exceptions).map((e) => e.metadata),
|
|
84
|
+
])
|
|
85
|
+
.flat()
|
|
86
|
+
.filter((m) => m.size() !== 0);
|
|
87
|
+
|
|
88
|
+
// COMPOSE JSON SCHEMAS
|
|
89
|
+
const json: IJsonSchemaCollection = JsonSchemasProgrammer.write({
|
|
90
|
+
version: "3.1",
|
|
91
|
+
metadatas,
|
|
92
|
+
});
|
|
93
|
+
const dict: WeakMap<Metadata, OpenApi.IJsonSchema> = new WeakMap();
|
|
94
|
+
json.schemas.forEach((schema, i) => dict.set(metadatas[i], schema));
|
|
95
|
+
const schema = (metadata: Metadata): OpenApi.IJsonSchema | undefined =>
|
|
96
|
+
dict.get(metadata);
|
|
97
|
+
|
|
98
|
+
// COMPOSE DOCUMENT
|
|
99
|
+
const document: OpenApi.IDocument = props.document;
|
|
100
|
+
document.components.schemas ??= {};
|
|
101
|
+
Object.assign(document.components.schemas, json.components.schemas);
|
|
102
|
+
fillPaths({
|
|
103
|
+
...props,
|
|
104
|
+
routes,
|
|
105
|
+
schema,
|
|
106
|
+
document,
|
|
107
|
+
});
|
|
108
|
+
return document;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export const initialize = async (
|
|
112
|
+
config: Omit<INestiaConfig.ISwaggerConfig, "output">,
|
|
113
|
+
): Promise<OpenApi.IDocument> => {
|
|
114
|
+
const pack = new Singleton(
|
|
115
|
+
async (): Promise<Partial<OpenApi.IDocument.IInfo> | null> => {
|
|
116
|
+
const location: string | null = await FileRetriever.file(
|
|
117
|
+
"package.json",
|
|
118
|
+
)(process.cwd());
|
|
119
|
+
if (location === null) return null;
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const content: string = await fs.promises.readFile(location, "utf8");
|
|
123
|
+
const data = typia.json.assertParse<{
|
|
124
|
+
name?: string;
|
|
125
|
+
version?: string;
|
|
126
|
+
description?: string;
|
|
127
|
+
license?:
|
|
128
|
+
| string
|
|
129
|
+
| {
|
|
130
|
+
type: string;
|
|
131
|
+
/** @format uri */
|
|
132
|
+
url: string;
|
|
133
|
+
};
|
|
134
|
+
}>(content);
|
|
135
|
+
return {
|
|
136
|
+
title: data.name,
|
|
137
|
+
version: data.version,
|
|
138
|
+
description: data.description,
|
|
139
|
+
license: data.license
|
|
140
|
+
? typeof data.license === "string"
|
|
141
|
+
? { name: data.license }
|
|
142
|
+
: typeof data.license === "object"
|
|
143
|
+
? {
|
|
144
|
+
name: data.license.type,
|
|
145
|
+
url: data.license.url,
|
|
146
|
+
}
|
|
147
|
+
: undefined
|
|
148
|
+
: undefined,
|
|
149
|
+
};
|
|
150
|
+
} catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
openapi: "3.1.0",
|
|
158
|
+
servers: config.servers ?? [
|
|
159
|
+
{
|
|
160
|
+
url: "https://github.com/samchon/nestia",
|
|
161
|
+
description: "insert your server url",
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
info: {
|
|
165
|
+
...(config.info ?? {}),
|
|
166
|
+
version: config.info?.version ?? (await pack.get())?.version ?? "0.1.0",
|
|
167
|
+
title:
|
|
168
|
+
config.info?.title ??
|
|
169
|
+
(await pack.get())?.title ??
|
|
170
|
+
"Swagger Documents",
|
|
171
|
+
description:
|
|
172
|
+
config.info?.description ??
|
|
173
|
+
(await pack.get())?.description ??
|
|
174
|
+
"Generated by nestia - https://github.com/samchon/nestia",
|
|
175
|
+
license: config.info?.license ?? (await pack.get())?.license,
|
|
176
|
+
},
|
|
177
|
+
paths: {},
|
|
178
|
+
components: {
|
|
179
|
+
schemas: {},
|
|
180
|
+
securitySchemes: config.security,
|
|
181
|
+
},
|
|
182
|
+
tags: config.tags ?? [],
|
|
183
|
+
"x-samchon-emended-v4": true,
|
|
184
|
+
};
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const fillPaths = (props: {
|
|
188
|
+
config: Omit<INestiaConfig.ISwaggerConfig, "output">;
|
|
189
|
+
document: OpenApi.IDocument;
|
|
190
|
+
schema: (metadata: Metadata) => OpenApi.IJsonSchema | undefined;
|
|
191
|
+
routes: ITypedHttpRoute[];
|
|
192
|
+
}): void => {
|
|
193
|
+
// SWAGGER CUSTOMIZER
|
|
194
|
+
const customizers: Array<() => void> = [];
|
|
195
|
+
const neighbor = {
|
|
196
|
+
at: new Singleton(() => {
|
|
197
|
+
const functor: Map<Function, Endpoint> = new Map();
|
|
198
|
+
for (const r of props.routes) {
|
|
199
|
+
const method: OpenApi.Method =
|
|
200
|
+
r.method.toLowerCase() as OpenApi.Method;
|
|
201
|
+
const path: string = getPath(r);
|
|
202
|
+
const operation: OpenApi.IOperation | undefined =
|
|
203
|
+
props.document.paths?.[path]?.[method];
|
|
204
|
+
if (operation === undefined) continue;
|
|
205
|
+
functor.set(r.function, {
|
|
206
|
+
method,
|
|
207
|
+
path,
|
|
208
|
+
route: operation,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
return functor;
|
|
212
|
+
}),
|
|
213
|
+
get: new Singleton(
|
|
214
|
+
() =>
|
|
215
|
+
(key: Accessor): OpenApi.IOperation | undefined => {
|
|
216
|
+
const method: OpenApi.Method =
|
|
217
|
+
key.method.toLowerCase() as OpenApi.Method;
|
|
218
|
+
const path: string =
|
|
219
|
+
"/" +
|
|
220
|
+
key.path
|
|
221
|
+
.split("/")
|
|
222
|
+
.filter((str) => !!str.length)
|
|
223
|
+
.map((str) =>
|
|
224
|
+
str.startsWith(":") ? `{${str.substring(1)}}` : str,
|
|
225
|
+
)
|
|
226
|
+
.join("/");
|
|
227
|
+
return props.document.paths?.[path]?.[method];
|
|
228
|
+
},
|
|
229
|
+
),
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// COMPOSE OPERATIONS
|
|
233
|
+
for (const r of props.routes) {
|
|
234
|
+
const operation: OpenApi.IOperation = SwaggerOperationComposer.compose({
|
|
235
|
+
...props,
|
|
236
|
+
route: r,
|
|
237
|
+
});
|
|
238
|
+
const path: string = getPath(r);
|
|
239
|
+
props.document.paths ??= {};
|
|
240
|
+
props.document.paths[path] ??= {};
|
|
241
|
+
props.document.paths[path][r.method.toLowerCase() as "get"] = operation;
|
|
242
|
+
|
|
243
|
+
const closure: Function | Function[] | undefined = Reflect.getMetadata(
|
|
244
|
+
"nestia/SwaggerCustomizer",
|
|
245
|
+
r.controller.class.prototype,
|
|
246
|
+
r.name,
|
|
247
|
+
);
|
|
248
|
+
if (closure !== undefined) {
|
|
249
|
+
const array: Function[] = Array.isArray(closure) ? closure : [closure];
|
|
250
|
+
customizers.push(() => {
|
|
251
|
+
for (const closure of array)
|
|
252
|
+
closure({
|
|
253
|
+
swagger: props.document,
|
|
254
|
+
method: r.method,
|
|
255
|
+
path,
|
|
256
|
+
route: operation,
|
|
257
|
+
at: (func: Function) => neighbor.at.get().get(func),
|
|
258
|
+
get: (accessor: Accessor) => neighbor.get.get()(accessor),
|
|
259
|
+
} satisfies SwaggerCustomizer.IProps);
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// DO CUSTOMIZE
|
|
265
|
+
for (const fn of customizers) fn();
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const getPath = (route: ITypedHttpRoute): string => {
|
|
269
|
+
let str: string = route.path;
|
|
270
|
+
for (const param of route.pathParameters)
|
|
271
|
+
str = str.replace(`:${param.field}`, `{${param.field}}`);
|
|
272
|
+
return str;
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
interface Accessor {
|
|
277
|
+
method: string;
|
|
278
|
+
path: string;
|
|
279
|
+
}
|
|
280
|
+
interface Endpoint {
|
|
281
|
+
method: string;
|
|
282
|
+
path: string;
|
|
283
|
+
route: OpenApi.IOperation;
|
|
284
|
+
}
|