@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.
Files changed (36) hide show
  1. package/bin/index.js +1 -1
  2. package/dist/es/cli.js +37 -0
  3. package/dist/es/cli.js.map +1 -0
  4. package/dist/es/commands/httpStatusCodeMap.js +115 -0
  5. package/dist/es/commands/httpStatusCodeMap.js.map +1 -0
  6. package/dist/{esm/index.mjs → es/commands/tsToOpenApi.js} +27 -168
  7. package/dist/es/commands/tsToOpenApi.js.map +1 -0
  8. package/dist/es/index.js +7 -0
  9. package/dist/es/index.js.map +1 -0
  10. package/dist/es/models/IInputPath.js +2 -0
  11. package/dist/es/models/IInputPath.js.map +1 -0
  12. package/dist/es/models/IInputResult.js +2 -0
  13. package/dist/es/models/IInputResult.js.map +1 -0
  14. package/dist/es/models/IRestRoute.js +2 -0
  15. package/dist/es/models/IRestRoute.js.map +1 -0
  16. package/dist/es/models/IRestRouteEntryPoints.js +2 -0
  17. package/dist/es/models/IRestRouteEntryPoints.js.map +1 -0
  18. package/dist/es/models/ITag.js +4 -0
  19. package/dist/es/models/ITag.js.map +1 -0
  20. package/dist/es/models/ITsToOpenApiConfig.js +2 -0
  21. package/dist/es/models/ITsToOpenApiConfig.js.map +1 -0
  22. package/dist/es/models/ITsToOpenApiConfigEntryPoint.js +4 -0
  23. package/dist/es/models/ITsToOpenApiConfigEntryPoint.js.map +1 -0
  24. package/dist/locales/en.json +46 -73
  25. package/dist/types/commands/tsToOpenApi.d.ts +1 -1
  26. package/dist/types/index.d.ts +4 -4
  27. package/dist/types/models/IInputResult.d.ts +11 -3
  28. package/dist/types/models/IRestRoute.d.ts +98 -0
  29. package/dist/types/models/IRestRouteEntryPoints.d.ts +23 -0
  30. package/dist/types/models/ITag.d.ts +13 -0
  31. package/dist/types/models/ITsToOpenApiConfig.d.ts +1 -1
  32. package/docs/changelog.md +64 -0
  33. package/docs/examples.md +1 -1
  34. package/locales/.validate-ignore +1 -0
  35. package/package.json +20 -11
  36. package/dist/cjs/index.cjs +0 -1083
@@ -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;