@twin.org/ts-to-openapi 0.0.2-next.8 → 0.0.3-next.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/bin/index.js +1 -1
- package/dist/es/cli.js +37 -0
- package/dist/es/cli.js.map +1 -0
- package/dist/es/commands/httpStatusCodeMap.js +115 -0
- package/dist/es/commands/httpStatusCodeMap.js.map +1 -0
- package/dist/{esm/index.mjs → es/commands/tsToOpenApi.js} +27 -168
- package/dist/es/commands/tsToOpenApi.js.map +1 -0
- package/dist/es/index.js +7 -0
- package/dist/es/index.js.map +1 -0
- package/dist/es/models/IInputPath.js +2 -0
- package/dist/es/models/IInputPath.js.map +1 -0
- package/dist/es/models/IInputResult.js +2 -0
- package/dist/es/models/IInputResult.js.map +1 -0
- package/dist/es/models/IRestRoute.js +2 -0
- package/dist/es/models/IRestRoute.js.map +1 -0
- package/dist/es/models/IRestRouteEntryPoints.js +2 -0
- package/dist/es/models/IRestRouteEntryPoints.js.map +1 -0
- package/dist/es/models/ITag.js +4 -0
- package/dist/es/models/ITag.js.map +1 -0
- package/dist/es/models/ITsToOpenApiConfig.js +2 -0
- package/dist/es/models/ITsToOpenApiConfig.js.map +1 -0
- package/dist/es/models/ITsToOpenApiConfigEntryPoint.js +4 -0
- package/dist/es/models/ITsToOpenApiConfigEntryPoint.js.map +1 -0
- package/dist/locales/en.json +46 -73
- package/dist/types/commands/tsToOpenApi.d.ts +1 -1
- package/dist/types/index.d.ts +4 -4
- package/dist/types/models/IInputResult.d.ts +11 -3
- package/dist/types/models/IRestRoute.d.ts +98 -0
- package/dist/types/models/IRestRouteEntryPoints.d.ts +23 -0
- package/dist/types/models/ITag.d.ts +13 -0
- package/dist/types/models/ITsToOpenApiConfig.d.ts +1 -1
- package/docs/changelog.md +64 -0
- package/docs/examples.md +1 -1
- package/locales/.validate-ignore +1 -0
- package/package.json +20 -11
- package/dist/cjs/index.cjs +0 -1083
package/dist/cjs/index.cjs
DELETED
|
@@ -1,1083 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
var path = require('node:path');
|
|
4
|
-
var node_url = require('node:url');
|
|
5
|
-
var cliCore = require('@twin.org/cli-core');
|
|
6
|
-
var promises = require('node:fs/promises');
|
|
7
|
-
var core = require('@twin.org/core');
|
|
8
|
-
var toolsCore = require('@twin.org/tools-core');
|
|
9
|
-
var web = require('@twin.org/web');
|
|
10
|
-
var tsJsonSchemaGenerator = require('ts-json-schema-generator');
|
|
11
|
-
|
|
12
|
-
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
13
|
-
const HTTP_STATUS_CODE_MAP = {
|
|
14
|
-
ok: {
|
|
15
|
-
code: web.HttpStatusCode.ok,
|
|
16
|
-
responseType: "IOkResponse"
|
|
17
|
-
},
|
|
18
|
-
created: {
|
|
19
|
-
code: web.HttpStatusCode.created,
|
|
20
|
-
responseType: "ICreatedResponse"
|
|
21
|
-
},
|
|
22
|
-
accepted: {
|
|
23
|
-
code: web.HttpStatusCode.accepted,
|
|
24
|
-
responseType: "IAcceptedResponse"
|
|
25
|
-
},
|
|
26
|
-
noContent: {
|
|
27
|
-
code: web.HttpStatusCode.noContent,
|
|
28
|
-
responseType: "INoContentResponse"
|
|
29
|
-
},
|
|
30
|
-
badRequest: {
|
|
31
|
-
code: web.HttpStatusCode.badRequest,
|
|
32
|
-
responseType: "IBadRequestResponse",
|
|
33
|
-
example: {
|
|
34
|
-
name: "GeneralError",
|
|
35
|
-
message: "component.error",
|
|
36
|
-
properties: {
|
|
37
|
-
foo: "bar"
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
},
|
|
41
|
-
unauthorized: {
|
|
42
|
-
code: web.HttpStatusCode.unauthorized,
|
|
43
|
-
responseType: "IUnauthorizedResponse",
|
|
44
|
-
example: {
|
|
45
|
-
name: "UnauthorizedError",
|
|
46
|
-
message: "component.error"
|
|
47
|
-
}
|
|
48
|
-
},
|
|
49
|
-
forbidden: {
|
|
50
|
-
code: web.HttpStatusCode.forbidden,
|
|
51
|
-
responseType: "IForbiddenResponse",
|
|
52
|
-
example: {
|
|
53
|
-
name: "NotImplementedError",
|
|
54
|
-
message: "component.error",
|
|
55
|
-
properties: {
|
|
56
|
-
method: "aMethod"
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
},
|
|
60
|
-
notFound: {
|
|
61
|
-
code: web.HttpStatusCode.notFound,
|
|
62
|
-
responseType: "INotFoundResponse",
|
|
63
|
-
example: {
|
|
64
|
-
name: "NotFoundError",
|
|
65
|
-
message: "component.error",
|
|
66
|
-
properties: {
|
|
67
|
-
notFoundId: "1"
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
},
|
|
71
|
-
conflict: {
|
|
72
|
-
code: web.HttpStatusCode.conflict,
|
|
73
|
-
responseType: "IConflictResponse",
|
|
74
|
-
example: {
|
|
75
|
-
name: "ConflictError",
|
|
76
|
-
message: "component.error",
|
|
77
|
-
properties: {
|
|
78
|
-
conflicts: ["1"]
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
},
|
|
82
|
-
internalServerError: {
|
|
83
|
-
code: web.HttpStatusCode.internalServerError,
|
|
84
|
-
responseType: "IInternalServerErrorResponse",
|
|
85
|
-
example: {
|
|
86
|
-
name: "InternalServerError",
|
|
87
|
-
message: "component.error"
|
|
88
|
-
}
|
|
89
|
-
},
|
|
90
|
-
unprocessableEntity: {
|
|
91
|
-
code: web.HttpStatusCode.unprocessableEntity,
|
|
92
|
-
responseType: "IUnprocessableEntityResponse",
|
|
93
|
-
example: {
|
|
94
|
-
name: "UnprocessableError",
|
|
95
|
-
message: "component.error"
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
/**
|
|
100
|
-
* Get the HTTP status code from the error code type.
|
|
101
|
-
* @param errorCodeType The error code type.
|
|
102
|
-
* @returns The HTTP status code.
|
|
103
|
-
*/
|
|
104
|
-
function getHttpStatusCodeFromType(errorCodeType) {
|
|
105
|
-
for (const httpStatusCodeType of Object.values(HTTP_STATUS_CODE_MAP)) {
|
|
106
|
-
if (httpStatusCodeType.responseType === errorCodeType) {
|
|
107
|
-
return httpStatusCodeType.code;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
return web.HttpStatusCode.ok;
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* Get the HTTP example from the error code type.
|
|
114
|
-
* @param errorCodeType The error code type.
|
|
115
|
-
* @returns The example.
|
|
116
|
-
*/
|
|
117
|
-
function getHttpExampleFromType(errorCodeType) {
|
|
118
|
-
for (const httpStatusCodeType of Object.values(HTTP_STATUS_CODE_MAP)) {
|
|
119
|
-
if (httpStatusCodeType.responseType === errorCodeType) {
|
|
120
|
-
return httpStatusCodeType.example;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Copyright 2024 IOTA Stiftung.
|
|
126
|
-
// SPDX-License-Identifier: Apache-2.0.
|
|
127
|
-
/**
|
|
128
|
-
* Build the root command to be consumed by the CLI.
|
|
129
|
-
* @param program The command to build on.
|
|
130
|
-
*/
|
|
131
|
-
function buildCommandTsToOpenApi(program) {
|
|
132
|
-
program
|
|
133
|
-
.argument(core.I18n.formatMessage("commands.ts-to-openapi.options.config.param"), core.I18n.formatMessage("commands.ts-to-openapi.options.config.description"))
|
|
134
|
-
.argument(core.I18n.formatMessage("commands.ts-to-openapi.options.output-file.param"), core.I18n.formatMessage("commands.ts-to-openapi.options.output-file.description"))
|
|
135
|
-
.action(async (config, outputFile, opts) => {
|
|
136
|
-
await actionCommandTsToOpenApi(config, outputFile);
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* Action the root command.
|
|
141
|
-
* @param configFile The optional configuration file.
|
|
142
|
-
* @param outputFile The output file for the generation OpenApi spec.
|
|
143
|
-
* @param opts The options for the command.
|
|
144
|
-
*/
|
|
145
|
-
async function actionCommandTsToOpenApi(configFile, outputFile, opts) {
|
|
146
|
-
let outputWorkingDir;
|
|
147
|
-
try {
|
|
148
|
-
let config;
|
|
149
|
-
const fullConfigFile = path.resolve(configFile);
|
|
150
|
-
const fullOutputFile = path.resolve(outputFile);
|
|
151
|
-
outputWorkingDir = path.join(path.dirname(fullOutputFile), "working");
|
|
152
|
-
cliCore.CLIDisplay.value(core.I18n.formatMessage("commands.ts-to-openapi.labels.configJson"), fullConfigFile);
|
|
153
|
-
cliCore.CLIDisplay.value(core.I18n.formatMessage("commands.ts-to-openapi.labels.outputFile"), fullOutputFile);
|
|
154
|
-
cliCore.CLIDisplay.value(core.I18n.formatMessage("commands.ts-to-openapi.labels.outputWorkingDir"), outputWorkingDir);
|
|
155
|
-
cliCore.CLIDisplay.break();
|
|
156
|
-
try {
|
|
157
|
-
cliCore.CLIDisplay.task(core.I18n.formatMessage("commands.ts-to-openapi.progress.loadingConfigJson"));
|
|
158
|
-
cliCore.CLIDisplay.break();
|
|
159
|
-
config = await cliCore.CLIUtils.readJsonFile(fullConfigFile);
|
|
160
|
-
}
|
|
161
|
-
catch (err) {
|
|
162
|
-
throw new core.GeneralError("commands", "commands.ts-to-openapi.configFailed", undefined, err);
|
|
163
|
-
}
|
|
164
|
-
if (core.Is.empty(config)) {
|
|
165
|
-
throw new core.GeneralError("commands", "commands.ts-to-openapi.configFailed");
|
|
166
|
-
}
|
|
167
|
-
cliCore.CLIDisplay.task(core.I18n.formatMessage("commands.ts-to-openapi.progress.creatingWorkingDir"));
|
|
168
|
-
await promises.mkdir(outputWorkingDir, { recursive: true });
|
|
169
|
-
cliCore.CLIDisplay.break();
|
|
170
|
-
await tsToOpenApi(config ?? {}, fullOutputFile, outputWorkingDir);
|
|
171
|
-
cliCore.CLIDisplay.break();
|
|
172
|
-
cliCore.CLIDisplay.done();
|
|
173
|
-
}
|
|
174
|
-
finally {
|
|
175
|
-
try {
|
|
176
|
-
if (outputWorkingDir) {
|
|
177
|
-
await promises.rm(outputWorkingDir, { recursive: true });
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
catch { }
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
/**
|
|
184
|
-
* Convert the TypeScript definitions to OpenAPI spec.
|
|
185
|
-
* @param config The configuration for the app.
|
|
186
|
-
* @param outputFile The location of the file to output the OpenAPI spec.
|
|
187
|
-
* @param workingDirectory The folder the app was run from.
|
|
188
|
-
*/
|
|
189
|
-
async function tsToOpenApi(config, outputFile, workingDirectory) {
|
|
190
|
-
await promises.writeFile(path.join(workingDirectory, "package.json"), JSON.stringify({
|
|
191
|
-
version: "1.0.0",
|
|
192
|
-
name: "ts-to-openapi-working",
|
|
193
|
-
dependencies: {}
|
|
194
|
-
}, undefined, "\t"));
|
|
195
|
-
await promises.writeFile(path.join(workingDirectory, "tsconfig.json"), JSON.stringify({
|
|
196
|
-
compilerOptions: {}
|
|
197
|
-
}, undefined, "\t"));
|
|
198
|
-
const openApi = {
|
|
199
|
-
openapi: toolsCore.OpenApiHelper.API_VERSION,
|
|
200
|
-
info: {
|
|
201
|
-
title: config.title,
|
|
202
|
-
description: config.description,
|
|
203
|
-
version: config.version,
|
|
204
|
-
license: {
|
|
205
|
-
name: config.licenseName,
|
|
206
|
-
url: config.licenseUrl
|
|
207
|
-
}
|
|
208
|
-
},
|
|
209
|
-
servers: core.Is.arrayValue(config.servers) ? config.servers.map(s => ({ url: s })) : undefined,
|
|
210
|
-
tags: [],
|
|
211
|
-
paths: {}
|
|
212
|
-
};
|
|
213
|
-
cliCore.CLIDisplay.task(core.I18n.formatMessage("commands.ts-to-openapi.progress.creatingSecuritySchemas"));
|
|
214
|
-
cliCore.CLIDisplay.break();
|
|
215
|
-
const authSecurity = [];
|
|
216
|
-
const securitySchemes = {};
|
|
217
|
-
buildSecurity(config, securitySchemes, authSecurity);
|
|
218
|
-
const types = Object.values(HTTP_STATUS_CODE_MAP).map(h => h.responseType);
|
|
219
|
-
const inputResults = [];
|
|
220
|
-
const typeRoots = [];
|
|
221
|
-
const restRoutesAndTags = await loadPackages(config, workingDirectory, typeRoots);
|
|
222
|
-
for (const restRouteAndTag of restRoutesAndTags) {
|
|
223
|
-
const paths = await processPackageRestDetails(restRouteAndTag.restRoutes);
|
|
224
|
-
inputResults.push({
|
|
225
|
-
paths,
|
|
226
|
-
tags: restRouteAndTag.tags
|
|
227
|
-
});
|
|
228
|
-
for (const inputPath of paths) {
|
|
229
|
-
if (inputPath.requestType && !types.includes(inputPath.requestType)) {
|
|
230
|
-
types.push(inputPath.requestType);
|
|
231
|
-
}
|
|
232
|
-
if (inputPath.responseType) {
|
|
233
|
-
const rt = inputPath.responseType.map(i => i.type);
|
|
234
|
-
for (const r of rt) {
|
|
235
|
-
if (r && inputPath.responseType && !types.includes(r)) {
|
|
236
|
-
types.push(r);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
cliCore.CLIDisplay.task(core.I18n.formatMessage("commands.ts-to-openapi.progress.generatingSchemas"));
|
|
243
|
-
const autoExpandTypes = config.autoExpandTypes ?? [];
|
|
244
|
-
const defaultExpandTypes = ["/ObjectOrArray<.*>/"];
|
|
245
|
-
for (const defaultType of defaultExpandTypes) {
|
|
246
|
-
if (!autoExpandTypes.includes(defaultType)) {
|
|
247
|
-
autoExpandTypes.push(defaultType);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
const schemas = await generateSchemas(typeRoots, types, autoExpandTypes, workingDirectory);
|
|
251
|
-
for (const type in schemas) {
|
|
252
|
-
if (core.Is.object(config.overrides?.[type])) {
|
|
253
|
-
cliCore.CLIDisplay.task(core.I18n.formatMessage("commands.ts-to-openapi.progress.overridingSchema"));
|
|
254
|
-
schemas[type] = config.overrides?.[type];
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
const usedCommonResponseTypes = [];
|
|
258
|
-
for (let i = 0; i < inputResults.length; i++) {
|
|
259
|
-
const result = inputResults[i];
|
|
260
|
-
for (const tag of result.tags) {
|
|
261
|
-
const exists = openApi.tags?.find(t => t.name === tag.name);
|
|
262
|
-
if (!exists) {
|
|
263
|
-
openApi.tags?.push(tag);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
for (const inputPath of result.paths) {
|
|
267
|
-
const responses = [];
|
|
268
|
-
const responseTypes = inputPath.responseType;
|
|
269
|
-
const pathSpecificAuthSecurity = [];
|
|
270
|
-
if (authSecurity.length > 0 && !inputPath.skipAuth) {
|
|
271
|
-
pathSpecificAuthSecurity.push(...authSecurity);
|
|
272
|
-
}
|
|
273
|
-
if (pathSpecificAuthSecurity.length > 0) {
|
|
274
|
-
responseTypes.push({
|
|
275
|
-
statusCode: web.HttpStatusCode.unauthorized,
|
|
276
|
-
type: "IUnauthorizedResponse"
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
for (const responseType of responseTypes) {
|
|
280
|
-
if (schemas[responseType.type]) {
|
|
281
|
-
let headers;
|
|
282
|
-
let examples;
|
|
283
|
-
if (core.Is.arrayValue(responseType.examples)) {
|
|
284
|
-
for (const example of responseType.examples) {
|
|
285
|
-
if (core.Is.object(example.response)) {
|
|
286
|
-
if (core.Is.objectValue(example.response.headers)) {
|
|
287
|
-
headers ??= {};
|
|
288
|
-
const headersSchema = schemas[responseType.type].properties
|
|
289
|
-
?.headers;
|
|
290
|
-
for (const header in example.response.headers) {
|
|
291
|
-
const headerValue = example.response.headers[header];
|
|
292
|
-
const propertySchema = headersSchema.properties?.[header];
|
|
293
|
-
const schemaType = core.Is.object(propertySchema)
|
|
294
|
-
? propertySchema?.type
|
|
295
|
-
: undefined;
|
|
296
|
-
headers[header] = {
|
|
297
|
-
schema: {
|
|
298
|
-
type: core.Is.string(schemaType) ? schemaType : "string"
|
|
299
|
-
},
|
|
300
|
-
description: `e.g. ${core.Is.array(headerValue) ? headerValue.join(",") : headerValue}`
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
if (!core.Is.undefined(example.response.body)) {
|
|
305
|
-
examples ??= {};
|
|
306
|
-
examples[example.id] = {
|
|
307
|
-
summary: example.description,
|
|
308
|
-
value: example.response.body
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
else {
|
|
315
|
-
const statusExample = getHttpExampleFromType(responseType.type);
|
|
316
|
-
if (statusExample) {
|
|
317
|
-
examples = {};
|
|
318
|
-
examples.exampleResponse = {
|
|
319
|
-
value: statusExample
|
|
320
|
-
};
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
let mimeType;
|
|
324
|
-
let schemaType;
|
|
325
|
-
let schemaFormat;
|
|
326
|
-
let schemaRef;
|
|
327
|
-
let description = schemas[responseType.type]?.description;
|
|
328
|
-
if (core.Is.stringValue(responseType.mimeType)) {
|
|
329
|
-
mimeType = responseType.mimeType;
|
|
330
|
-
}
|
|
331
|
-
else {
|
|
332
|
-
const hasBody = core.Is.notEmpty(schemas[responseType.type]?.properties?.body);
|
|
333
|
-
if (hasBody) {
|
|
334
|
-
mimeType = web.MimeTypes.Json;
|
|
335
|
-
}
|
|
336
|
-
else {
|
|
337
|
-
mimeType = web.MimeTypes.PlainText;
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
// Perform some special handling for binary octet-streams to produce a nicer spec output
|
|
341
|
-
if (responseType.type === "Uint8Array") {
|
|
342
|
-
schemaType = "string";
|
|
343
|
-
schemaFormat = "binary";
|
|
344
|
-
schemaRef = undefined;
|
|
345
|
-
description = "Binary data";
|
|
346
|
-
if (core.Is.objectValue(examples)) {
|
|
347
|
-
const exampleKeys = Object.keys(examples);
|
|
348
|
-
const firstExample = examples[exampleKeys[0]];
|
|
349
|
-
description = firstExample.summary;
|
|
350
|
-
firstExample.summary = "Binary Data";
|
|
351
|
-
for (const exampleKey in examples) {
|
|
352
|
-
examples[exampleKey].value = "";
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
else {
|
|
357
|
-
schemaRef = `#/definitions/${responseType.type}`;
|
|
358
|
-
}
|
|
359
|
-
responses.push({
|
|
360
|
-
code: responseType.statusCode,
|
|
361
|
-
description,
|
|
362
|
-
content: responseType.type === "ICreatedResponse" ||
|
|
363
|
-
responseType.type === "INoContentResponse"
|
|
364
|
-
? undefined
|
|
365
|
-
: {
|
|
366
|
-
[mimeType]: {
|
|
367
|
-
schema: {
|
|
368
|
-
$ref: schemaRef,
|
|
369
|
-
type: schemaType,
|
|
370
|
-
format: schemaFormat
|
|
371
|
-
},
|
|
372
|
-
examples
|
|
373
|
-
}
|
|
374
|
-
},
|
|
375
|
-
headers
|
|
376
|
-
});
|
|
377
|
-
}
|
|
378
|
-
if (!usedCommonResponseTypes.includes(responseType.type)) {
|
|
379
|
-
usedCommonResponseTypes.push(responseType.type);
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
if (inputPath.responseCodes.length > 0) {
|
|
383
|
-
for (const responseCode of inputPath.responseCodes) {
|
|
384
|
-
const responseCodeDetails = HTTP_STATUS_CODE_MAP[responseCode];
|
|
385
|
-
// Only include the response code if it hasn't already been
|
|
386
|
-
// included with a specific response
|
|
387
|
-
if (!responseTypes.some(r => r.statusCode === responseCodeDetails.code)) {
|
|
388
|
-
let examples;
|
|
389
|
-
if (responseCodeDetails.example) {
|
|
390
|
-
examples = {
|
|
391
|
-
exampleResponse: {
|
|
392
|
-
value: responseCodeDetails.example
|
|
393
|
-
}
|
|
394
|
-
};
|
|
395
|
-
}
|
|
396
|
-
if (schemas[responseCodeDetails.responseType]) {
|
|
397
|
-
responses.push({
|
|
398
|
-
code: responseCodeDetails.code,
|
|
399
|
-
description: schemas[responseCodeDetails.responseType].description,
|
|
400
|
-
content: {
|
|
401
|
-
[web.MimeTypes.Json]: {
|
|
402
|
-
schema: {
|
|
403
|
-
$ref: `#/definitions/${responseCodeDetails.responseType}`
|
|
404
|
-
},
|
|
405
|
-
examples
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
});
|
|
409
|
-
}
|
|
410
|
-
if (!usedCommonResponseTypes.includes(responseCodeDetails.responseType)) {
|
|
411
|
-
usedCommonResponseTypes.push(responseCodeDetails.responseType);
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
const pathQueryHeaderParams = inputPath.pathParameters.map(p => ({
|
|
417
|
-
name: p,
|
|
418
|
-
description: "",
|
|
419
|
-
required: true,
|
|
420
|
-
schema: {
|
|
421
|
-
type: "string"
|
|
422
|
-
},
|
|
423
|
-
in: "path",
|
|
424
|
-
style: "simple"
|
|
425
|
-
}));
|
|
426
|
-
const requestExample = inputPath.requestExamples?.[0]
|
|
427
|
-
?.request;
|
|
428
|
-
if (core.Is.object(requestExample?.pathParams)) {
|
|
429
|
-
for (const pathOrQueryParam of pathQueryHeaderParams) {
|
|
430
|
-
if (requestExample.pathParams[pathOrQueryParam.name]) {
|
|
431
|
-
pathOrQueryParam.example = requestExample.pathParams[pathOrQueryParam.name];
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
let requestObject = inputPath.requestType
|
|
436
|
-
? schemas[inputPath.requestType]
|
|
437
|
-
: undefined;
|
|
438
|
-
if (requestObject?.properties) {
|
|
439
|
-
// If there are any properties other than body, query, pathParams and headers
|
|
440
|
-
// we should throw an error as we don't know what to do with them
|
|
441
|
-
const otherKeys = Object.keys(requestObject.properties).filter(k => !["body", "query", "pathParams", "headers"].includes(k));
|
|
442
|
-
if (otherKeys.length > 0) {
|
|
443
|
-
throw new core.GeneralError("commands", "commands.ts-to-openapi.unsupportedProperties", {
|
|
444
|
-
keys: otherKeys.join(", ")
|
|
445
|
-
});
|
|
446
|
-
}
|
|
447
|
-
// If there is a path params object convert these to params
|
|
448
|
-
if (core.Is.object(requestObject.properties.pathParams)) {
|
|
449
|
-
for (const pathParam of pathQueryHeaderParams) {
|
|
450
|
-
const prop = requestObject.properties.pathParams.properties?.[pathParam.name];
|
|
451
|
-
if (core.Is.object(prop)) {
|
|
452
|
-
pathParam.description = prop.description ?? pathParam.description;
|
|
453
|
-
pathParam.schema = {
|
|
454
|
-
type: prop.type,
|
|
455
|
-
enum: prop.enum,
|
|
456
|
-
$ref: prop.$ref
|
|
457
|
-
};
|
|
458
|
-
pathParam.required = true;
|
|
459
|
-
delete requestObject.properties.pathParams.properties?.[pathParam.name];
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
delete requestObject.properties.pathParams;
|
|
463
|
-
}
|
|
464
|
-
// If there is a query object convert these to params as well
|
|
465
|
-
if (core.Is.object(requestObject.properties.query)) {
|
|
466
|
-
for (const prop in requestObject.properties.query.properties) {
|
|
467
|
-
const queryProp = requestObject.properties.query.properties[prop];
|
|
468
|
-
if (core.Is.object(queryProp)) {
|
|
469
|
-
let example;
|
|
470
|
-
if (core.Is.object(requestExample.query) && requestExample.query[prop]) {
|
|
471
|
-
example = requestExample.query[prop];
|
|
472
|
-
}
|
|
473
|
-
pathQueryHeaderParams.push({
|
|
474
|
-
name: prop,
|
|
475
|
-
description: queryProp.description,
|
|
476
|
-
required: Boolean(requestObject.required?.includes(prop)),
|
|
477
|
-
schema: {
|
|
478
|
-
type: queryProp.type,
|
|
479
|
-
enum: queryProp.enum,
|
|
480
|
-
$ref: queryProp.$ref
|
|
481
|
-
},
|
|
482
|
-
in: "query",
|
|
483
|
-
example
|
|
484
|
-
});
|
|
485
|
-
delete requestObject.properties.query.properties[prop];
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
delete requestObject.properties.query;
|
|
489
|
-
}
|
|
490
|
-
// If there are headers in the object convert these to spec params
|
|
491
|
-
if (core.Is.object(requestObject.properties.headers)) {
|
|
492
|
-
const headerProperties = requestObject.properties.headers.properties;
|
|
493
|
-
for (const prop in headerProperties) {
|
|
494
|
-
const headerSchema = headerProperties[prop];
|
|
495
|
-
if (core.Is.object(headerSchema)) {
|
|
496
|
-
let example;
|
|
497
|
-
if (core.Is.object(requestExample.headers) && requestExample.headers[prop]) {
|
|
498
|
-
example = requestExample.headers[prop];
|
|
499
|
-
}
|
|
500
|
-
pathQueryHeaderParams.push({
|
|
501
|
-
name: prop,
|
|
502
|
-
description: headerSchema.description,
|
|
503
|
-
required: true,
|
|
504
|
-
schema: {
|
|
505
|
-
type: "string"
|
|
506
|
-
},
|
|
507
|
-
in: "header",
|
|
508
|
-
style: "simple",
|
|
509
|
-
example
|
|
510
|
-
});
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
delete requestObject.properties.headers;
|
|
514
|
-
}
|
|
515
|
-
// If we have used all the properties from the object in the
|
|
516
|
-
// path we should remove it.
|
|
517
|
-
if (Object.keys(requestObject.properties).length === 0 && inputPath.requestType) {
|
|
518
|
-
delete schemas[inputPath.requestType];
|
|
519
|
-
requestObject = undefined;
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
if (config.restRoutes) {
|
|
523
|
-
let fullPath = core.StringHelper.trimTrailingSlashes(inputPath.path);
|
|
524
|
-
if (fullPath.length === 0) {
|
|
525
|
-
fullPath = "/";
|
|
526
|
-
}
|
|
527
|
-
openApi.paths[fullPath] ??= {};
|
|
528
|
-
const method = inputPath.method.toLowerCase();
|
|
529
|
-
openApi.paths[fullPath][method] = {
|
|
530
|
-
operationId: inputPath.operationId,
|
|
531
|
-
summary: inputPath.summary,
|
|
532
|
-
tags: [inputPath.tag],
|
|
533
|
-
parameters: pathQueryHeaderParams.length > 0
|
|
534
|
-
? pathQueryHeaderParams.map(p => ({
|
|
535
|
-
name: p.name,
|
|
536
|
-
description: p.description,
|
|
537
|
-
in: p.in,
|
|
538
|
-
required: p.required,
|
|
539
|
-
schema: p.schema,
|
|
540
|
-
style: p.style,
|
|
541
|
-
example: p.example
|
|
542
|
-
}))
|
|
543
|
-
: undefined
|
|
544
|
-
};
|
|
545
|
-
if (pathSpecificAuthSecurity.length > 0) {
|
|
546
|
-
openApi.paths[fullPath][method].security = pathSpecificAuthSecurity;
|
|
547
|
-
}
|
|
548
|
-
if (requestObject && inputPath.requestType) {
|
|
549
|
-
let examples;
|
|
550
|
-
if (core.Is.arrayValue(inputPath.requestExamples)) {
|
|
551
|
-
for (const example of inputPath.requestExamples) {
|
|
552
|
-
if (core.Is.object(example.request) &&
|
|
553
|
-
!core.Is.undefined(example.request.body)) {
|
|
554
|
-
examples ??= {};
|
|
555
|
-
examples[example.id] = {
|
|
556
|
-
summary: example.description,
|
|
557
|
-
value: example.request.body
|
|
558
|
-
};
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
let requestMimeType;
|
|
563
|
-
if (core.Is.stringValue(inputPath.requestMimeType)) {
|
|
564
|
-
requestMimeType = inputPath.requestMimeType;
|
|
565
|
-
}
|
|
566
|
-
else {
|
|
567
|
-
const hasBody = core.Is.notEmpty(schemas[inputPath.requestType]?.properties?.body);
|
|
568
|
-
if (hasBody) {
|
|
569
|
-
requestMimeType = web.MimeTypes.Json;
|
|
570
|
-
}
|
|
571
|
-
else {
|
|
572
|
-
requestMimeType = web.MimeTypes.PlainText;
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
openApi.paths[fullPath][method].requestBody = {
|
|
576
|
-
description: requestObject.description,
|
|
577
|
-
required: true,
|
|
578
|
-
content: {
|
|
579
|
-
[requestMimeType]: {
|
|
580
|
-
schema: {
|
|
581
|
-
$ref: `#/definitions/${inputPath.requestType}`
|
|
582
|
-
},
|
|
583
|
-
examples
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
};
|
|
587
|
-
}
|
|
588
|
-
if (responses.length > 0) {
|
|
589
|
-
const openApiResponses = {};
|
|
590
|
-
for (const response of responses) {
|
|
591
|
-
const code = response.code;
|
|
592
|
-
if (code) {
|
|
593
|
-
delete response.code;
|
|
594
|
-
openApiResponses[code] ??= {};
|
|
595
|
-
openApiResponses[code] = core.ObjectHelper.merge(openApiResponses[code], response);
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
openApi.paths[fullPath][method].responses = openApiResponses;
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
await finaliseOutput(usedCommonResponseTypes, schemas, openApi, securitySchemes, config.externalReferences, autoExpandTypes, outputFile);
|
|
604
|
-
}
|
|
605
|
-
/**
|
|
606
|
-
* Finalise the schemas and output the spec.
|
|
607
|
-
* @param usedCommonResponseTypes The common response types used.
|
|
608
|
-
* @param schemas The schemas.
|
|
609
|
-
* @param openApi The OpenAPI spec.
|
|
610
|
-
* @param securitySchemes The security schemes.
|
|
611
|
-
* @param externalReferences The external references.
|
|
612
|
-
* @param autoExpandTypes The auto expand types.
|
|
613
|
-
* @param outputFile The output file.
|
|
614
|
-
*/
|
|
615
|
-
async function finaliseOutput(usedCommonResponseTypes, schemas, openApi, securitySchemes, externalReferences, autoExpandTypes, outputFile) {
|
|
616
|
-
cliCore.CLIDisplay.break();
|
|
617
|
-
cliCore.CLIDisplay.task(core.I18n.formatMessage("commands.ts-to-openapi.progress.finalisingSchemas"));
|
|
618
|
-
// Remove the response codes that we haven't used
|
|
619
|
-
for (const httpStatusCode in HTTP_STATUS_CODE_MAP) {
|
|
620
|
-
if (!usedCommonResponseTypes.includes(HTTP_STATUS_CODE_MAP[httpStatusCode].responseType)) {
|
|
621
|
-
delete schemas[HTTP_STATUS_CODE_MAP[httpStatusCode].responseType];
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
const substituteSchemas = [];
|
|
625
|
-
const finalExternals = {};
|
|
626
|
-
// Remove the I, < and > from names
|
|
627
|
-
const finalSchemas = {};
|
|
628
|
-
for (const schema in schemas) {
|
|
629
|
-
const props = schemas[schema].properties;
|
|
630
|
-
let skipSchema = false;
|
|
631
|
-
if (core.Is.object(props)) {
|
|
632
|
-
tidySchemaProperties(props);
|
|
633
|
-
// Any request/response objects should be added to the final schemas
|
|
634
|
-
// but only the body property, if there is no body then we don't
|
|
635
|
-
// need to add it to the schemas
|
|
636
|
-
if (schema.endsWith("Response") || schema.endsWith("Request")) {
|
|
637
|
-
if (core.Is.object(props.body)) {
|
|
638
|
-
schemas[schema] = props.body;
|
|
639
|
-
}
|
|
640
|
-
else {
|
|
641
|
-
skipSchema = true;
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
// If the schema is external then remove it from the final schemas
|
|
646
|
-
if (core.Is.object(externalReferences)) {
|
|
647
|
-
for (const external in externalReferences) {
|
|
648
|
-
const re = new RegExp(`^I?${external}(?<!Request|Response)$`);
|
|
649
|
-
if (re.test(schema)) {
|
|
650
|
-
skipSchema = true;
|
|
651
|
-
finalExternals[core.StringHelper.stripPrefix(schema)] = schema.replace(re, externalReferences[external]);
|
|
652
|
-
break;
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
// We can remove any auto expand types from the final schema as they
|
|
657
|
-
// will have been expanded inline so no need to keep them
|
|
658
|
-
for (const autoExpandType of autoExpandTypes) {
|
|
659
|
-
const regExp = toolsCore.JsonSchemaHelper.stringToRegEx(autoExpandType);
|
|
660
|
-
if (regExp.test(schema)) {
|
|
661
|
-
skipSchema = true;
|
|
662
|
-
break;
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
if (!skipSchema) {
|
|
666
|
-
// If the final schema has no properties and is just a ref to another object type
|
|
667
|
-
// then replace the references with that of the referenced type
|
|
668
|
-
const ref = schemas[schema].$ref;
|
|
669
|
-
if (!core.Is.arrayValue(schemas[schema].properties) && core.Is.stringValue(ref)) {
|
|
670
|
-
substituteSchemas.push({ from: schema, to: ref });
|
|
671
|
-
}
|
|
672
|
-
else {
|
|
673
|
-
let finalName = schema;
|
|
674
|
-
// If the type has an interface name e.g. ISomething then strip the I
|
|
675
|
-
if (/I[A-Z]/.test(finalName)) {
|
|
676
|
-
finalName = finalName.slice(1);
|
|
677
|
-
}
|
|
678
|
-
if (finalName.endsWith("[]")) {
|
|
679
|
-
finalName = `ListOf${finalName.slice(0, -2)}`;
|
|
680
|
-
}
|
|
681
|
-
finalSchemas[finalName] = schemas[schema];
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
// Remove standard types that we don't want in the final output
|
|
686
|
-
const removeTypes = ["HttpStatusCode", "Uint8Array", "ArrayBuffer"];
|
|
687
|
-
for (const type of removeTypes) {
|
|
688
|
-
delete finalSchemas[type];
|
|
689
|
-
}
|
|
690
|
-
for (const type in finalSchemas) {
|
|
691
|
-
toolsCore.JsonSchemaHelper.processArrays(finalSchemas[type]);
|
|
692
|
-
}
|
|
693
|
-
const schemaKeys = Object.keys(finalSchemas);
|
|
694
|
-
schemaKeys.sort();
|
|
695
|
-
const sortedSchemas = {};
|
|
696
|
-
for (const key of schemaKeys) {
|
|
697
|
-
sortedSchemas[key] = finalSchemas[key];
|
|
698
|
-
}
|
|
699
|
-
openApi.components = {
|
|
700
|
-
schemas: sortedSchemas,
|
|
701
|
-
securitySchemes
|
|
702
|
-
};
|
|
703
|
-
let json = JSON.stringify(openApi, undefined, "\t");
|
|
704
|
-
// Remove the reference only schemas, repeating until no more substitutions
|
|
705
|
-
let performedSubstitution;
|
|
706
|
-
do {
|
|
707
|
-
performedSubstitution = false;
|
|
708
|
-
for (const substituteSchema of substituteSchemas) {
|
|
709
|
-
const schemaParts = substituteSchema.to.split("/");
|
|
710
|
-
const find = new RegExp(`#/definitions/${substituteSchema.from}`, "g");
|
|
711
|
-
if (find.test(json)) {
|
|
712
|
-
json = json.replace(find, `#/definitions/${schemaParts[schemaParts.length - 1]}`);
|
|
713
|
-
performedSubstitution = true;
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
} while (performedSubstitution);
|
|
717
|
-
// Update the location of the components
|
|
718
|
-
json = json.replace(/#\/definitions\//g, "#/components/schemas/");
|
|
719
|
-
// Remove the I from the type names as long as they are interfaces
|
|
720
|
-
json = json.replace(/#\/components\/schemas\/I([A-Z].*)/g, "#/components/schemas/$1");
|
|
721
|
-
// Remove the array [] from the type names
|
|
722
|
-
// eslint-disable-next-line unicorn/better-regex
|
|
723
|
-
json = json.replace(/#\/components\/schemas\/(.*)\[\]/g, "#/components/schemas/ListOf$1");
|
|
724
|
-
json = toolsCore.JsonSchemaHelper.normaliseTypeName(json);
|
|
725
|
-
// Remove external references
|
|
726
|
-
for (const finalExternal in finalExternals) {
|
|
727
|
-
json = json.replace(new RegExp(`"#/components/schemas/${core.StringHelper.stripPrefix(finalExternal)}"`, "g"), `"${finalExternals[finalExternal]}"`);
|
|
728
|
-
}
|
|
729
|
-
cliCore.CLIDisplay.task(core.I18n.formatMessage("commands.ts-to-openapi.progress.writingOutputFile"), outputFile);
|
|
730
|
-
try {
|
|
731
|
-
await promises.mkdir(path.dirname(outputFile), { recursive: true });
|
|
732
|
-
}
|
|
733
|
-
catch { }
|
|
734
|
-
await promises.writeFile(outputFile, `${json}\n`);
|
|
735
|
-
}
|
|
736
|
-
/**
|
|
737
|
-
* Build the security schemas from the config.
|
|
738
|
-
* @param config The configuration.
|
|
739
|
-
* @param securitySchemes The security schemes.
|
|
740
|
-
* @param authSecurity The auth security.
|
|
741
|
-
*/
|
|
742
|
-
function buildSecurity(config, securitySchemes, authSecurity) {
|
|
743
|
-
if (core.Is.arrayValue(config.authMethods)) {
|
|
744
|
-
for (const authMethod of config.authMethods) {
|
|
745
|
-
const security = {};
|
|
746
|
-
if (authMethod === "basic") {
|
|
747
|
-
securitySchemes.basicAuthScheme = {
|
|
748
|
-
type: "http",
|
|
749
|
-
scheme: "basic"
|
|
750
|
-
};
|
|
751
|
-
security.basicAuthScheme = [];
|
|
752
|
-
}
|
|
753
|
-
else if (authMethod === "jwtBearer") {
|
|
754
|
-
securitySchemes.jwtBearerAuthScheme = {
|
|
755
|
-
type: "http",
|
|
756
|
-
scheme: "bearer",
|
|
757
|
-
bearerFormat: "JWT"
|
|
758
|
-
};
|
|
759
|
-
security.jwtBearerAuthScheme = [];
|
|
760
|
-
}
|
|
761
|
-
else if (authMethod === "jwtCookie") {
|
|
762
|
-
securitySchemes.jwtCookieAuthScheme = {
|
|
763
|
-
type: "apiKey",
|
|
764
|
-
in: "cookie",
|
|
765
|
-
name: "auth_token"
|
|
766
|
-
};
|
|
767
|
-
security.jwtCookieAuthScheme = [];
|
|
768
|
-
}
|
|
769
|
-
authSecurity.push(security);
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
/**
|
|
774
|
-
* Process the REST details for a package.
|
|
775
|
-
* @param baseDir The base directory other locations are relative to.
|
|
776
|
-
* @param prefix The prefix.
|
|
777
|
-
* @param restDetails The package details.
|
|
778
|
-
* @returns The paths and schemas for the input.
|
|
779
|
-
* @internal
|
|
780
|
-
*/
|
|
781
|
-
async function processPackageRestDetails(restRoutes) {
|
|
782
|
-
const paths = [];
|
|
783
|
-
cliCore.CLIDisplay.task(core.I18n.formatMessage("commands.ts-to-openapi.progress.processingRoutes"));
|
|
784
|
-
for (const route of restRoutes) {
|
|
785
|
-
cliCore.CLIDisplay.value(core.I18n.formatMessage("commands.ts-to-openapi.labels.route"), `${route.operationId} ${route.method} ${route.path}`, 1);
|
|
786
|
-
const pathParameters = [];
|
|
787
|
-
const pathPaths = route.path.split("/");
|
|
788
|
-
const finalPathParts = [];
|
|
789
|
-
for (const part of pathPaths) {
|
|
790
|
-
if (part.startsWith(":")) {
|
|
791
|
-
finalPathParts.push(`{${part.slice(1)}}`);
|
|
792
|
-
pathParameters.push(part.slice(1));
|
|
793
|
-
}
|
|
794
|
-
else {
|
|
795
|
-
finalPathParts.push(part);
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
const responseType = [];
|
|
799
|
-
// If there is no response type automatically add a success
|
|
800
|
-
if (core.Is.empty(route.responseType)) {
|
|
801
|
-
// But only if we haven't got a response already for different content type
|
|
802
|
-
if (responseType.length === 0) {
|
|
803
|
-
responseType.push({
|
|
804
|
-
type: "IOkResponse",
|
|
805
|
-
statusCode: web.HttpStatusCode.ok
|
|
806
|
-
});
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
else if (core.Is.array(route.responseType)) {
|
|
810
|
-
// Find the response codes for the response types
|
|
811
|
-
for (const rt of route.responseType) {
|
|
812
|
-
const responseCode = getHttpStatusCodeFromType(rt.type);
|
|
813
|
-
responseType.push({
|
|
814
|
-
...rt,
|
|
815
|
-
mimeType: rt.mimeType,
|
|
816
|
-
statusCode: responseCode,
|
|
817
|
-
examples: rt.examples
|
|
818
|
-
});
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
const inputPath = {
|
|
822
|
-
path: finalPathParts.join("/"),
|
|
823
|
-
method: route.method,
|
|
824
|
-
pathParameters,
|
|
825
|
-
operationId: route.operationId,
|
|
826
|
-
tag: route.tag,
|
|
827
|
-
summary: route.summary,
|
|
828
|
-
requestType: route.requestType?.type,
|
|
829
|
-
requestMimeType: route.requestType?.mimeType,
|
|
830
|
-
requestExamples: route.requestType?.examples,
|
|
831
|
-
responseType,
|
|
832
|
-
responseCodes: ["badRequest", "internalServerError"],
|
|
833
|
-
skipAuth: route.skipAuth ?? false
|
|
834
|
-
};
|
|
835
|
-
const handlerSource = route.handler.toString();
|
|
836
|
-
let match;
|
|
837
|
-
const re = /httpstatuscode\.([_a-z]*)/gi;
|
|
838
|
-
while ((match = re.exec(handlerSource)) !== null) {
|
|
839
|
-
inputPath.responseCodes.push(match[1]);
|
|
840
|
-
}
|
|
841
|
-
paths.push(inputPath);
|
|
842
|
-
}
|
|
843
|
-
cliCore.CLIDisplay.break();
|
|
844
|
-
return paths;
|
|
845
|
-
}
|
|
846
|
-
/**
|
|
847
|
-
* Generate schemas for the models.
|
|
848
|
-
* @param modelDirWildcards The filenames for all the models.
|
|
849
|
-
* @param types The types of the schema objects.
|
|
850
|
-
* @param outputWorkingDir The working directory.
|
|
851
|
-
* @returns Nothing.
|
|
852
|
-
* @internal
|
|
853
|
-
*/
|
|
854
|
-
async function generateSchemas(modelDirWildcards, types, autoExpandTypes, outputWorkingDir) {
|
|
855
|
-
const allSchemas = {};
|
|
856
|
-
const arraySingularTypes = [];
|
|
857
|
-
for (const type of types) {
|
|
858
|
-
if (type.endsWith("[]")) {
|
|
859
|
-
const singularType = type.slice(0, -2);
|
|
860
|
-
arraySingularTypes.push(singularType);
|
|
861
|
-
if (!types.includes(singularType)) {
|
|
862
|
-
types.push(singularType);
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
for (const files of modelDirWildcards) {
|
|
867
|
-
cliCore.CLIDisplay.value(core.I18n.formatMessage("commands.ts-to-openapi.progress.models"), files.replace(/\\/g, "/"), 1);
|
|
868
|
-
const generator = tsJsonSchemaGenerator.createGenerator({
|
|
869
|
-
path: files.replace(/\\/g, "/"),
|
|
870
|
-
type: "*",
|
|
871
|
-
tsconfig: path.join(outputWorkingDir, "tsconfig.json"),
|
|
872
|
-
skipTypeCheck: true,
|
|
873
|
-
expose: "all"
|
|
874
|
-
});
|
|
875
|
-
const schema = generator.createSchema("*");
|
|
876
|
-
if (schema.definitions) {
|
|
877
|
-
for (const def in schema.definitions) {
|
|
878
|
-
const defSub = toolsCore.JsonSchemaHelper.normaliseTypeName(def);
|
|
879
|
-
allSchemas[defSub] = schema.definitions[def];
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
const referencedSchemas = {};
|
|
884
|
-
toolsCore.JsonSchemaHelper.extractTypes(allSchemas, [...types, ...autoExpandTypes], referencedSchemas);
|
|
885
|
-
toolsCore.JsonSchemaHelper.expandTypes(referencedSchemas, autoExpandTypes);
|
|
886
|
-
for (const arraySingularType of arraySingularTypes) {
|
|
887
|
-
referencedSchemas[`${arraySingularType}[]`] = {
|
|
888
|
-
type: "array",
|
|
889
|
-
items: {
|
|
890
|
-
$ref: `#/components/schemas/${arraySingularType}`
|
|
891
|
-
}
|
|
892
|
-
};
|
|
893
|
-
}
|
|
894
|
-
return referencedSchemas;
|
|
895
|
-
}
|
|
896
|
-
/**
|
|
897
|
-
* Tidy up the schemas for use in OpenAPI context.
|
|
898
|
-
* @param props The properties to tidy up.
|
|
899
|
-
* @internal
|
|
900
|
-
*/
|
|
901
|
-
function tidySchemaProperties(props) {
|
|
902
|
-
for (const prop in props) {
|
|
903
|
-
const p = props[prop];
|
|
904
|
-
if (core.Is.object(p)) {
|
|
905
|
-
// For OpenAPI we don't include a description for
|
|
906
|
-
// items that have refs
|
|
907
|
-
if (p.$ref) {
|
|
908
|
-
delete p.description;
|
|
909
|
-
}
|
|
910
|
-
if (p.properties) {
|
|
911
|
-
tidySchemaProperties(p.properties);
|
|
912
|
-
}
|
|
913
|
-
if (p.items &&
|
|
914
|
-
core.Is.object(p.items) &&
|
|
915
|
-
core.Is.object(p.items.properties)) {
|
|
916
|
-
tidySchemaProperties(p.items.properties);
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
/**
|
|
922
|
-
* Load the packages from config and get the routes and tags from them.
|
|
923
|
-
* @param tsToOpenApiConfig The app config.
|
|
924
|
-
* @param outputWorkingDir The working directory.
|
|
925
|
-
* @param typeRoots The model roots.
|
|
926
|
-
* @returns The routes and tags for each package.
|
|
927
|
-
* @internal
|
|
928
|
-
*/
|
|
929
|
-
async function loadPackages(tsToOpenApiConfig, outputWorkingDir, typeRoots) {
|
|
930
|
-
const restRoutes = [];
|
|
931
|
-
let localNpmRoot = await cliCore.CLIUtils.findNpmRoot(process.cwd());
|
|
932
|
-
localNpmRoot = localNpmRoot.replace(/[/\\]node_modules/, "");
|
|
933
|
-
const packages = [];
|
|
934
|
-
const localPackages = [];
|
|
935
|
-
for (const configRestRoutes of tsToOpenApiConfig.restRoutes) {
|
|
936
|
-
if (core.Is.stringValue(configRestRoutes.package)) {
|
|
937
|
-
const existsLocally = await cliCore.CLIUtils.dirExists(path.join(localNpmRoot, "node_modules", configRestRoutes.package));
|
|
938
|
-
if (existsLocally) {
|
|
939
|
-
if (!localPackages.includes(configRestRoutes.package)) {
|
|
940
|
-
localPackages.push(configRestRoutes.package);
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
else {
|
|
944
|
-
const version = configRestRoutes.version ?? "latest";
|
|
945
|
-
const newPackage = `${configRestRoutes.package}@${version}`;
|
|
946
|
-
if (!packages.includes(newPackage)) {
|
|
947
|
-
packages.push(`${configRestRoutes.package}@${version}`);
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
if (packages.length > 0) {
|
|
953
|
-
cliCore.CLIDisplay.task(core.I18n.formatMessage("commands.ts-to-openapi.progress.installingNpmPackages"), packages.join(" "));
|
|
954
|
-
await cliCore.CLIUtils.runShellCmd("npm", ["install", ...packages], outputWorkingDir);
|
|
955
|
-
cliCore.CLIDisplay.break();
|
|
956
|
-
}
|
|
957
|
-
for (const configRestRoutes of tsToOpenApiConfig.restRoutes) {
|
|
958
|
-
const typeFolders = ["models", "errors"];
|
|
959
|
-
const packageName = configRestRoutes.package;
|
|
960
|
-
const packageRoot = configRestRoutes.packageRoot;
|
|
961
|
-
if (!core.Is.stringValue(packageName) && !core.Is.stringValue(packageRoot)) {
|
|
962
|
-
// eslint-disable-next-line no-restricted-syntax
|
|
963
|
-
throw new Error("Package name or root must be specified");
|
|
964
|
-
}
|
|
965
|
-
let rootFolder;
|
|
966
|
-
let npmResolveFolder;
|
|
967
|
-
if (core.Is.stringValue(packageName)) {
|
|
968
|
-
if (localPackages.includes(packageName)) {
|
|
969
|
-
npmResolveFolder = localNpmRoot;
|
|
970
|
-
rootFolder = path.join(localNpmRoot, "node_modules", packageName);
|
|
971
|
-
}
|
|
972
|
-
else {
|
|
973
|
-
npmResolveFolder = outputWorkingDir;
|
|
974
|
-
rootFolder = path.join(outputWorkingDir, "node_modules", packageName);
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
else {
|
|
978
|
-
rootFolder = path.resolve(packageRoot ?? "");
|
|
979
|
-
npmResolveFolder = rootFolder;
|
|
980
|
-
}
|
|
981
|
-
const pkgJson = (await cliCore.CLIUtils.readJsonFile(path.join(rootFolder, "package.json"))) ?? { name: "" };
|
|
982
|
-
cliCore.CLIDisplay.task(core.I18n.formatMessage("commands.ts-to-openapi.progress.processingPackage"), pkgJson.name);
|
|
983
|
-
for (const typeFolder of typeFolders) {
|
|
984
|
-
const typesDir = path.join(rootFolder, "dist", "types", typeFolder);
|
|
985
|
-
if (await cliCore.CLIUtils.dirExists(typesDir)) {
|
|
986
|
-
const newRoot = path.join(typesDir, "**/*.ts");
|
|
987
|
-
if (!typeRoots.includes(newRoot)) {
|
|
988
|
-
typeRoots.push(newRoot);
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
if (pkgJson.dependencies) {
|
|
993
|
-
const nodeModulesFolder = await cliCore.CLIUtils.findNpmRoot(npmResolveFolder);
|
|
994
|
-
for (const dep in pkgJson.dependencies) {
|
|
995
|
-
if (dep.startsWith("@twin.org")) {
|
|
996
|
-
for (const typeFolder of typeFolders) {
|
|
997
|
-
const typesDirDep = path.join(nodeModulesFolder, dep, "dist", "types", typeFolder);
|
|
998
|
-
if (await cliCore.CLIUtils.dirExists(typesDirDep)) {
|
|
999
|
-
const newRoot = path.join(typesDirDep, "**/*.ts");
|
|
1000
|
-
if (!typeRoots.includes(newRoot)) {
|
|
1001
|
-
typeRoots.push(newRoot);
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
cliCore.CLIDisplay.task(core.I18n.formatMessage("commands.ts-to-openapi.progress.importingModule"), pkgJson.name);
|
|
1009
|
-
const pkg = await import(`file://${path.join(rootFolder, "dist/esm/index.mjs")}`);
|
|
1010
|
-
if (!core.Is.array(pkg.restEntryPoints)) {
|
|
1011
|
-
throw new core.GeneralError("commands", "commands.ts-to-openapi.missingRestRoutesEntryPoints", {
|
|
1012
|
-
method: "restEntryPoints",
|
|
1013
|
-
package: pkgJson.name
|
|
1014
|
-
});
|
|
1015
|
-
}
|
|
1016
|
-
const packageEntryPoints = pkg.restEntryPoints;
|
|
1017
|
-
const entryPoints = configRestRoutes.entryPoints ?? packageEntryPoints.map(e => ({ name: e.name }));
|
|
1018
|
-
for (const entryPoint of entryPoints) {
|
|
1019
|
-
const packageEntryPoint = packageEntryPoints.find(e => e.name === entryPoint.name);
|
|
1020
|
-
if (!core.Is.object(packageEntryPoint)) {
|
|
1021
|
-
throw new core.GeneralError("commands", "commands.ts-to-openapi.missingRestRoutesEntryPoint", {
|
|
1022
|
-
entryPoint: entryPoint.name,
|
|
1023
|
-
package: pkgJson.name
|
|
1024
|
-
});
|
|
1025
|
-
}
|
|
1026
|
-
let baseRouteName = core.StringHelper.trimTrailingSlashes(entryPoint.baseRoutePath ?? packageEntryPoint.defaultBaseRoute ?? "");
|
|
1027
|
-
if (baseRouteName.length > 0) {
|
|
1028
|
-
baseRouteName = `/${core.StringHelper.trimLeadingSlashes(baseRouteName)}`;
|
|
1029
|
-
}
|
|
1030
|
-
let routes = packageEntryPoint.generateRoutes(baseRouteName, "dummy-service");
|
|
1031
|
-
routes = routes.filter(r => !(r.excludeFromSpec ?? false));
|
|
1032
|
-
if (core.Is.stringValue(entryPoint.operationIdDistinguisher)) {
|
|
1033
|
-
for (const route of routes) {
|
|
1034
|
-
route.operationId = `${route.operationId}${entryPoint.operationIdDistinguisher}`;
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
restRoutes.push({
|
|
1038
|
-
restRoutes: routes,
|
|
1039
|
-
tags: packageEntryPoint.tags
|
|
1040
|
-
});
|
|
1041
|
-
}
|
|
1042
|
-
cliCore.CLIDisplay.break();
|
|
1043
|
-
}
|
|
1044
|
-
return restRoutes;
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
// Copyright 2024 IOTA Stiftung.
|
|
1048
|
-
// SPDX-License-Identifier: Apache-2.0.
|
|
1049
|
-
/**
|
|
1050
|
-
* The main entry point for the CLI.
|
|
1051
|
-
*/
|
|
1052
|
-
class CLI extends cliCore.CLIBase {
|
|
1053
|
-
/**
|
|
1054
|
-
* Run the app.
|
|
1055
|
-
* @param argv The process arguments.
|
|
1056
|
-
* @param localesDirectory The directory for the locales, default to relative to the script.
|
|
1057
|
-
* @param options Additional options.
|
|
1058
|
-
* @param options.overrideOutputWidth Override the output width.
|
|
1059
|
-
* @returns The exit code.
|
|
1060
|
-
*/
|
|
1061
|
-
async run(argv, localesDirectory, options) {
|
|
1062
|
-
return this.execute({
|
|
1063
|
-
title: "TWIN TypeScript To OpenAPI",
|
|
1064
|
-
appName: "ts-to-openapi",
|
|
1065
|
-
version: "0.0.2-next.8", // x-release-please-version
|
|
1066
|
-
icon: "⚙️ ",
|
|
1067
|
-
supportsEnvFiles: false,
|
|
1068
|
-
overrideOutputWidth: options?.overrideOutputWidth
|
|
1069
|
-
}, localesDirectory ?? path.join(path.dirname(node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))), "../locales"), argv);
|
|
1070
|
-
}
|
|
1071
|
-
/**
|
|
1072
|
-
* Configure any options or actions at the root program level.
|
|
1073
|
-
* @param program The root program command.
|
|
1074
|
-
*/
|
|
1075
|
-
configureRoot(program) {
|
|
1076
|
-
buildCommandTsToOpenApi(program);
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
exports.CLI = CLI;
|
|
1081
|
-
exports.actionCommandTsToOpenApi = actionCommandTsToOpenApi;
|
|
1082
|
-
exports.buildCommandTsToOpenApi = buildCommandTsToOpenApi;
|
|
1083
|
-
exports.tsToOpenApi = tsToOpenApi;
|