@sap/cds-dk 8.2.3 → 8.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +66 -11
- package/bin/build.js +1 -1
- package/bin/cds.js +17 -4
- package/bin/compile/index.js +12 -3
- package/bin/deploy/index.js +22 -3
- package/bin/env.js +3 -2
- package/bin/import.js +24 -14
- package/bin/init.js +8 -4
- package/bin/repl.js +21 -8
- package/bin/util/fuzzySearch.js +8 -5
- package/lib/build/plugins/helm/files/subcharts/content-deployment/Chart.yaml +1 -1
- package/lib/build/plugins/helm/files/subcharts/content-deployment/values.schema.json +1 -1
- package/lib/build/plugins/helm/files/subcharts/content-deployment/values.yaml +1 -1
- package/lib/build/plugins/helm/files/subcharts/web-application/Chart.yaml +1 -1
- package/lib/build/plugins/helm/files/subcharts/web-application/templates/virtual-service.yaml +23 -12
- package/lib/build/plugins/helm/files/subcharts/web-application/values.schema.json +1 -1
- package/lib/build/plugins/helm/files/subcharts/web-application/values.yaml +2 -2
- package/lib/build/util.js +32 -20
- package/lib/compile/to-mermaid.js +87 -0
- package/lib/deploy/to-hana/hana.js +2 -21
- package/lib/env/schemas/cds-rc.js +174 -2
- package/lib/import/asyncapi/parserContext.js +4 -30
- package/lib/import/common.js +2 -2
- package/lib/import/importapi.js +3 -3
- package/lib/import/odata/v2parser.js +3 -2
- package/lib/import/openapi/importOpenAPI.js +622 -537
- package/lib/import/openapi/utilities.js +7 -2
- package/lib/init/constants.js +2 -1
- package/lib/init/index.js +4 -0
- package/lib/init/merge.js +2 -2
- package/lib/init/mvn.js +2 -2
- package/lib/init/projectReader.js +2 -1
- package/lib/init/registries/mta.js +8 -1
- package/lib/init/registries/xs-app.js +10 -0
- package/lib/init/template/approuter/files/package.json +1 -1
- package/lib/init/template/cf-manifest/files/manifest.yml.hbs +3 -1
- package/lib/init/template/cf-manifest/index.js +2 -0
- package/lib/init/template/cloud-logging/files/mta.yaml.hbs +13 -2
- package/lib/init/template/cloud-logging/files/{package.json → package.json.hbs} +5 -1
- package/lib/init/template/cloud-logging/index.js +4 -3
- package/lib/init/template/data/as-csv.js +6 -0
- package/lib/init/template/data/as-json.js +8 -1
- package/lib/init/template/data/index.js +1 -1
- package/lib/init/template/destination/files/mta.yaml.hbs +5 -5
- package/lib/init/template/dynatrace/files/mta.yaml.hbs +22 -6
- package/lib/init/template/dynatrace/files/package.json.hbs +5 -2
- package/lib/init/template/dynatrace/files/values.yaml +15 -0
- package/lib/init/template/dynatrace/index.js +10 -5
- package/lib/init/template/enterprise-messaging-shared/index.js +1 -1
- package/lib/init/template/file-based-messaging/index.js +1 -1
- package/lib/init/template/hana/files/package.json.hbs +2 -0
- package/lib/init/template/hana/index.js +1 -1
- package/lib/init/template/handler/index.js +16 -1
- package/lib/init/template/helm/files/values.yaml.hbs +2 -2
- package/lib/init/template/helm/index.js +6 -1
- package/lib/init/template/html5-repo/files/app-package.json.hbs +3 -1
- package/lib/init/template/html5-repo/files/ui5-deploy.yaml.hbs +7 -0
- package/lib/init/template/html5-repo/index.js +3 -1
- package/lib/init/template/local-messaging/index.js +1 -1
- package/lib/init/template/mta/files/mta.yaml.hbs +1 -1
- package/lib/init/template/mta/index.js +2 -0
- package/lib/init/template/nodejs/files/.vscode/extensions.json +0 -5
- package/lib/init/template/nodejs/files/.vscode/settings.json +1 -1
- package/lib/init/template/nodejs/files/.vscode/tasks.json +0 -2
- package/lib/init/template/nodejs/files/package.json.hbs +1 -1
- package/lib/init/template/postgres/index.js +1 -1
- package/lib/init/template/redis-messaging/index.js +1 -1
- package/lib/init/template/side-by-side-extensibility/files/mta.yaml.hbs +40 -0
- package/lib/init/template/side-by-side-extensibility/files/package.json.hbs +17 -0
- package/lib/init/template/side-by-side-extensibility/index.js +34 -0
- package/lib/init/template/typer/index.js +2 -1
- package/lib/init/template/typescript/files/package.json +2 -2
- package/lib/init/template/workzone-standard/files/manifest.json.hbs +15 -1
- package/lib/init/template/workzone-standard/files/mta.yaml.hbs +11 -2
- package/lib/init/template/workzone-standard/files/xs-app.json.hbs +18 -0
- package/lib/init/template/workzone-standard/index.js +25 -7
- package/lib/init/template/xsuaa/index.js +1 -1
- package/lib/mtx/auth_manager.js +7 -5
- package/lib/mtx/util/errors.js +4 -4
- package/lib/util/fs.js +1 -1
- package/lib/util/jsonc.js +1 -1
- package/lib/util/jsonc.pegjs +6 -2
- package/npm-shrinkwrap.json +78 -149
- package/package.json +1 -1
|
@@ -1,618 +1,653 @@
|
|
|
1
1
|
const {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
cdsName,
|
|
3
|
+
nameFromPath,
|
|
4
|
+
pathAndMethod,
|
|
5
|
+
serviceName,
|
|
6
6
|
} = require("./utilities");
|
|
7
7
|
|
|
8
8
|
module.exports = { importOpenAPI };
|
|
9
9
|
|
|
10
10
|
// we are not interested (yet) in HEAD, OPTIONS, TRACE
|
|
11
11
|
const IS_METHOD = {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
delete: true,
|
|
13
|
+
get: true,
|
|
14
|
+
patch: true,
|
|
15
|
+
post: true,
|
|
16
|
+
put: true,
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
const STANDARD_HEADERS = [
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
"accept",
|
|
21
|
+
"accept-encoding",
|
|
22
|
+
"accept-language",
|
|
23
|
+
"authorization",
|
|
24
|
+
"content-type",
|
|
25
|
+
"if-match",
|
|
26
26
|
];
|
|
27
27
|
|
|
28
28
|
function importOpenAPI(input) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
29
|
+
const csn = {
|
|
30
|
+
definitions: {},
|
|
31
|
+
meta: { creator: "cds-import-openapi" },
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const context = {
|
|
35
|
+
serviceName: serviceName(input.info?.title || "TODO.service"),
|
|
36
|
+
oasVersion: input.openapi || input.swagger,
|
|
37
|
+
v3: !!input.openapi,
|
|
38
|
+
schemas: input.openapi ? input.components?.schemas : input.definitions,
|
|
39
|
+
anonymous: [],
|
|
40
|
+
JSON: false,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
//TODO: complain if neither swagger nor openapi3, i.e. context.oasVersion is undefined
|
|
44
|
+
const service = {
|
|
45
|
+
kind: "service",
|
|
46
|
+
"@Capabilities.BatchSupported": false,
|
|
47
|
+
"@Capabilities.KeyAsSegmentSupported": true,
|
|
48
|
+
};
|
|
49
|
+
if (input.info?.title) service["@Core.Description"] = input.info.title;
|
|
50
|
+
if (input.info?.version) service["@Core.SchemaVersion"] = input.info.version;
|
|
51
|
+
if (input.info?.description)
|
|
52
|
+
service["@Core.LongDescription"] = input.info.description;
|
|
53
|
+
csn.definitions[context.serviceName] = service;
|
|
54
|
+
|
|
55
|
+
operations(context, csn, input);
|
|
56
|
+
|
|
57
|
+
reuseTypes(context, csn);
|
|
58
|
+
|
|
59
|
+
anonymousTypes(context, csn);
|
|
60
|
+
|
|
61
|
+
return csn;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
function reuseTypes(context, csn) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
65
|
+
for (const [name, schema] of Object.entries(context.schemas || {})) {
|
|
66
|
+
const typeName = `${context.serviceName}_types.${cdsName(name)}`;
|
|
67
|
+
csn.definitions[typeName] = cdsType(context, schema, false, true);
|
|
68
|
+
csn.definitions[typeName].kind = "type";
|
|
69
|
+
delete csn.definitions[typeName].notNull;
|
|
70
|
+
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
function anonymousTypes(context, csn) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
74
|
+
for (const a of context.anonymous) {
|
|
75
|
+
csn.definitions[a.name] = a.type;
|
|
76
|
+
csn.definitions[a.name].kind = "type";
|
|
77
|
+
}
|
|
78
|
+
if (context.JSON) {
|
|
79
|
+
csn.definitions["common.JSON"] = { kind: "type", type: "cds.LargeString" };
|
|
80
|
+
}
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
function operations(context, csn, input) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (param.in === "body") {
|
|
122
|
-
bodyParam = param;
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
if (STANDARD_HEADERS.includes(param.name.toLowerCase())) continue;
|
|
126
|
-
|
|
127
|
-
const cdsParam = cdsType(
|
|
128
|
-
context,
|
|
129
|
-
context.v3 ? param?.schema : param,
|
|
130
|
-
false,
|
|
131
|
-
false,
|
|
132
|
-
true
|
|
133
|
-
);
|
|
134
|
-
if (param.description) cdsParam["@description"] = param.description;
|
|
135
|
-
cdsParam["@openapi.in"] = param.in;
|
|
136
|
-
|
|
137
|
-
// only add the annotation "@openapi.explode" for true scenario
|
|
138
|
-
if (param.explode || param.style === "form")
|
|
139
|
-
cdsParam["@openapi.explode"] = true;
|
|
140
|
-
if (!param.explode)
|
|
141
|
-
delete cdsParam["@openapi.explode"];
|
|
142
|
-
|
|
143
|
-
if ((param.in === "path" && param.style && param.style !== "simple") ||
|
|
144
|
-
(param.in === "query" && param.style && param.style !== "form")
|
|
145
|
-
)
|
|
146
|
-
cdsParam["@openapi.style"] = param.style;
|
|
147
|
-
if (param.in === "query" && param.collectionFormat === "ssv")
|
|
148
|
-
cdsParam["@openapi.style"] = "spaceDelimited";
|
|
149
|
-
if (param.in === "query" && param.collectionFormat === "pipes")
|
|
150
|
-
cdsParam["@openapi.style"] = "pipeDelimited";
|
|
151
|
-
|
|
152
|
-
if (param.in === "query" && param.allowReserved === true)
|
|
153
|
-
cdsParam["@openapi.allowReserved"] = true;
|
|
154
|
-
|
|
155
|
-
if (param.required && param.in !== "path")
|
|
156
|
-
cdsParam["@openapi.required"] = true;
|
|
157
|
-
|
|
158
|
-
if (!param.required) {
|
|
159
|
-
delete param.notNull;
|
|
160
|
-
if (param.default !== undefined)
|
|
161
|
-
cdsParam["default"] = { "val": param.default };
|
|
162
|
-
}
|
|
163
|
-
const name = cdsName(param.name);
|
|
164
|
-
if (name !== param.name) cdsParam["@openapi.name"] = param.name;
|
|
165
|
-
cdsOperation.params[name] = cdsParam;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const schema = context.v3
|
|
169
|
-
? requestBodySchema(context, operation.requestBody, input.components)
|
|
170
|
-
: v2Schema(context, bodyParam?.schema, operation.consumes);
|
|
171
|
-
if (schema) {
|
|
172
|
-
cdsOperation.params.body = cdsType(context, schema);
|
|
173
|
-
cdsOperation.params.body["@openapi.in"] = "body";
|
|
84
|
+
const reuseParameters = context.v3
|
|
85
|
+
? input.components?.parameters
|
|
86
|
+
: input.parameters;
|
|
87
|
+
const reuseResponses = context.v3
|
|
88
|
+
? input.components?.responses
|
|
89
|
+
: input.responses;
|
|
90
|
+
for (const [path, item] of Object.entries(input.paths || {})) {
|
|
91
|
+
const globalParameters = item.parameters || [];
|
|
92
|
+
for (const [method, operation] of Object.entries(item)) {
|
|
93
|
+
if (!IS_METHOD[method]) continue;
|
|
94
|
+
const cdsOperation = {
|
|
95
|
+
kind: method === "get" ? "function" : "action",
|
|
96
|
+
params: {},
|
|
97
|
+
};
|
|
98
|
+
if (operation.tags) cdsOperation["@Common.Label"] = operation.tags[0];
|
|
99
|
+
if (operation.summary)
|
|
100
|
+
cdsOperation["@Core.Description"] = operation.summary;
|
|
101
|
+
if (operation.description)
|
|
102
|
+
cdsOperation["@Core.LongDescription"] = operation.description;
|
|
103
|
+
if (cdsOperation.kind === "action" && method !== "post")
|
|
104
|
+
cdsOperation["@openapi.method"] = method.toUpperCase();
|
|
105
|
+
cdsOperation["@openapi.path"] = path;
|
|
106
|
+
|
|
107
|
+
let bodyParam;
|
|
108
|
+
|
|
109
|
+
for (let param of globalParameters.concat(operation.parameters || [])) {
|
|
110
|
+
if (param.$ref) {
|
|
111
|
+
// resolve reuse parameter
|
|
112
|
+
const expectedPrefix = context.v3
|
|
113
|
+
? "#/components/parameters/"
|
|
114
|
+
: "#/parameters/";
|
|
115
|
+
if (!param.$ref.startsWith(expectedPrefix)) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`TODO: unexpected reference ${param.$ref} in parameter for ${method} of ${path}`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
param = reuseParameters[param.$ref.substring(expectedPrefix.length)];
|
|
174
121
|
}
|
|
175
122
|
|
|
176
|
-
|
|
177
|
-
|
|
123
|
+
if (param.in === "body") {
|
|
124
|
+
bodyParam = param;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (STANDARD_HEADERS.includes(param.name.toLowerCase())) continue;
|
|
128
|
+
|
|
129
|
+
const cdsParam = cdsType(
|
|
130
|
+
context,
|
|
131
|
+
context.v3 ? param?.schema : param,
|
|
132
|
+
false,
|
|
133
|
+
false,
|
|
134
|
+
true,
|
|
135
|
+
);
|
|
136
|
+
if (param.description) cdsParam["@description"] = param.description;
|
|
137
|
+
cdsParam["@openapi.in"] = param.in;
|
|
138
|
+
|
|
139
|
+
// only add the annotation "@openapi.explode" for true scenario
|
|
140
|
+
if (param.explode || param.style === "form")
|
|
141
|
+
cdsParam["@openapi.explode"] = true;
|
|
142
|
+
if (!param.explode) delete cdsParam["@openapi.explode"];
|
|
178
143
|
|
|
179
|
-
|
|
180
|
-
|
|
144
|
+
if (
|
|
145
|
+
(param.in === "path" && param.style && param.style !== "simple") ||
|
|
146
|
+
(param.in === "query" && param.style && param.style !== "form")
|
|
147
|
+
)
|
|
148
|
+
cdsParam["@openapi.style"] = param.style;
|
|
149
|
+
if (param.in === "query" && param.collectionFormat === "ssv")
|
|
150
|
+
cdsParam["@openapi.style"] = "spaceDelimited";
|
|
151
|
+
if (param.in === "query" && param.collectionFormat === "pipes")
|
|
152
|
+
cdsParam["@openapi.style"] = "pipeDelimited";
|
|
153
|
+
|
|
154
|
+
if (param.in === "query" && param.allowReserved === true)
|
|
155
|
+
cdsParam["@openapi.allowReserved"] = true;
|
|
156
|
+
|
|
157
|
+
if (param.required && param.in !== "path")
|
|
158
|
+
cdsParam["@openapi.required"] = true;
|
|
159
|
+
|
|
160
|
+
if (!param.required) {
|
|
161
|
+
delete param.notNull;
|
|
162
|
+
if (param.default !== undefined)
|
|
163
|
+
cdsParam["default"] = { val: param.default };
|
|
164
|
+
}
|
|
165
|
+
const name = cdsName(param.name);
|
|
166
|
+
if (name !== param.name) cdsParam["@openapi.name"] = param.name;
|
|
167
|
+
cdsOperation.params[name] = cdsParam;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const schema = context.v3
|
|
171
|
+
? requestBodySchema(context, operation.requestBody, input.components)
|
|
172
|
+
: v2Schema(context, bodyParam?.schema, operation.consumes);
|
|
173
|
+
if (schema) {
|
|
174
|
+
cdsOperation.params.body = cdsType(context, schema);
|
|
175
|
+
cdsOperation.params.body["@openapi.in"] = "body";
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!operation.responses)
|
|
179
|
+
throw new Error(
|
|
180
|
+
`TODO: no responses for path ${path}, method ${method}`,
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const successCode = Object.keys(operation.responses || {}).find((r) =>
|
|
184
|
+
r.startsWith("2"),
|
|
185
|
+
);
|
|
186
|
+
if (successCode) {
|
|
187
|
+
let response = operation.responses[successCode];
|
|
188
|
+
if (response?.$ref) {
|
|
189
|
+
// resolve reuse response
|
|
190
|
+
const expectedPrefix = context.v3
|
|
191
|
+
? "#/components/responses/"
|
|
192
|
+
: "#/responses/";
|
|
193
|
+
if (!response.$ref.startsWith(expectedPrefix))
|
|
194
|
+
throw new Error(
|
|
195
|
+
`TODO: unexpected reference ${response.$ref} in ${successCode} response for method ${method} of path ${path}`,
|
|
181
196
|
);
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
// resolve reuse response
|
|
186
|
-
const expectedPrefix = context.v3
|
|
187
|
-
? "#/components/responses/"
|
|
188
|
-
: "#/responses/";
|
|
189
|
-
if (!response.$ref.startsWith(expectedPrefix))
|
|
190
|
-
throw new Error(`TODO: unexpected reference ${response.$ref} in ${successCode} response for method ${method} of path ${path}`);
|
|
191
|
-
else
|
|
192
|
-
response =
|
|
193
|
-
reuseResponses[response.$ref.substring(expectedPrefix.length)];
|
|
194
|
-
}
|
|
195
|
-
const responseSchema = context.v3
|
|
196
|
-
? v3Schema(context, response)
|
|
197
|
-
: v2Schema(context, response.schema, operation.produces);
|
|
198
|
-
if (responseSchema)
|
|
199
|
-
cdsOperation.returns = cdsType(context, responseSchema);
|
|
200
|
-
else if (method === "get")
|
|
201
|
-
cdsOperation.returns = { type: "cds.Boolean" };
|
|
202
|
-
} else if (method === "get")
|
|
203
|
-
cdsOperation.returns = { type: "cds.Boolean" };
|
|
204
|
-
|
|
205
|
-
const operationName = `${context.serviceName}.${nameFromPath(
|
|
206
|
-
path,
|
|
207
|
-
method
|
|
208
|
-
)}`;
|
|
209
|
-
|
|
210
|
-
if (csn.definitions[operationName]) {
|
|
211
|
-
const existing = pathAndMethod(csn.definitions[operationName]);
|
|
212
|
-
throw new Error(`Name collision: same name ${operationName} for method ${method} of path ${path} and method ${existing.method.toLowerCase()} of path ${existing.path}`);
|
|
213
|
-
}
|
|
214
|
-
csn.definitions[operationName] = cdsOperation;
|
|
197
|
+
else
|
|
198
|
+
response =
|
|
199
|
+
reuseResponses[response.$ref.substring(expectedPrefix.length)];
|
|
215
200
|
}
|
|
201
|
+
const responseSchema = context.v3
|
|
202
|
+
? v3Schema(context, response)
|
|
203
|
+
: v2Schema(context, response.schema, operation.produces);
|
|
204
|
+
if (responseSchema)
|
|
205
|
+
cdsOperation.returns = cdsType(context, responseSchema);
|
|
206
|
+
else if (method === "get")
|
|
207
|
+
cdsOperation.returns = { type: "cds.Boolean" };
|
|
208
|
+
} else if (method === "get")
|
|
209
|
+
cdsOperation.returns = { type: "cds.Boolean" };
|
|
210
|
+
|
|
211
|
+
const operationName = `${context.serviceName}.${nameFromPath(
|
|
212
|
+
path,
|
|
213
|
+
method,
|
|
214
|
+
)}`;
|
|
215
|
+
|
|
216
|
+
if (csn.definitions[operationName]) {
|
|
217
|
+
const existing = pathAndMethod(csn.definitions[operationName]);
|
|
218
|
+
throw new Error(
|
|
219
|
+
`Name collision: same name ${operationName} for method ${method} of path ${path} and method ${existing.method.toLowerCase()} of path ${existing.path
|
|
220
|
+
}`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
csn.definitions[operationName] = cdsOperation;
|
|
216
224
|
}
|
|
225
|
+
}
|
|
217
226
|
}
|
|
218
227
|
|
|
219
228
|
function requestBodySchema(context, requestBody, components) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
229
|
+
while (requestBody?.$ref) {
|
|
230
|
+
const expectedPrefix = "#/components/requestBodies/";
|
|
231
|
+
if (!requestBody.$ref.startsWith(expectedPrefix)) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
`Unexpected request body reference ${requestBody.$ref}`,
|
|
234
|
+
);
|
|
235
|
+
} else {
|
|
236
|
+
requestBody =
|
|
237
|
+
components?.requestBodies[
|
|
238
|
+
requestBody.$ref.substring(expectedPrefix.length)
|
|
239
|
+
];
|
|
232
240
|
}
|
|
241
|
+
}
|
|
233
242
|
|
|
234
|
-
|
|
243
|
+
return v3Schema(context, requestBody);
|
|
235
244
|
}
|
|
236
245
|
|
|
237
246
|
function v3Schema(context, body) {
|
|
238
|
-
|
|
247
|
+
if (!body?.content) return undefined;
|
|
239
248
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
249
|
+
const contentTypes = Object.keys(body.content);
|
|
250
|
+
if (contentTypes.includes("application/json"))
|
|
251
|
+
return body.content["application/json"].schema;
|
|
243
252
|
|
|
244
|
-
|
|
253
|
+
if (contentTypes.length === 0) return undefined;
|
|
245
254
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
255
|
+
// if (contentTypes.length > 1)
|
|
256
|
+
// context.messages.push({
|
|
257
|
+
// message: `Multiple requestBody content-types not including application/json`,
|
|
258
|
+
// input: contentTypes,
|
|
259
|
+
// });
|
|
251
260
|
|
|
252
|
-
|
|
261
|
+
const contentType = contentTypes[0];
|
|
253
262
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
263
|
+
return {
|
|
264
|
+
$contentType: contentType,
|
|
265
|
+
...body.content[contentType]?.schema,
|
|
266
|
+
};
|
|
258
267
|
}
|
|
259
268
|
|
|
260
269
|
function v2Schema(context, schema, contentTypes) {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
270
|
+
if (
|
|
271
|
+
!schema ||
|
|
272
|
+
!contentTypes ||
|
|
273
|
+
contentTypes.includes("application/json") ||
|
|
274
|
+
contentTypes.includes("application/json;charset=utf-8") ||
|
|
275
|
+
contentTypes.includes("application/json;charset=UTF-8")
|
|
276
|
+
)
|
|
277
|
+
return schema;
|
|
278
|
+
|
|
279
|
+
// take the first content type if there are multiple
|
|
280
|
+
const contentType = contentTypes[0];
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
$contentType: contentType,
|
|
284
|
+
...schema,
|
|
285
|
+
};
|
|
277
286
|
}
|
|
278
287
|
|
|
279
288
|
function cdsType(
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
289
|
+
context,
|
|
290
|
+
schema,
|
|
291
|
+
arrayItem = false,
|
|
292
|
+
namedType = false,
|
|
293
|
+
forParameter = false,
|
|
285
294
|
) {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
295
|
+
let type = {};
|
|
296
|
+
let hasIncludes = false;
|
|
297
|
+
|
|
298
|
+
if (!arrayItem) if (schema.title) type["@title"] = schema.title;
|
|
299
|
+
if (schema.description) type["@description"] = schema.description;
|
|
300
|
+
if (schema.$contentType && schema.$contentType !== "*/*")
|
|
301
|
+
type["@openapi.contentType"] = schema.$contentType;
|
|
302
|
+
|
|
303
|
+
if (schema.$ref) {
|
|
304
|
+
const refType = referencedType(context, schema.$ref);
|
|
305
|
+
let nRefType = refType;
|
|
306
|
+
if (refType.schema?.$ref)
|
|
307
|
+
nRefType = indirectlyReferencedType(context, refType.schema);
|
|
308
|
+
if (namedType && normalizeSchemaType(nRefType.schema) === "object") {
|
|
309
|
+
type.kind = "type";
|
|
310
|
+
type.includes = [refType.name];
|
|
311
|
+
type.elements = {};
|
|
312
|
+
} else {
|
|
313
|
+
type.type = refType.name;
|
|
314
|
+
if (schema.maxLength) type.length = schema.maxLength;
|
|
315
|
+
}
|
|
316
|
+
return type;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
let schemaType = normalizeSchemaType(schema);
|
|
320
|
+
|
|
321
|
+
switch (schemaType) {
|
|
322
|
+
case "array":
|
|
323
|
+
//TODO: complain if "xml"
|
|
324
|
+
if (schema.items) {
|
|
325
|
+
const itemsType = cdsType(
|
|
326
|
+
context,
|
|
327
|
+
schema.items,
|
|
328
|
+
true,
|
|
329
|
+
false,
|
|
330
|
+
forParameter,
|
|
331
|
+
);
|
|
332
|
+
const annotations = Object.keys(itemsType).filter((k) =>
|
|
333
|
+
k.startsWith("@"),
|
|
334
|
+
);
|
|
335
|
+
// make anonymous type for item if
|
|
336
|
+
// - item has annotation
|
|
337
|
+
// - item has default
|
|
338
|
+
// - item is itself an array
|
|
339
|
+
if (annotations.length > 0 || itemsType.default || itemsType.items) {
|
|
340
|
+
type.items = anonymousType(context, itemsType);
|
|
304
341
|
} else {
|
|
305
|
-
|
|
306
|
-
if (schema.maxLength) type.length = schema.maxLength;
|
|
342
|
+
type.items = itemsType;
|
|
307
343
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
break;
|
|
393
|
-
|
|
394
|
-
case "string":
|
|
395
|
-
switch (schema.format) {
|
|
396
|
-
case "binary":
|
|
397
|
-
type.type = "cds.LargeBinary";
|
|
398
|
-
if (schema.maxLength) type.length = schema.maxLength;
|
|
399
|
-
break;
|
|
400
|
-
case "date":
|
|
401
|
-
type.type = "cds.Date";
|
|
402
|
-
break;
|
|
403
|
-
case "date-time":
|
|
404
|
-
type.type = "cds.Timestamp";
|
|
405
|
-
break;
|
|
406
|
-
case "time":
|
|
407
|
-
type.type = "cds.Time";
|
|
408
|
-
break;
|
|
409
|
-
case "uuid":
|
|
410
|
-
type.type = "cds.UUID";
|
|
411
|
-
break;
|
|
412
|
-
default:
|
|
413
|
-
type.type = "cds.String";
|
|
414
|
-
if (schema.maxLength) type.length = schema.maxLength;
|
|
415
|
-
}
|
|
416
|
-
if (Array.isArray(schema.enum)) {
|
|
417
|
-
type["@assert.range"] = true;
|
|
418
|
-
type.enum = Object.fromEntries(
|
|
419
|
-
schema.enum.map((val) => {
|
|
420
|
-
const key = cdsName(val);
|
|
421
|
-
return key === val ? [val, {}] : [key, { val }];
|
|
422
|
-
})
|
|
423
|
-
);
|
|
424
|
-
}
|
|
425
|
-
addDefault(type, schema, forParameter);
|
|
426
|
-
addPrimitiveExample(type, schema);
|
|
427
|
-
break;
|
|
428
|
-
|
|
429
|
-
case undefined:
|
|
430
|
-
schemaType = bestMatchingType(context, schema);
|
|
431
|
-
if (
|
|
432
|
-
(!namedType || schemaType !== "object") && schema.allOf && schema.allOf.length === 1 &&
|
|
433
|
-
Object.keys(schema).filter(
|
|
434
|
-
(k) => !["description", "nullable"].includes(k) && !k.startsWith("x-")
|
|
435
|
-
).length === 1) {
|
|
436
|
-
const normalizedSchema = { ...schema.allOf[0] };
|
|
437
|
-
if (schema.description) normalizedSchema.description = schema.description;
|
|
438
|
-
type = cdsType(context, normalizedSchema, arrayItem, namedType, forParameter);
|
|
439
|
-
break;
|
|
440
|
-
}
|
|
441
|
-
if (schema.allOf && schema.allOf.length === 2 && schema.allOf[0].$ref && !schema.allOf[1].$ref && normalizeSchemaType(schema.allOf[1]) !== "object") {
|
|
442
|
-
const normalizedSchema = Object.assign(
|
|
443
|
-
{ ...schema.allOf[0] },
|
|
444
|
-
schema.allOf[1]
|
|
445
|
-
);
|
|
446
|
-
type = cdsType(
|
|
447
|
-
context,
|
|
448
|
-
normalizedSchema,
|
|
449
|
-
arrayItem,
|
|
450
|
-
namedType,
|
|
451
|
-
forParameter
|
|
452
|
-
);
|
|
453
|
-
break;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
if (schemaType) {
|
|
457
|
-
schema.type = schemaType;
|
|
458
|
-
type = cdsType(context, schema, arrayItem, namedType, forParameter);
|
|
459
|
-
break;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
// last resort
|
|
463
|
-
// eslint-disable-next-line no-fallthrough
|
|
344
|
+
} else {
|
|
345
|
+
type = someJSON(context, schema, arrayItem, type);
|
|
346
|
+
}
|
|
347
|
+
break;
|
|
348
|
+
|
|
349
|
+
case "boolean":
|
|
350
|
+
type.type = "cds.Boolean";
|
|
351
|
+
addDefault(type, schema, forParameter);
|
|
352
|
+
break;
|
|
353
|
+
|
|
354
|
+
case "file":
|
|
355
|
+
type.type = "cds.String";
|
|
356
|
+
break;
|
|
357
|
+
|
|
358
|
+
case "integer":
|
|
359
|
+
type.type = "cds.Integer";
|
|
360
|
+
if (schema.format === "int64") type.type = "cds.Integer64";
|
|
361
|
+
addDefault(type, schema, forParameter);
|
|
362
|
+
addPrimitiveExample(type, schema);
|
|
363
|
+
break;
|
|
364
|
+
|
|
365
|
+
case "number":
|
|
366
|
+
type.type = "cds.Decimal";
|
|
367
|
+
if (schema.format === "double" || schema.format === "float")
|
|
368
|
+
type.type = "cds.Double";
|
|
369
|
+
addDefault(type, schema, forParameter);
|
|
370
|
+
addPrimitiveExample(type, schema);
|
|
371
|
+
break;
|
|
372
|
+
|
|
373
|
+
case "object":
|
|
374
|
+
type.elements = {};
|
|
375
|
+
//TODO: discriminator(context, type, schema);
|
|
376
|
+
|
|
377
|
+
if (schema.anyOf) {
|
|
378
|
+
type["@openapi.anyOf"] = JSON.stringify(schema.anyOf);
|
|
379
|
+
type["@open"] = true;
|
|
380
|
+
}
|
|
381
|
+
if (schema.oneOf) {
|
|
382
|
+
type["@openapi.oneOf"] = JSON.stringify(schema.oneOf);
|
|
383
|
+
type["@open"] = true;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
for (const subSchema of schema.allOf || []) {
|
|
387
|
+
if (subSchema.$ref) {
|
|
388
|
+
hasIncludes = true;
|
|
389
|
+
const refType = referencedType(context, subSchema.$ref);
|
|
390
|
+
if (!type.includes) type.includes = [];
|
|
391
|
+
type.includes.push(refType.name);
|
|
392
|
+
} else if (subSchema.type === "object" || subSchema.properties) {
|
|
393
|
+
//TODO: what if subSchema has allOf/...? Better recurse here and "merge" the types?
|
|
394
|
+
structElements(context, type, subSchema);
|
|
395
|
+
} else {
|
|
396
|
+
// should not get here
|
|
397
|
+
throw new Error(`Error: object with non-object sub-schema`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
structElements(context, type, schema);
|
|
402
|
+
|
|
403
|
+
if (!namedType && hasIncludes) {
|
|
404
|
+
type = anonymousType(context, type);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
break;
|
|
408
|
+
|
|
409
|
+
case "string":
|
|
410
|
+
switch (schema.format) {
|
|
411
|
+
case "binary":
|
|
412
|
+
type.type = "cds.LargeBinary";
|
|
413
|
+
break;
|
|
414
|
+
case "date":
|
|
415
|
+
type.type = "cds.Date";
|
|
416
|
+
break;
|
|
417
|
+
case "date-time":
|
|
418
|
+
type.type = "cds.Timestamp";
|
|
419
|
+
break;
|
|
420
|
+
case "time":
|
|
421
|
+
type.type = "cds.Time";
|
|
422
|
+
break;
|
|
423
|
+
case "uuid":
|
|
424
|
+
type.type = "cds.UUID";
|
|
425
|
+
break;
|
|
464
426
|
default:
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
427
|
+
type.type = "cds.String";
|
|
428
|
+
if (schema.maxLength) type.length = schema.maxLength;
|
|
429
|
+
}
|
|
430
|
+
if (Array.isArray(schema.enum)) {
|
|
431
|
+
type["@assert.range"] = true;
|
|
432
|
+
type.enum = Object.fromEntries(
|
|
433
|
+
schema.enum
|
|
434
|
+
.filter((val) => val !== null)
|
|
435
|
+
.map((val) => {
|
|
436
|
+
const key = cdsName(val);
|
|
437
|
+
return key === val ? [val, {}] : [key, { val }];
|
|
438
|
+
}),
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
if (schema.pattern) type["@assert.format"] = schema.pattern;
|
|
442
|
+
addDefault(type, schema, forParameter);
|
|
443
|
+
addPrimitiveExample(type, schema);
|
|
444
|
+
break;
|
|
445
|
+
|
|
446
|
+
case undefined:
|
|
447
|
+
schemaType = bestMatchingType(context, schema);
|
|
448
|
+
if (
|
|
449
|
+
(!namedType || schemaType !== "object") &&
|
|
450
|
+
schema.allOf &&
|
|
451
|
+
schema.allOf.length === 1 &&
|
|
452
|
+
Object.keys(schema).filter(
|
|
453
|
+
(k) =>
|
|
454
|
+
!["description", "nullable"].includes(k) && !k.startsWith("x-"),
|
|
455
|
+
).length === 1
|
|
456
|
+
) {
|
|
457
|
+
const normalizedSchema = { ...schema.allOf[0] };
|
|
458
|
+
if (schema.description)
|
|
459
|
+
normalizedSchema.description = schema.description;
|
|
460
|
+
type = cdsType(
|
|
461
|
+
context,
|
|
462
|
+
normalizedSchema,
|
|
463
|
+
arrayItem,
|
|
464
|
+
namedType,
|
|
465
|
+
forParameter,
|
|
466
|
+
);
|
|
467
|
+
break;
|
|
468
|
+
}
|
|
469
|
+
if (
|
|
470
|
+
schema.allOf &&
|
|
471
|
+
schema.allOf.length === 2 &&
|
|
472
|
+
schema.allOf[0].$ref &&
|
|
473
|
+
!schema.allOf[1].$ref &&
|
|
474
|
+
normalizeSchemaType(schema.allOf[1]) !== "object"
|
|
475
|
+
) {
|
|
476
|
+
const normalizedSchema = Object.assign(
|
|
477
|
+
{ ...schema.allOf[0] },
|
|
478
|
+
schema.allOf[1],
|
|
479
|
+
);
|
|
480
|
+
type = cdsType(
|
|
481
|
+
context,
|
|
482
|
+
normalizedSchema,
|
|
483
|
+
arrayItem,
|
|
484
|
+
namedType,
|
|
485
|
+
forParameter,
|
|
486
|
+
);
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (schemaType) {
|
|
491
|
+
schema.type = schemaType;
|
|
492
|
+
type = cdsType(context, schema, arrayItem, namedType, forParameter);
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// last resort
|
|
497
|
+
// eslint-disable-next-line no-fallthrough
|
|
498
|
+
default:
|
|
499
|
+
type = someJSON(context, schema, arrayItem, type);
|
|
500
|
+
break;
|
|
501
|
+
}
|
|
502
|
+
return type;
|
|
469
503
|
}
|
|
470
504
|
|
|
471
505
|
function someJSON(context, schema, arrayItem, type) {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
return type;
|
|
506
|
+
context.JSON = true;
|
|
507
|
+
const jsonSchema = minimalSchema(schema);
|
|
508
|
+
if (arrayItem && jsonSchema !== "{}") {
|
|
509
|
+
type = anonymousType(context, {
|
|
510
|
+
type: "common.JSON",
|
|
511
|
+
"@openapi.schema": jsonSchema,
|
|
512
|
+
});
|
|
513
|
+
} else {
|
|
514
|
+
type.type = "common.JSON";
|
|
515
|
+
if (jsonSchema !== "{}") type["@openapi.schema"] = jsonSchema;
|
|
516
|
+
}
|
|
517
|
+
return type;
|
|
485
518
|
}
|
|
486
519
|
|
|
487
520
|
function addDefault(type, schema, forParameter) {
|
|
488
|
-
|
|
489
|
-
|
|
521
|
+
if (schema.default !== undefined) type.default = { val: schema.default };
|
|
522
|
+
if (forParameter) return;
|
|
490
523
|
}
|
|
491
524
|
|
|
492
525
|
function addPrimitiveExample(type, schema) {
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
526
|
+
const example = schema.examples?.[0] || schema.example;
|
|
527
|
+
if (!example || example.$ref) return;
|
|
528
|
+
type["@Core.Example.$Type"] = "Core.PrimitiveExampleValue";
|
|
529
|
+
type["@Core.Example.Value"] = example;
|
|
497
530
|
}
|
|
498
531
|
|
|
499
532
|
function minimalSchema(schema) {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
533
|
+
const s = { ...schema };
|
|
534
|
+
delete s.title;
|
|
535
|
+
delete s.description;
|
|
536
|
+
delete s.$contentType;
|
|
537
|
+
return JSON.stringify(s);
|
|
505
538
|
}
|
|
506
539
|
|
|
507
540
|
function anonymousType(context, type) {
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
541
|
+
//TODO: construct newName from path leading here instead of using a counter
|
|
542
|
+
// pro: makes imported models more stable
|
|
543
|
+
// con: may cause name clashes
|
|
544
|
+
const newName = `${context.serviceName}.anonymous.type${context.anonymous.length}`;
|
|
545
|
+
context.anonymous.push({ name: newName, type: type });
|
|
546
|
+
return { type: newName };
|
|
514
547
|
}
|
|
515
548
|
|
|
516
549
|
function bestMatchingType(context, schema) {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
550
|
+
if (schema.type) return schema.type;
|
|
551
|
+
let type;
|
|
552
|
+
if (schema.allOf || schema.anyOf || schema.oneOf) {
|
|
553
|
+
const xOf = (schema.allOf || []).concat(
|
|
554
|
+
schema.anyOf || [],
|
|
555
|
+
schema.oneOf || [],
|
|
556
|
+
);
|
|
557
|
+
for (const subSchema of xOf) {
|
|
558
|
+
// determine overall type of this construct
|
|
559
|
+
if (subSchema.$ref) {
|
|
560
|
+
const refType = indirectlyReferencedType(context, subSchema);
|
|
561
|
+
if (!refType.schema) return undefined;
|
|
562
|
+
|
|
563
|
+
if (
|
|
564
|
+
refType.schema.allOf ||
|
|
565
|
+
refType.schema.anyOf ||
|
|
566
|
+
refType.schema.oneOf
|
|
567
|
+
) {
|
|
568
|
+
const subType = bestMatchingType(context, refType.schema);
|
|
569
|
+
type = betterType(type, { type: subType });
|
|
570
|
+
} else type = betterType(type, refType.schema);
|
|
571
|
+
} else {
|
|
572
|
+
//TODO: nested xOf - not yet encountered
|
|
573
|
+
type = betterType(type, subSchema);
|
|
574
|
+
}
|
|
542
575
|
}
|
|
543
|
-
|
|
576
|
+
}
|
|
577
|
+
return type;
|
|
544
578
|
}
|
|
545
579
|
|
|
546
580
|
function indirectlyReferencedType(context, subSchema) {
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
581
|
+
let refType = { schema: subSchema };
|
|
582
|
+
let limit = 10;
|
|
583
|
+
while (refType.schema?.$ref && limit-- > 0)
|
|
584
|
+
refType = referencedType(context, refType.schema.$ref);
|
|
585
|
+
return refType;
|
|
552
586
|
}
|
|
553
587
|
|
|
554
588
|
function referencedType(context, ref) {
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
589
|
+
const expectedPrefix = context.v3
|
|
590
|
+
? "#/components/schemas/"
|
|
591
|
+
: "#/definitions/";
|
|
592
|
+
if (!ref.startsWith(expectedPrefix)) {
|
|
593
|
+
throw new Error(`TODO: unexpected reference ${ref} in schema.`);
|
|
594
|
+
}
|
|
595
|
+
const schemaName = decodeURIComponent(ref.substring(expectedPrefix.length));
|
|
596
|
+
const schema = context.schemas[schemaName];
|
|
597
|
+
|
|
598
|
+
return {
|
|
599
|
+
name: `${context.serviceName}_types.${cdsName(schemaName)}`,
|
|
600
|
+
schema,
|
|
601
|
+
};
|
|
567
602
|
}
|
|
568
603
|
|
|
569
604
|
function normalizeSchemaType(schema) {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
605
|
+
if (!schema) return undefined;
|
|
606
|
+
if (!schema.type && schema.items) return "array";
|
|
607
|
+
if (!schema.type && schema.maxLength) return "string";
|
|
608
|
+
if (!schema.type && schema.pattern) return "string";
|
|
609
|
+
|
|
610
|
+
if (!schema.type && schema.enum?.every((v) => typeof v === "string"))
|
|
611
|
+
return "string";
|
|
612
|
+
|
|
613
|
+
if (
|
|
614
|
+
!schema.type &&
|
|
615
|
+
(schema.properties ||
|
|
616
|
+
schema.patternProperties ||
|
|
617
|
+
schema.additionalProperties ||
|
|
618
|
+
schema.discriminator)
|
|
619
|
+
)
|
|
620
|
+
return "object";
|
|
621
|
+
|
|
622
|
+
let schemaType = schema.type;
|
|
623
|
+
if (Array.isArray(schemaType)) {
|
|
624
|
+
schemaType = schemaType.sort();
|
|
625
|
+
if (schemaType.includes("null")) {
|
|
626
|
+
const index = schemaType.indexOf("null");
|
|
627
|
+
schemaType.splice(index, 1);
|
|
628
|
+
}
|
|
629
|
+
if (schemaType.length === 1) schemaType = schemaType[0];
|
|
578
630
|
if (
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
schema.additionalProperties ||
|
|
583
|
-
schema.discriminator)
|
|
631
|
+
schemaType.length === 2 &&
|
|
632
|
+
schemaType[0] === "integer" &&
|
|
633
|
+
schemaType[1] === "number"
|
|
584
634
|
)
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
if (Array.isArray(schemaType)) {
|
|
589
|
-
schemaType = schemaType.sort();
|
|
590
|
-
if (schemaType.includes("null")) {
|
|
591
|
-
const index = schemaType.indexOf("null");
|
|
592
|
-
schemaType.splice(index, 1);
|
|
593
|
-
}
|
|
594
|
-
if (schemaType.length === 1) schemaType = schemaType[0];
|
|
595
|
-
if (
|
|
596
|
-
schemaType.length === 2 &&
|
|
597
|
-
schemaType[0] === "integer" &&
|
|
598
|
-
schemaType[1] === "number"
|
|
599
|
-
)
|
|
600
|
-
return "number";
|
|
601
|
-
}
|
|
602
|
-
return schemaType;
|
|
635
|
+
return "number";
|
|
636
|
+
}
|
|
637
|
+
return schemaType;
|
|
603
638
|
}
|
|
604
639
|
|
|
605
640
|
//TODO: better name :-)
|
|
606
641
|
function betterType(currentType, schema) {
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
642
|
+
const schemaType = normalizeSchemaType(schema);
|
|
643
|
+
if (currentType === undefined) return schemaType;
|
|
644
|
+
if (currentType === schemaType) return currentType;
|
|
645
|
+
if (currentType === "string" && ["integer", "number"].includes(schemaType))
|
|
646
|
+
return "string";
|
|
647
|
+
if (["integer", "number"].includes(currentType) && schemaType === "string")
|
|
648
|
+
return "string";
|
|
649
|
+
//TODO: more cases here?
|
|
650
|
+
return null;
|
|
616
651
|
}
|
|
617
652
|
|
|
618
653
|
// function discriminator(context, type, schema) {
|
|
@@ -633,11 +668,61 @@ function betterType(currentType, schema) {
|
|
|
633
668
|
// }
|
|
634
669
|
|
|
635
670
|
function structElements(context, type, schema) {
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
671
|
+
checkCircularReference(schema);
|
|
672
|
+
const schemaName = Object.keys(context.schemas || {}).find(
|
|
673
|
+
(key) => context.schemas[key] === schema,
|
|
674
|
+
);
|
|
675
|
+
const refPath = `#/components/schemas/${schemaName}`;
|
|
676
|
+
//TODO: interpret "required"
|
|
677
|
+
for (const [prop, propSchema] of Object.entries(schema.properties || {})) {
|
|
678
|
+
const name = cdsName(prop);
|
|
679
|
+
//to detect recursive data types
|
|
680
|
+
if (propSchema?.$ref === refPath || propSchema?.items?.$ref === refPath) {
|
|
681
|
+
console.warn(
|
|
682
|
+
`Recursive data type detected: ${schemaName} for the property ${name}`,
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
//TODO: check if property "name" already exists and has identical cdsType, warn otherwise
|
|
686
|
+
type.elements[name] = cdsType(context, propSchema);
|
|
687
|
+
if (name !== prop) type.elements[name]["@openapi.name"] = prop;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// function to check circular references and throw error if found
|
|
692
|
+
function checkCircularReference(schema, visited = new Set()) {
|
|
693
|
+
if (schema.$ref) {
|
|
694
|
+
const ref = schema.$ref;
|
|
695
|
+
if (visited.has(ref)) {
|
|
696
|
+
console.warn(`Circular reference detected: ${ref}`);
|
|
642
697
|
}
|
|
643
|
-
|
|
698
|
+
visited.add(ref);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (schema.allOf) {
|
|
702
|
+
schema.allOf.forEach((subSchema) =>
|
|
703
|
+
checkCircularReference(subSchema, visited),
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if (schema.anyOf) {
|
|
708
|
+
schema.anyOf.forEach((subSchema) =>
|
|
709
|
+
checkCircularReference(subSchema, visited),
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
if (schema.oneOf) {
|
|
714
|
+
schema.oneOf.forEach((subSchema) =>
|
|
715
|
+
checkCircularReference(subSchema, visited),
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
if (schema.properties) {
|
|
720
|
+
Object.values(schema.properties).forEach((subSchema) =>
|
|
721
|
+
checkCircularReference(subSchema, visited),
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (schema.items) {
|
|
726
|
+
checkCircularReference(schema.items, visited);
|
|
727
|
+
}
|
|
728
|
+
}
|