@nestia/sdk 13.0.4 → 14.0.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/README.md +1 -1
- package/assets/bundle/distribute/package.json +2 -2
- package/assets/bundle/distribute/tsconfig.json +15 -105
- package/lib/INestiaConfig.d.ts +19 -10
- package/lib/NestiaSdkApplication.js +58 -27
- package/lib/NestiaSdkApplication.js.map +1 -1
- package/lib/NestiaSwaggerComposer.js +11 -10
- package/lib/NestiaSwaggerComposer.js.map +1 -1
- package/lib/analyses/ConfigAnalyzer.js +4 -4
- package/lib/analyses/ConfigAnalyzer.js.map +1 -1
- package/lib/analyses/ParameterNameAnalyzer.d.ts +20 -0
- package/lib/analyses/ParameterNameAnalyzer.js +48 -0
- package/lib/analyses/ParameterNameAnalyzer.js.map +1 -0
- package/lib/analyses/PathAnalyzer.d.ts +30 -0
- package/lib/analyses/PathAnalyzer.js +108 -16
- package/lib/analyses/PathAnalyzer.js.map +1 -1
- package/lib/analyses/ReflectControllerAnalyzer.js +2 -1
- package/lib/analyses/ReflectControllerAnalyzer.js.map +1 -1
- package/lib/analyses/ReflectHttpOperationAnalyzer.js +7 -7
- package/lib/analyses/ReflectHttpOperationAnalyzer.js.map +1 -1
- package/lib/analyses/ReflectHttpOperationParameterAnalyzer.js +122 -23
- package/lib/analyses/ReflectHttpOperationParameterAnalyzer.js.map +1 -1
- package/lib/analyses/ReflectHttpOperationResponseAnalyzer.js +5 -2
- package/lib/analyses/ReflectHttpOperationResponseAnalyzer.js.map +1 -1
- package/lib/analyses/ReflectMcpOperationAnalyzer.js +5 -2
- package/lib/analyses/ReflectMcpOperationAnalyzer.js.map +1 -1
- package/lib/analyses/ReflectWebSocketOperationAnalyzer.js +6 -3
- package/lib/analyses/ReflectWebSocketOperationAnalyzer.js.map +1 -1
- package/lib/analyses/SecurityAnalyzer.d.ts +10 -0
- package/lib/analyses/SecurityAnalyzer.js +24 -19
- package/lib/analyses/SecurityAnalyzer.js.map +1 -1
- package/lib/analyses/SwaggerExampleAnalyzer.d.ts +15 -0
- package/lib/analyses/SwaggerExampleAnalyzer.js +20 -0
- package/lib/analyses/SwaggerExampleAnalyzer.js.map +1 -0
- package/lib/analyses/TypedHttpRouteAnalyzer.js +30 -21
- package/lib/analyses/TypedHttpRouteAnalyzer.js.map +1 -1
- package/lib/analyses/TypedWebSocketRouteAnalyzer.js +1 -1
- package/lib/analyses/TypedWebSocketRouteAnalyzer.js.map +1 -1
- package/lib/executable/internal/NestiaSdkWatcher.js +3 -3
- package/lib/executable/internal/NestiaSdkWatcher.js.map +1 -1
- package/lib/executable/sdk.js +16 -1
- package/lib/executable/sdk.js.map +1 -1
- package/lib/factories/ExpressionFactory.d.ts +3 -0
- package/lib/factories/ExpressionFactory.js +6 -2
- package/lib/factories/ExpressionFactory.js.map +1 -1
- package/lib/factories/LiteralFactory.js +3 -1
- package/lib/factories/LiteralFactory.js.map +1 -1
- package/lib/generates/SdkGenerator.js +3 -3
- package/lib/generates/SdkGenerator.js.map +1 -1
- package/lib/generates/SwaggerGenerator.js +135 -27
- package/lib/generates/SwaggerGenerator.js.map +1 -1
- package/lib/generates/internal/FilePrinter.d.ts +14 -0
- package/lib/generates/internal/FilePrinter.js +31 -0
- package/lib/generates/internal/FilePrinter.js.map +1 -1
- package/lib/generates/internal/ImportDictionary.d.ts +2 -0
- package/lib/generates/internal/ImportDictionary.js +13 -0
- package/lib/generates/internal/ImportDictionary.js.map +1 -1
- package/lib/generates/internal/SdkAliasCollection.d.ts +2 -1
- package/lib/generates/internal/SdkAliasCollection.js +8 -4
- package/lib/generates/internal/SdkAliasCollection.js.map +1 -1
- package/lib/generates/internal/SdkDistributionComposer.js +72 -14
- package/lib/generates/internal/SdkDistributionComposer.js.map +1 -1
- package/lib/generates/internal/SdkFileProgrammer.js +83 -12
- package/lib/generates/internal/SdkFileProgrammer.js.map +1 -1
- package/lib/generates/internal/SdkHttpFunctionProgrammer.js +21 -23
- package/lib/generates/internal/SdkHttpFunctionProgrammer.js.map +1 -1
- package/lib/generates/internal/SdkHttpNamespaceProgrammer.js +27 -34
- package/lib/generates/internal/SdkHttpNamespaceProgrammer.js.map +1 -1
- package/lib/generates/internal/SdkHttpParameterProgrammer.d.ts +40 -1
- package/lib/generates/internal/SdkHttpParameterProgrammer.js +105 -6
- package/lib/generates/internal/SdkHttpParameterProgrammer.js.map +1 -1
- package/lib/generates/internal/SdkHttpRouteProgrammer.js +16 -32
- package/lib/generates/internal/SdkHttpRouteProgrammer.js.map +1 -1
- package/lib/generates/internal/SdkHttpSimulationProgrammer.js +18 -14
- package/lib/generates/internal/SdkHttpSimulationProgrammer.js.map +1 -1
- package/lib/generates/internal/SdkMcpRouteProgrammer.js +39 -11
- package/lib/generates/internal/SdkMcpRouteProgrammer.js.map +1 -1
- package/lib/generates/internal/SdkPathTemplate.d.ts +21 -0
- package/lib/generates/internal/SdkPathTemplate.js +50 -0
- package/lib/generates/internal/SdkPathTemplate.js.map +1 -0
- package/lib/generates/internal/SdkTypeProgrammer.js +15 -15
- package/lib/generates/internal/SdkTypeProgrammer.js.map +1 -1
- package/lib/generates/internal/SdkTypeTagProgrammer.d.ts +20 -1
- package/lib/generates/internal/SdkTypeTagProgrammer.js +386 -67
- package/lib/generates/internal/SdkTypeTagProgrammer.js.map +1 -1
- package/lib/generates/internal/SdkWebSocketNamespaceProgrammer.js +27 -36
- package/lib/generates/internal/SdkWebSocketNamespaceProgrammer.js.map +1 -1
- package/lib/generates/internal/SdkWebSocketParameterProgrammer.d.ts +35 -1
- package/lib/generates/internal/SdkWebSocketParameterProgrammer.js +72 -5
- package/lib/generates/internal/SdkWebSocketParameterProgrammer.js.map +1 -1
- package/lib/generates/internal/SdkWebSocketRouteProgrammer.js +57 -33
- package/lib/generates/internal/SdkWebSocketRouteProgrammer.js.map +1 -1
- package/lib/generates/internal/SwaggerOperationComposer.js +19 -8
- package/lib/generates/internal/SwaggerOperationComposer.js.map +1 -1
- package/lib/generates/internal/SwaggerOperationParameterComposer.d.ts +1 -1
- package/lib/generates/internal/SwaggerOperationParameterComposer.js +201 -19
- package/lib/generates/internal/SwaggerOperationParameterComposer.js.map +1 -1
- package/lib/generates/internal/SwaggerOperationResponseComposer.d.ts +2 -0
- package/lib/generates/internal/SwaggerOperationResponseComposer.js +39 -7
- package/lib/generates/internal/SwaggerOperationResponseComposer.js.map +1 -1
- package/lib/internal/legacy.d.ts +49 -18
- package/lib/internal/legacy.js +84 -25
- package/lib/internal/legacy.js.map +1 -1
- package/lib/structures/IOperationMetadata.d.ts +22 -0
- package/lib/structures/IReflectHttpOperationException.d.ts +3 -2
- package/lib/structures/IReflectHttpOperationParameter.d.ts +8 -3
- package/lib/structures/IReflectHttpOperationSuccess.d.ts +4 -3
- package/lib/structures/ITypedHttpRoute.d.ts +8 -0
- package/lib/structures/ITypedHttpRouteException.d.ts +3 -1
- package/lib/structures/ITypedHttpRouteParameter.d.ts +3 -1
- package/lib/structures/ITypedHttpRouteSuccess.d.ts +3 -1
- package/lib/structures/ITypedWebSocketRoute.d.ts +8 -0
- package/lib/utils/SourceFinder.d.ts +10 -0
- package/lib/utils/SourceFinder.js +27 -4
- package/lib/utils/SourceFinder.js.map +1 -1
- package/lib/utils/VersioningStrategy.d.ts +13 -1
- package/lib/utils/VersioningStrategy.js +24 -12
- package/lib/utils/VersioningStrategy.js.map +1 -1
- package/native/go.mod +1 -1
- package/native/go.sum +2 -2
- package/native/sdk/sdk_http_rules.go +210 -0
- package/native/sdk/sdk_metadata_json.go +73 -3
- package/native/sdk/sdk_transform.go +801 -49
- package/package.json +8 -8
- package/src/INestiaConfig.ts +19 -10
- package/src/NestiaSdkApplication.ts +63 -34
- package/src/NestiaSwaggerComposer.ts +11 -8
- package/src/analyses/ConfigAnalyzer.ts +3 -3
- package/src/analyses/ParameterNameAnalyzer.ts +51 -0
- package/src/analyses/PathAnalyzer.ts +106 -14
- package/src/analyses/ReflectControllerAnalyzer.ts +2 -1
- package/src/analyses/ReflectHttpOperationAnalyzer.ts +4 -4
- package/src/analyses/ReflectHttpOperationParameterAnalyzer.ts +122 -39
- package/src/analyses/ReflectHttpOperationResponseAnalyzer.ts +6 -7
- package/src/analyses/ReflectMcpOperationAnalyzer.ts +6 -2
- package/src/analyses/ReflectWebSocketOperationAnalyzer.ts +7 -3
- package/src/analyses/SecurityAnalyzer.ts +30 -20
- package/src/analyses/SwaggerExampleAnalyzer.ts +23 -0
- package/src/analyses/TypedHttpRouteAnalyzer.ts +29 -20
- package/src/analyses/TypedWebSocketRouteAnalyzer.ts +1 -0
- package/src/executable/internal/NestiaSdkWatcher.ts +2 -2
- package/src/executable/sdk.ts +24 -1
- package/src/factories/ExpressionFactory.ts +8 -2
- package/src/factories/LiteralFactory.ts +7 -2
- package/src/generates/SdkGenerator.ts +3 -3
- package/src/generates/SwaggerGenerator.ts +173 -32
- package/src/generates/internal/FilePrinter.ts +28 -0
- package/src/generates/internal/ImportDictionary.ts +11 -0
- package/src/generates/internal/SdkAliasCollection.ts +11 -4
- package/src/generates/internal/SdkDistributionComposer.ts +89 -15
- package/src/generates/internal/SdkFileProgrammer.ts +108 -16
- package/src/generates/internal/SdkHttpFunctionProgrammer.ts +28 -30
- package/src/generates/internal/SdkHttpNamespaceProgrammer.ts +34 -64
- package/src/generates/internal/SdkHttpParameterProgrammer.ts +193 -13
- package/src/generates/internal/SdkHttpRouteProgrammer.ts +17 -31
- package/src/generates/internal/SdkHttpSimulationProgrammer.ts +51 -14
- package/src/generates/internal/SdkMcpRouteProgrammer.ts +112 -15
- package/src/generates/internal/SdkPathTemplate.ts +73 -0
- package/src/generates/internal/SdkTypeProgrammer.ts +19 -22
- package/src/generates/internal/SdkTypeTagProgrammer.ts +506 -78
- package/src/generates/internal/SdkWebSocketNamespaceProgrammer.ts +43 -68
- package/src/generates/internal/SdkWebSocketParameterProgrammer.ts +126 -5
- package/src/generates/internal/SdkWebSocketRouteProgrammer.ts +71 -48
- package/src/generates/internal/SwaggerOperationComposer.ts +20 -6
- package/src/generates/internal/SwaggerOperationParameterComposer.ts +279 -30
- package/src/generates/internal/SwaggerOperationResponseComposer.ts +55 -11
- package/src/internal/legacy.ts +114 -26
- package/src/structures/IOperationMetadata.ts +28 -0
- package/src/structures/IReflectHttpOperationException.ts +7 -2
- package/src/structures/IReflectHttpOperationParameter.ts +9 -2
- package/src/structures/IReflectHttpOperationSuccess.ts +8 -3
- package/src/structures/ITypedHttpRoute.ts +8 -0
- package/src/structures/ITypedHttpRouteException.ts +4 -1
- package/src/structures/ITypedHttpRouteParameter.ts +3 -1
- package/src/structures/ITypedHttpRouteSuccess.ts +4 -1
- package/src/structures/ITypedWebSocketRoute.ts +8 -0
- package/src/utils/SourceFinder.ts +25 -4
- package/src/utils/VersioningStrategy.ts +28 -13
- package/lib/validators/HttpHeadersValidator.d.ts +0 -11
- package/lib/validators/HttpHeadersValidator.js +0 -15
- package/lib/validators/HttpHeadersValidator.js.map +0 -1
- package/lib/validators/HttpQueryValidator.d.ts +0 -10
- package/lib/validators/HttpQueryValidator.js +0 -14
- package/lib/validators/HttpQueryValidator.js.map +0 -1
- package/src/validators/HttpHeadersValidator.ts +0 -12
- package/src/validators/HttpQueryValidator.ts +0 -11
|
@@ -22,7 +22,7 @@ export namespace SwaggerOperationComposer {
|
|
|
22
22
|
...SwaggerDescriptionComposer.getJsDocTexts({
|
|
23
23
|
jsDocTags: props.route.jsDocTags,
|
|
24
24
|
name: "tag",
|
|
25
|
-
}).map((t) => t.split(
|
|
25
|
+
}).map((t) => t.trim().split(/\s+/)[0]!),
|
|
26
26
|
]);
|
|
27
27
|
if (tags.size) {
|
|
28
28
|
props.document.tags ??= [];
|
|
@@ -33,11 +33,13 @@ export namespace SwaggerOperationComposer {
|
|
|
33
33
|
jsDocTags: props.route.jsDocTags,
|
|
34
34
|
name: "tag",
|
|
35
35
|
})) {
|
|
36
|
-
|
|
36
|
+
// the name is the first word; a tag's text may run over lines
|
|
37
|
+
const name: string = texts.trim().split(/\s+/)[0]!;
|
|
38
|
+
const description: string = texts.trim().substring(name.length).trim();
|
|
37
39
|
if (description.length)
|
|
38
40
|
props.document.tags.find(
|
|
39
41
|
(elem) => elem.name === name,
|
|
40
|
-
)!.description ??= description
|
|
42
|
+
)!.description ??= description;
|
|
41
43
|
}
|
|
42
44
|
}
|
|
43
45
|
|
|
@@ -52,8 +54,7 @@ export namespace SwaggerOperationComposer {
|
|
|
52
54
|
? [{}]
|
|
53
55
|
: tag.text.map((text) => {
|
|
54
56
|
const line: string[] = text.text
|
|
55
|
-
.split(
|
|
56
|
-
.filter((s) => s.trim())
|
|
57
|
+
.split(/\s+/)
|
|
57
58
|
.filter((s) => !!s.length);
|
|
58
59
|
if (line.length === 0) return {};
|
|
59
60
|
return {
|
|
@@ -79,7 +80,7 @@ export namespace SwaggerOperationComposer {
|
|
|
79
80
|
props.route.operationId ??
|
|
80
81
|
props.config.operationId?.({
|
|
81
82
|
class: props.route.controller.class.name,
|
|
82
|
-
function: props.route.
|
|
83
|
+
function: props.route.key,
|
|
83
84
|
method: props.route.method as "GET",
|
|
84
85
|
path: props.route.path,
|
|
85
86
|
}),
|
|
@@ -87,6 +88,7 @@ export namespace SwaggerOperationComposer {
|
|
|
87
88
|
...props.route.pathParameters,
|
|
88
89
|
...props.route.queryParameters,
|
|
89
90
|
...(props.route.queryObject ? [props.route.queryObject] : []),
|
|
91
|
+
...props.route.headerParameters,
|
|
90
92
|
...(props.route.headerObject ? [props.route.headerObject] : []),
|
|
91
93
|
]
|
|
92
94
|
.map((p) =>
|
|
@@ -101,6 +103,7 @@ export namespace SwaggerOperationComposer {
|
|
|
101
103
|
.flat(),
|
|
102
104
|
requestBody: writeRequestBody(props),
|
|
103
105
|
responses: SwaggerOperationResponseComposer.compose({
|
|
106
|
+
config: props.config,
|
|
104
107
|
schema: props.schema,
|
|
105
108
|
route: props.route,
|
|
106
109
|
}),
|
|
@@ -108,6 +111,13 @@ export namespace SwaggerOperationComposer {
|
|
|
108
111
|
...(props.route.extensions ?? {}),
|
|
109
112
|
"x-samchon-accessor": props.route.accessor,
|
|
110
113
|
"x-samchon-controller": props.route.controller.class.name,
|
|
114
|
+
...(props.config.additional === true
|
|
115
|
+
? {
|
|
116
|
+
"x-nestia-method": props.route.method,
|
|
117
|
+
"x-nestia-namespace": props.route.accessor.join("."),
|
|
118
|
+
"x-nestia-jsDocTags": props.route.jsDocTags,
|
|
119
|
+
}
|
|
120
|
+
: {}),
|
|
111
121
|
};
|
|
112
122
|
};
|
|
113
123
|
}
|
|
@@ -135,6 +145,8 @@ export namespace SwaggerOperationComposer {
|
|
|
135
145
|
* helper across two rules that disagree would trade one defect for another.
|
|
136
146
|
*/
|
|
137
147
|
const writeRequestBody = (props: {
|
|
148
|
+
config: Omit<INestiaConfig.ISwaggerConfig, "output">;
|
|
149
|
+
document: OpenApi.IDocument;
|
|
138
150
|
schema: (metadata: MetadataSchema) => OpenApi.IJsonSchema | undefined;
|
|
139
151
|
route: ITypedHttpRoute;
|
|
140
152
|
}): OpenApi.IOperation.IRequestBody | undefined => {
|
|
@@ -144,6 +156,8 @@ const writeRequestBody = (props: {
|
|
|
144
156
|
);
|
|
145
157
|
if (schema === undefined) return undefined;
|
|
146
158
|
return SwaggerOperationParameterComposer.body({
|
|
159
|
+
config: props.config,
|
|
160
|
+
document: props.document,
|
|
147
161
|
schema,
|
|
148
162
|
jsDocTags: props.route.jsDocTags,
|
|
149
163
|
parameter: props.route.body,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { OpenApi } from "@typia/interface";
|
|
2
|
+
import { OpenApiTypeChecker } from "@typia/utils";
|
|
2
3
|
import { VariadicSingleton } from "tstl";
|
|
3
4
|
import { IJsDocTagInfo, IJsonSchemaCollection } from "typia";
|
|
4
5
|
|
|
@@ -6,7 +7,9 @@ import { INestiaConfig } from "../../INestiaConfig";
|
|
|
6
7
|
import {
|
|
7
8
|
JsonSchemasProgrammer,
|
|
8
9
|
MetadataObjectType,
|
|
10
|
+
MetadataProperty,
|
|
9
11
|
isRequiredOf,
|
|
12
|
+
isSoleLiteralOf,
|
|
10
13
|
} from "../../internal/legacy";
|
|
11
14
|
import { ITypedHttpRouteParameter } from "../../structures/ITypedHttpRouteParameter";
|
|
12
15
|
import { SwaggerDescriptionComposer } from "./SwaggerDescriptionComposer";
|
|
@@ -33,8 +36,8 @@ export namespace SwaggerOperationParameterComposer {
|
|
|
33
36
|
: header({ ...props, parameter: props.parameter });
|
|
34
37
|
|
|
35
38
|
export const body = (
|
|
36
|
-
props:
|
|
37
|
-
): OpenApi.IOperation.IRequestBody => {
|
|
39
|
+
props: IProps<ITypedHttpRouteParameter.IBody>,
|
|
40
|
+
): OpenApi.IOperation.IRequestBody | undefined => {
|
|
38
41
|
const description: string | undefined =
|
|
39
42
|
props.parameter.description ??
|
|
40
43
|
SwaggerDescriptionComposer.descriptionFromJsDocTag({
|
|
@@ -42,7 +45,7 @@ export namespace SwaggerOperationParameterComposer {
|
|
|
42
45
|
tag: "param",
|
|
43
46
|
parameter: props.parameter.name,
|
|
44
47
|
});
|
|
45
|
-
|
|
48
|
+
const output: OpenApi.IOperation.IRequestBody = {
|
|
46
49
|
description: props.parameter.encrypted
|
|
47
50
|
? `${warning.get(!!description)}${description ?? ""}`
|
|
48
51
|
: description,
|
|
@@ -56,6 +59,68 @@ export namespace SwaggerOperationParameterComposer {
|
|
|
56
59
|
required: props.parameter.metadata.required,
|
|
57
60
|
...(props.parameter.encrypted ? { "x-nestia-encrypted": true } : {}),
|
|
58
61
|
};
|
|
62
|
+
return props.config.openapi === "2.0"
|
|
63
|
+
? swaggerV2Body(props, output)
|
|
64
|
+
: output;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The request body a Swagger 2.0 document can hold.
|
|
69
|
+
*
|
|
70
|
+
* Typia's downgrader refuses what 2.0 has no place for rather than lose it,
|
|
71
|
+
* so one route used to fail the whole document (#1649). A 2.0 body parameter
|
|
72
|
+
* holds only a schema: no example, and no `x-nestia-encrypted` flag, whose
|
|
73
|
+
* warning the description keeps. A form becomes one `formData` parameter per
|
|
74
|
+
* field, which carry neither the body's description nor the form object's own
|
|
75
|
+
* attributes, and are each required or not; so a form keeps only its fields,
|
|
76
|
+
* is required when one of them is, and without fields has no body to list. A
|
|
77
|
+
* `formData` file is one file, never an array, a union, or null, so a field
|
|
78
|
+
* taking several files, or none, is listed as one file, optional unless it
|
|
79
|
+
* requires one ({@link swaggerV2FormField}).
|
|
80
|
+
*/
|
|
81
|
+
const swaggerV2Body = (
|
|
82
|
+
props: IProps<ITypedHttpRouteParameter.IBody>,
|
|
83
|
+
body: OpenApi.IOperation.IRequestBody,
|
|
84
|
+
): OpenApi.IOperation.IRequestBody | undefined => {
|
|
85
|
+
const contentType: string = props.parameter.contentType;
|
|
86
|
+
if (
|
|
87
|
+
contentType !== "multipart/form-data" &&
|
|
88
|
+
contentType !== "application/x-www-form-urlencoded"
|
|
89
|
+
)
|
|
90
|
+
return {
|
|
91
|
+
description: body.description,
|
|
92
|
+
content: { [contentType]: { schema: props.schema } },
|
|
93
|
+
required: body.required,
|
|
94
|
+
};
|
|
95
|
+
const object: OpenApi.IJsonSchema.IObject | undefined = resolveObject(
|
|
96
|
+
props.document,
|
|
97
|
+
props.schema,
|
|
98
|
+
);
|
|
99
|
+
if (
|
|
100
|
+
object === undefined ||
|
|
101
|
+
Object.keys(object.properties ?? {}).length === 0
|
|
102
|
+
)
|
|
103
|
+
return undefined;
|
|
104
|
+
const fields: [string, ISwaggerV2FormField][] = Object.entries(
|
|
105
|
+
object.properties ?? {},
|
|
106
|
+
).map(([key, value]) => [key, swaggerV2FormField(value)]);
|
|
107
|
+
const required: string[] = (object.required ?? []).filter(
|
|
108
|
+
(key) => fields.find(([name]) => name === key)?.[1].optional !== true,
|
|
109
|
+
);
|
|
110
|
+
return {
|
|
111
|
+
content: {
|
|
112
|
+
[contentType]: {
|
|
113
|
+
schema: {
|
|
114
|
+
type: "object",
|
|
115
|
+
properties: Object.fromEntries(
|
|
116
|
+
fields.map(([key, field]) => [key, field.schema]),
|
|
117
|
+
),
|
|
118
|
+
required,
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
required: required.length !== 0,
|
|
123
|
+
};
|
|
59
124
|
};
|
|
60
125
|
|
|
61
126
|
const path = (
|
|
@@ -103,48 +168,76 @@ export namespace SwaggerOperationParameterComposer {
|
|
|
103
168
|
ITypedHttpRouteParameter.IHeaders | ITypedHttpRouteParameter.IQuery
|
|
104
169
|
>,
|
|
105
170
|
): OpenApi.IOperation.IParameter[] => {
|
|
171
|
+
const object: MetadataObjectType | undefined = props.parameter.metadata
|
|
172
|
+
.objects[0]?.type as MetadataObjectType | undefined;
|
|
106
173
|
const param: OpenApi.IOperation.IParameter = {
|
|
107
174
|
name: props.parameter.field ?? props.parameter.name,
|
|
108
175
|
in: props.parameter.category === "query" ? "query" : "header",
|
|
109
176
|
schema: props.schema,
|
|
110
177
|
description: parameterDescription(props),
|
|
111
|
-
|
|
178
|
+
// An unnamed object's keys are the request's own query keys or headers,
|
|
179
|
+
// so it is mandatory only when one of the keys the document describes
|
|
180
|
+
// is; an object whose described properties are all optional is satisfied
|
|
181
|
+
// when the request sends none of them. A field-named parameter is one
|
|
182
|
+
// key, required as declared.
|
|
183
|
+
required:
|
|
184
|
+
props.parameter.metadata.required &&
|
|
185
|
+
(props.parameter.field !== null ||
|
|
186
|
+
(object?.properties.some(
|
|
187
|
+
(p) => isDescribed(p) && isRequiredOf(p.value),
|
|
188
|
+
) ??
|
|
189
|
+
true)),
|
|
112
190
|
example: props.parameter.example,
|
|
113
191
|
examples: props.parameter.examples,
|
|
114
192
|
};
|
|
193
|
+
// A field-named parameter is one key, which typia's HTTP rules (#1648)
|
|
194
|
+
// keep atomic, and a query or headers object passed them: one object of
|
|
195
|
+
// statically named atomic or array-of-atomic properties, so each has a
|
|
196
|
+
// name to give its parameter.
|
|
197
|
+
if (props.parameter.field !== null || object === undefined) return [param];
|
|
198
|
+
// `decompose: false` keeps a query object one parameter where the format
|
|
199
|
+
// can say so: OpenAPI 3.x spreads it into its keys with `style: form` and
|
|
200
|
+
// `explode: true`. Swagger 2.0 has no object query parameter, and no
|
|
201
|
+
// format spreads an object into headers, so one parameter would be a key
|
|
202
|
+
// or header no request carries (#1653); those are always decomposed.
|
|
115
203
|
if (
|
|
116
|
-
props.config.decompose === false
|
|
117
|
-
props.parameter.
|
|
204
|
+
props.config.decompose === false &&
|
|
205
|
+
props.parameter.category === "query" &&
|
|
206
|
+
props.config.openapi !== "2.0"
|
|
118
207
|
)
|
|
119
|
-
return [param];
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
208
|
+
return [{ ...param, style: "form", explode: true }];
|
|
209
|
+
// One parameter per property typia's object schema describes, so the
|
|
210
|
+
// decomposed form says what `decompose: false` would say about the object:
|
|
211
|
+
// the property's schema, and in the parameter's own fields its
|
|
212
|
+
// description, deprecation, and share of the object's examples.
|
|
213
|
+
return object.properties
|
|
214
|
+
.filter(isDescribed)
|
|
215
|
+
.map((p): IDecomposedParameter | null => {
|
|
216
|
+
const key: string = String(p.key.constants[0]!.values[0]!.value);
|
|
217
|
+
const json: IJsonSchemaCollection | null =
|
|
218
|
+
JsonSchemasProgrammer.writeProperty({
|
|
219
|
+
version: "3.1",
|
|
220
|
+
metadata: props.parameter.metadata,
|
|
221
|
+
key,
|
|
222
|
+
value: p.value,
|
|
223
|
+
});
|
|
224
|
+
if (json === null) return null;
|
|
133
225
|
SwaggerReadonlyArrayEmender.emend({
|
|
134
226
|
components: json.components,
|
|
135
227
|
schema: json.schemas[0],
|
|
136
228
|
metadata: p.value,
|
|
137
229
|
});
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
230
|
+
// Only what the document lacks: the parameter's own schema already
|
|
231
|
+
// brought every component its properties reach, emended for readonly
|
|
232
|
+
// arrays, and this copy is not.
|
|
233
|
+
props.document.components ??= {};
|
|
234
|
+
props.document.components.schemas ??= {};
|
|
235
|
+
for (const [name, schema] of Object.entries(
|
|
236
|
+
json.components.schemas ?? {},
|
|
237
|
+
))
|
|
238
|
+
props.document.components.schemas[name] ??= schema;
|
|
146
239
|
return {
|
|
147
|
-
name:
|
|
240
|
+
name: key,
|
|
148
241
|
in: props.parameter.category === "query" ? "query" : "header",
|
|
149
242
|
schema: json.schemas[0]!,
|
|
150
243
|
required: isRequiredOf(p.value),
|
|
@@ -153,11 +246,167 @@ export namespace SwaggerOperationParameterComposer {
|
|
|
153
246
|
jsDocTags: p.jsDocTags,
|
|
154
247
|
kind: "title",
|
|
155
248
|
}).description,
|
|
249
|
+
// Swagger 2.0 defines no `deprecated` on a parameter, and the
|
|
250
|
+
// downgrader copies the field through rather than refusing it.
|
|
251
|
+
deprecated:
|
|
252
|
+
props.config.openapi !== "2.0" &&
|
|
253
|
+
p.jsDocTags.some((tag) => tag.name === "deprecated")
|
|
254
|
+
? true
|
|
255
|
+
: undefined,
|
|
256
|
+
example: memberOf(props.parameter.example, key),
|
|
257
|
+
examples: membersOf(props.parameter.examples, key),
|
|
156
258
|
};
|
|
157
|
-
})
|
|
259
|
+
})
|
|
260
|
+
.filter((p): p is IDecomposedParameter => p !== null);
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* A decomposed parameter. OpenAPI 3.0 through 3.2 define `deprecated` on the
|
|
266
|
+
* Parameter Object, which typia's `OpenApi.IOperation.IParameter` does not
|
|
267
|
+
* model; its 3.x downgraders carry the field through unchanged.
|
|
268
|
+
*/
|
|
269
|
+
type IDecomposedParameter = OpenApi.IOperation.IParameter & {
|
|
270
|
+
deprecated?: boolean;
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Whether the document can describe a property on its own: it has a literal key
|
|
275
|
+
* to name it, and none of the `@hidden`, `@ignore`, and `@internal` tags
|
|
276
|
+
* typia's object schema drops it for.
|
|
277
|
+
*/
|
|
278
|
+
const isDescribed = (p: MetadataProperty): boolean =>
|
|
279
|
+
isSoleLiteralOf(p.key) &&
|
|
280
|
+
p.jsDocTags.every(
|
|
281
|
+
(tag) =>
|
|
282
|
+
tag.name !== "hidden" && tag.name !== "ignore" && tag.name !== "internal",
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
/** A form field as a Swagger 2.0 `formData` parameter holds it. */
|
|
286
|
+
interface ISwaggerV2FormField {
|
|
287
|
+
schema: OpenApi.IJsonSchema;
|
|
288
|
+
|
|
289
|
+
/** Whether a request may leave the field out though its type requires it. */
|
|
290
|
+
optional: boolean;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* A form field as a Swagger 2.0 `formData` parameter can hold it.
|
|
295
|
+
*
|
|
296
|
+
* A 2.0 file is one file: never an array, a union, or null. A field whose value
|
|
297
|
+
* is made of files, a union of files or one of them or null, or an array of
|
|
298
|
+
* those, becomes the one file a request may carry, keeping the field's title,
|
|
299
|
+
* description, and deprecation. A request may then carry none, since the server
|
|
300
|
+
* reads a missing field as null or an empty array, so such a field is not
|
|
301
|
+
* required unless its array requires an item; a plain file is kept as it is.
|
|
302
|
+
*/
|
|
303
|
+
const swaggerV2FormField = (
|
|
304
|
+
schema: OpenApi.IJsonSchema,
|
|
305
|
+
): ISwaggerV2FormField => {
|
|
306
|
+
const file: IFileOf | undefined = fileOf(schema, true);
|
|
307
|
+
if (file === undefined || file.binary === schema)
|
|
308
|
+
return { schema, optional: false };
|
|
309
|
+
const attributes = schema as OpenApi.IJsonSchema.IString;
|
|
310
|
+
return {
|
|
311
|
+
schema: {
|
|
312
|
+
...file.binary,
|
|
313
|
+
...(attributes.title !== undefined ? { title: attributes.title } : {}),
|
|
314
|
+
...(attributes.description !== undefined
|
|
315
|
+
? { description: attributes.description }
|
|
316
|
+
: {}),
|
|
317
|
+
...(attributes.deprecated !== undefined
|
|
318
|
+
? { deprecated: attributes.deprecated }
|
|
319
|
+
: {}),
|
|
320
|
+
},
|
|
321
|
+
optional: file.optional,
|
|
158
322
|
};
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
interface IFileOf {
|
|
326
|
+
binary: OpenApi.IJsonSchema;
|
|
327
|
+
optional: boolean;
|
|
159
328
|
}
|
|
160
329
|
|
|
330
|
+
/** The file a field's value is made of, if it is made of files only. */
|
|
331
|
+
const fileOf = (
|
|
332
|
+
schema: OpenApi.IJsonSchema,
|
|
333
|
+
top: boolean,
|
|
334
|
+
): IFileOf | undefined => {
|
|
335
|
+
if (isBinary(schema)) return { binary: schema, optional: false };
|
|
336
|
+
if (OpenApiTypeChecker.isOneOf(schema)) {
|
|
337
|
+
const members: OpenApi.IJsonSchema[] = schema.oneOf.filter(
|
|
338
|
+
(s) => OpenApiTypeChecker.isNull(s) === false,
|
|
339
|
+
);
|
|
340
|
+
const files: Array<IFileOf | undefined> = members.map((s) =>
|
|
341
|
+
fileOf(s, top),
|
|
342
|
+
);
|
|
343
|
+
if (files.length === 0 || files.some((f) => f === undefined))
|
|
344
|
+
return undefined;
|
|
345
|
+
return {
|
|
346
|
+
binary: files[0]!.binary,
|
|
347
|
+
optional:
|
|
348
|
+
members.length !== schema.oneOf.length ||
|
|
349
|
+
files.some((f) => f!.optional),
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
if (top && OpenApiTypeChecker.isArray(schema)) {
|
|
353
|
+
const item: IFileOf | undefined = fileOf(schema.items, false);
|
|
354
|
+
// an empty array is a request without the field, unless one file is due
|
|
355
|
+
return item === undefined
|
|
356
|
+
? undefined
|
|
357
|
+
: { binary: item.binary, optional: (schema.minItems ?? 0) === 0 };
|
|
358
|
+
}
|
|
359
|
+
return undefined;
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const isBinary = (schema: OpenApi.IJsonSchema): boolean =>
|
|
363
|
+
OpenApiTypeChecker.isString(schema) && schema.format === "binary";
|
|
364
|
+
|
|
365
|
+
/** The object schema `schema` is or refers to, if it is one. */
|
|
366
|
+
const resolveObject = (
|
|
367
|
+
document: OpenApi.IDocument,
|
|
368
|
+
schema: OpenApi.IJsonSchema,
|
|
369
|
+
): OpenApi.IJsonSchema.IObject | undefined => {
|
|
370
|
+
const visited: Set<string> = new Set();
|
|
371
|
+
while (OpenApiTypeChecker.isReference(schema)) {
|
|
372
|
+
if (visited.has(schema.$ref)) return undefined;
|
|
373
|
+
visited.add(schema.$ref);
|
|
374
|
+
const next: OpenApi.IJsonSchema | undefined =
|
|
375
|
+
document.components?.schemas?.[
|
|
376
|
+
schema.$ref.substring("#/components/schemas/".length)
|
|
377
|
+
];
|
|
378
|
+
if (next === undefined) return undefined;
|
|
379
|
+
schema = next;
|
|
380
|
+
}
|
|
381
|
+
return OpenApiTypeChecker.isObject(schema) ? schema : undefined;
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
/** The `key` member of an object example, if the example has one. */
|
|
385
|
+
const memberOf = (example: unknown, key: string): unknown =>
|
|
386
|
+
typeof example === "object" &&
|
|
387
|
+
example !== null &&
|
|
388
|
+
Object.prototype.hasOwnProperty.call(example, key)
|
|
389
|
+
? (example as Record<string, unknown>)[key]
|
|
390
|
+
: undefined;
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Named object examples narrowed to the ones whose value has a `key` member,
|
|
394
|
+
* each an Example Object of that member.
|
|
395
|
+
*/
|
|
396
|
+
const membersOf = (
|
|
397
|
+
examples: Record<string, OpenApi.IExample> | undefined,
|
|
398
|
+
key: string,
|
|
399
|
+
): Record<string, OpenApi.IExample> | undefined => {
|
|
400
|
+
if (examples === undefined) return undefined;
|
|
401
|
+
const entries: [string, OpenApi.IExample][] = Object.entries(examples)
|
|
402
|
+
.map(([name, example]): [string, OpenApi.IExample] => [
|
|
403
|
+
name,
|
|
404
|
+
{ ...example, value: memberOf(example.value, key) },
|
|
405
|
+
])
|
|
406
|
+
.filter(([, example]) => example.value !== undefined);
|
|
407
|
+
return entries.length !== 0 ? Object.fromEntries(entries) : undefined;
|
|
408
|
+
};
|
|
409
|
+
|
|
161
410
|
const warning = new VariadicSingleton((described: boolean): string => {
|
|
162
411
|
const summary = "Request body must be encrypted.";
|
|
163
412
|
const component =
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { OpenApi } from "@typia/interface";
|
|
2
2
|
import { VariadicSingleton } from "tstl";
|
|
3
3
|
|
|
4
|
+
import { INestiaConfig } from "../../INestiaConfig";
|
|
4
5
|
import { MetadataSchema } from "../../internal/legacy";
|
|
5
6
|
import { ITypedHttpRoute } from "../../structures/ITypedHttpRoute";
|
|
6
7
|
import { StringUtil } from "../../utils/StringUtil";
|
|
@@ -8,9 +9,17 @@ import { SwaggerDescriptionComposer } from "./SwaggerDescriptionComposer";
|
|
|
8
9
|
|
|
9
10
|
export namespace SwaggerOperationResponseComposer {
|
|
10
11
|
export const compose = (props: {
|
|
12
|
+
config: Omit<INestiaConfig.ISwaggerConfig, "output">;
|
|
11
13
|
schema: (metadata: MetadataSchema) => OpenApi.IJsonSchema | undefined;
|
|
12
14
|
route: ITypedHttpRoute;
|
|
13
15
|
}): Record<string, OpenApi.IOperation.IResponse> => {
|
|
16
|
+
// Swagger 2.0 keys a response's examples by MIME type, one each, so it has
|
|
17
|
+
// no named examples; the downgrader refuses the whole document rather than
|
|
18
|
+
// lose them (#1649). The single example still fits.
|
|
19
|
+
const named = (
|
|
20
|
+
examples: Record<string, OpenApi.IExample> | undefined,
|
|
21
|
+
): Record<string, OpenApi.IExample> | undefined =>
|
|
22
|
+
props.config.openapi === "2.0" ? undefined : examples;
|
|
14
23
|
const output: Record<string, OpenApi.IOperation.IResponse> = {};
|
|
15
24
|
// FROM DECORATOR
|
|
16
25
|
for (const [status, error] of Object.entries(props.route.exceptions))
|
|
@@ -20,7 +29,7 @@ export namespace SwaggerOperationResponseComposer {
|
|
|
20
29
|
contentType: "application/json",
|
|
21
30
|
schema: props.schema(error.metadata),
|
|
22
31
|
example: error.example,
|
|
23
|
-
examples: error.examples,
|
|
32
|
+
examples: named(error.examples),
|
|
24
33
|
}),
|
|
25
34
|
};
|
|
26
35
|
|
|
@@ -32,8 +41,8 @@ export namespace SwaggerOperationResponseComposer {
|
|
|
32
41
|
)?.text;
|
|
33
42
|
if (text === undefined) continue;
|
|
34
43
|
|
|
35
|
-
|
|
36
|
-
const status: string =
|
|
44
|
+
// the status is the first word; a tag's text may run over lines
|
|
45
|
+
const status: string = text.trim().split(/\s+/)[0]!;
|
|
37
46
|
if (
|
|
38
47
|
isNaN(Number(status)) &&
|
|
39
48
|
status !== "2XX" &&
|
|
@@ -43,8 +52,8 @@ export namespace SwaggerOperationResponseComposer {
|
|
|
43
52
|
)
|
|
44
53
|
continue;
|
|
45
54
|
|
|
46
|
-
const
|
|
47
|
-
|
|
55
|
+
const rest: string = text.trim().substring(status.length).trim();
|
|
56
|
+
const description: string | undefined = rest.length ? rest : undefined;
|
|
48
57
|
const oldbie = output[status];
|
|
49
58
|
if (description && oldbie !== undefined)
|
|
50
59
|
oldbie.description ??= description;
|
|
@@ -75,10 +84,11 @@ export namespace SwaggerOperationResponseComposer {
|
|
|
75
84
|
jsDocTags: props.route.jsDocTags,
|
|
76
85
|
tag: "return",
|
|
77
86
|
});
|
|
78
|
-
|
|
87
|
+
const status: string = String(
|
|
79
88
|
props.route.success.status ??
|
|
80
|
-
(props.route.method.toLowerCase() === "post" ? 201 : 200)
|
|
81
|
-
|
|
89
|
+
(props.route.method.toLowerCase() === "post" ? 201 : 200),
|
|
90
|
+
);
|
|
91
|
+
output[status] = {
|
|
82
92
|
description: props.route.success.encrypted
|
|
83
93
|
? `${warning.get(!!description, props.route.method)}${description ?? ""}`
|
|
84
94
|
: (description ?? ""),
|
|
@@ -91,12 +101,46 @@ export namespace SwaggerOperationResponseComposer {
|
|
|
91
101
|
}
|
|
92
102
|
: props.schema(props.route.success.metadata),
|
|
93
103
|
example: props.route.success.example,
|
|
94
|
-
examples: props.route.success.examples,
|
|
104
|
+
examples: named(props.route.success.examples),
|
|
95
105
|
}),
|
|
96
|
-
|
|
106
|
+
// Swagger 2.0 has no place for the flag, and the downgrader refuses the
|
|
107
|
+
// whole document rather than lose it; the description keeps the warning.
|
|
108
|
+
...(props.route.success.encrypted && props.config.openapi !== "2.0"
|
|
109
|
+
? { "x-nestia-encrypted": true }
|
|
110
|
+
: {}),
|
|
97
111
|
};
|
|
112
|
+
if (props.config.openapi === "2.0") swaggerV2Produces(output, status);
|
|
98
113
|
return output;
|
|
99
114
|
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Makes every response body of a Swagger 2.0 operation share one media type.
|
|
118
|
+
*
|
|
119
|
+
* 2.0 declares the media types an operation `produces` once for all of its
|
|
120
|
+
* responses, and the downgrader refuses an operation whose responses disagree
|
|
121
|
+
* rather than list a type a response does not use (#1649). A route whose
|
|
122
|
+
* success body is not JSON, such as an encrypted or a query-string one, still
|
|
123
|
+
* documents its exceptions as JSON, so where the success body has another
|
|
124
|
+
* type, the exceptions keep their status and description without a body.
|
|
125
|
+
*/
|
|
126
|
+
const swaggerV2Produces = (
|
|
127
|
+
output: Record<string, OpenApi.IOperation.IResponse>,
|
|
128
|
+
success: string,
|
|
129
|
+
): void => {
|
|
130
|
+
const kept: string | undefined = mediaOf(output[success]!);
|
|
131
|
+
if (kept === undefined) return;
|
|
132
|
+
for (const [status, response] of Object.entries(output))
|
|
133
|
+
if (status !== success && mediaOf(response) !== kept)
|
|
134
|
+
delete response.content;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/** The media types of a response body, or `undefined` without one. */
|
|
138
|
+
const mediaOf = (
|
|
139
|
+
response: OpenApi.IOperation.IResponse,
|
|
140
|
+
): string | undefined =>
|
|
141
|
+
response.content === undefined
|
|
142
|
+
? undefined
|
|
143
|
+
: Object.keys(response.content).sort().join(" ");
|
|
100
144
|
}
|
|
101
145
|
|
|
102
146
|
/**
|
|
@@ -126,7 +170,7 @@ const composeContent = (props: {
|
|
|
126
170
|
contentType: string | null | undefined;
|
|
127
171
|
schema: OpenApi.IJsonSchema | undefined;
|
|
128
172
|
example: any;
|
|
129
|
-
examples: Record<string,
|
|
173
|
+
examples: Record<string, OpenApi.IExample> | undefined;
|
|
130
174
|
}): OpenApi.IOperation.IContent | undefined => {
|
|
131
175
|
if (!props.contentType) return undefined;
|
|
132
176
|
const described: boolean =
|