@technicity/openapi-sdk-generator 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,954 @@
1
+ import * as path from "path";
2
+ import * as fs from "fs";
3
+ import * as os from "os";
4
+ import * as child_process from "child_process";
5
+ import * as fetch from "node-fetch";
6
+ import { compile } from "json-schema-to-typescript";
7
+ const SwaggerParser = require("@apidevtools/swagger-parser");
8
+ const Ajv = require("ajv");
9
+ const toJsonSchema = require("@openapi-contrib/openapi-schema-to-json-schema");
10
+ const fse = require("fs-extra");
11
+ const traverse = require("json-schema-traverse");
12
+ const { json2ts } = require("json-ts");
13
+ const prettier = require("prettier");
14
+ const prependFile = require("prepend-file");
15
+
16
+ export const schema = {
17
+ type: "object",
18
+ properties: {
19
+ def: {
20
+ type: "string",
21
+ description: "Path or URL to OpenAPI definition",
22
+ },
23
+ outdir: {
24
+ type: "string",
25
+ description: "Directory in which to generate files",
26
+ },
27
+ preProcessDef: {
28
+ type: "string",
29
+ description:
30
+ "String for JS function to run on OpenAPI def before prior to validation and de-referencing",
31
+ },
32
+ cacheType: {
33
+ type: "string",
34
+ enum: ["memory", "none"],
35
+ default: "memory",
36
+ description: "'memory' | 'none'",
37
+ },
38
+ throwOnNotOkayRes: {
39
+ type: "boolean",
40
+ default: false,
41
+ description:
42
+ "Whether a SDK method should throw when the response status code is not okay",
43
+ },
44
+ packageName: {
45
+ type: "string",
46
+ },
47
+ packageAuthor: {
48
+ type: "string",
49
+ },
50
+ packageDescription: {
51
+ type: "string",
52
+ },
53
+ module: {
54
+ type: "string",
55
+ enum: ["CommonJS", "ES2020"],
56
+ default: "CommonJS",
57
+ description: "module type for generated SDK",
58
+ },
59
+ },
60
+ additionalProperties: false,
61
+ required: ["def", "outdir"],
62
+ };
63
+
64
+ const ajv = new Ajv({ allErrors: true, useDefaults: true, coerceTypes: true });
65
+
66
+ export async function generate(opts) {
67
+ const valid = ajv.validate(schema, opts);
68
+ if (!valid) {
69
+ console.log(ajv.errors);
70
+ process.exit(1);
71
+ }
72
+
73
+ const isUrl =
74
+ opts.def.startsWith("http://") || opts.def.startsWith("https://");
75
+
76
+ if (!isUrl && !fs.existsSync(path.resolve(opts.def))) {
77
+ throw new Error("Invalid path.");
78
+ }
79
+
80
+ let doc = isUrl
81
+ ? await fetch(opts.def, {}).then((res) => res.json())
82
+ : JSON.parse(fs.readFileSync(path.resolve(opts.def), { encoding: "utf8" }));
83
+
84
+ if (opts.preProcessDef) {
85
+ const fn = Function("doc", opts.preProcessDef);
86
+ doc = fn(doc);
87
+ }
88
+
89
+ // https://apitools.dev/swagger-parser/docs/options.html
90
+ const api = await SwaggerParser.validate(doc);
91
+
92
+ // Ignore defs without operationId
93
+ for (let [p, v1] of Object.entries(api.paths)) {
94
+ for (let [m, v2] of Object.entries(v1)) {
95
+ if (
96
+ !getOperationId(v2) ||
97
+ opts.filterDef?.({ pathname: p, method: m }) === false
98
+ ) {
99
+ delete api.paths[p][m];
100
+ }
101
+ }
102
+ }
103
+
104
+ const optsTypeGet = "{headers?: {[k: string]: string}, strategy?: 'network'}";
105
+ const optsType = "{headers?: {[k: string]: string}, clearCache?: boolean}";
106
+
107
+ let routes = await Promise.all(
108
+ Object.keys(api.paths)
109
+ .sort()
110
+ .map(async (path) => {
111
+ let output = "";
112
+ if (api.paths[path].get) {
113
+ let responseType = await createResponseBodyType(
114
+ api.paths[path],
115
+ "get",
116
+ opts.throwOnNotOkayRes
117
+ );
118
+
119
+ let queryType = await createQueryType(api.paths[path].get.parameters);
120
+ const hasUrlParameters = (api.paths[path].get.parameters || []).some(
121
+ (x) => x.in === "path"
122
+ );
123
+
124
+ let getFcn = `${getOperationId(api.paths[path].get)}(
125
+ params${hasUrlParameters ? "" : "?"}: ${createParamType(path).type},
126
+ query?: ${queryType},
127
+ opts?: ${optsTypeGet}
128
+ ) : Promise<${responseType}> {
129
+ let path = "${convertPath(path)}";
130
+ return this._get(path, params, query, opts);
131
+ };`;
132
+
133
+ output += getFcn;
134
+ }
135
+
136
+ if (api.paths[path].post) {
137
+ const method = "post";
138
+ let requestBodyType = await createRequestBodyType(
139
+ api.paths[path],
140
+ method
141
+ );
142
+ const hasNoBody = requestBodyType.type === "undefined";
143
+ const requestBodyContentType = api.paths[path][method]?.requestBody
144
+ ? getRequestBodyContentType(api.paths[path], method)
145
+ : undefined;
146
+ let responseType = await createResponseBodyType(
147
+ api.paths[path],
148
+ method,
149
+ opts.throwOnNotOkayRes
150
+ );
151
+
152
+ // https://stackoverflow.com/a/35799817
153
+
154
+ let fcn = `${getOperationId(api.paths[path][method])}(
155
+ params: ${createParamType(path).type},
156
+ data${hasNoBody ? "?" : ""}: ${requestBodyType.type},
157
+ query?: ${await createQueryType(api.paths[path][method].parameters)},
158
+ opts?: ${optsType}
159
+ ) : Promise<${responseType}> {
160
+ let path = "${convertPath(path)}";
161
+ return this._${method}(path, params, data, query, opts, ${
162
+ requestBodyContentType === undefined ||
163
+ requestBodyContentType === "multipart/form-data"
164
+ ? "undefined"
165
+ : `"${requestBodyContentType}"`
166
+ });
167
+ };`;
168
+
169
+ output += fcn;
170
+ }
171
+
172
+ if (api.paths[path].patch) {
173
+ const method = "patch";
174
+ let requestBodyType = await createRequestBodyType(
175
+ api.paths[path],
176
+ method
177
+ );
178
+ const hasNoBody = requestBodyType.type === "undefined";
179
+ const requestBodyContentType = api.paths[path][method]?.requestBody
180
+ ? getRequestBodyContentType(api.paths[path], method)
181
+ : undefined;
182
+ let responseType = await createResponseBodyType(
183
+ api.paths[path],
184
+ method,
185
+ opts.throwOnNotOkayRes
186
+ );
187
+
188
+ // https://stackoverflow.com/a/35799817
189
+
190
+ let fcn = `${getOperationId(api.paths[path][method])}(
191
+ params: ${createParamType(path).type},
192
+ data${hasNoBody ? "?" : ""}: ${requestBodyType.type},
193
+ query?: ${await createQueryType(api.paths[path][method].parameters)},
194
+ opts?: ${optsType}
195
+ ) : Promise<${responseType}> {
196
+ let path = "${convertPath(path)}";
197
+ return this._${method}(path, params, data, query, opts, ${
198
+ requestBodyContentType === undefined ||
199
+ requestBodyContentType === "multipart/form-data"
200
+ ? "undefined"
201
+ : `"${requestBodyContentType}"`
202
+ });
203
+ };`;
204
+
205
+ output += fcn;
206
+ }
207
+
208
+ if (api.paths[path].put) {
209
+ const method = "put";
210
+ let requestBodyType = await createRequestBodyType(
211
+ api.paths[path],
212
+ method
213
+ );
214
+ const hasNoBody = requestBodyType.type === "undefined";
215
+ const requestBodyContentType = api.paths[path][method]?.requestBody
216
+ ? getRequestBodyContentType(api.paths[path], method)
217
+ : undefined;
218
+ let responseType = await createResponseBodyType(
219
+ api.paths[path],
220
+ method,
221
+ opts.throwOnNotOkayRes
222
+ );
223
+
224
+ // https://stackoverflow.com/a/35799817
225
+
226
+ let fcn = `${getOperationId(api.paths[path][method])}(
227
+ params: ${createParamType(path).type},
228
+ data${hasNoBody ? "?" : ""}: ${requestBodyType.type},
229
+ query?: ${await createQueryType(api.paths[path][method].parameters)},
230
+ opts?: ${optsType}
231
+ ) : Promise<${responseType}> {
232
+ let path = "${convertPath(path)}";
233
+ return this._${method}(path, params, data, query, opts, ${
234
+ requestBodyContentType === undefined ||
235
+ requestBodyContentType === "multipart/form-data"
236
+ ? "undefined"
237
+ : `"${requestBodyContentType}"`
238
+ });
239
+ };`;
240
+
241
+ output += fcn;
242
+ }
243
+
244
+ if (api.paths[path].delete) {
245
+ let responseType = await createResponseBodyType(
246
+ api.paths[path],
247
+ "delete",
248
+ opts.throwOnNotOkayRes
249
+ );
250
+ const hasUrlParameters = (
251
+ api.paths[path].delete.parameters || []
252
+ ).some((x) => x.in === "path");
253
+
254
+ let deleteFcn = `${getOperationId(api.paths[path].delete)}(
255
+ params${hasUrlParameters ? "" : "?"}: ${createParamType(path).type},
256
+ query?: ${await createQueryType(api.paths[path].delete.parameters)},
257
+ opts?: ${optsType}
258
+ ) : Promise<${responseType}> {
259
+ let path = "${convertPath(path)}";
260
+ return this._delete(path, params, query, opts);
261
+ };`;
262
+
263
+ output += deleteFcn;
264
+ }
265
+
266
+ return output;
267
+ })
268
+ );
269
+
270
+ const code = prettier.format(
271
+ getSDKCode(routes.join(" "), opts.throwOnNotOkayRes, opts.cacheType),
272
+ { parser: "typescript" }
273
+ );
274
+
275
+ const tsConfigJSON = {
276
+ compilerOptions: {
277
+ target: "ES2020",
278
+ module: opts.module,
279
+ moduleResolution: "node",
280
+ declaration: true,
281
+ outDir: "./dist",
282
+ forceConsistentCasingInFileNames: true,
283
+ },
284
+ include: ["index.ts"],
285
+ };
286
+
287
+ const typeScriptVersion = "4.5.5";
288
+
289
+ const packageJSON = {
290
+ name: "temp",
291
+ version: "1.0.0",
292
+ dependencies: {
293
+ "cross-fetch": "^3.1.5",
294
+ "node-cache": "^4.2.1",
295
+ qs: "^6.9.4",
296
+ "@types/qs": "^6.9.5",
297
+ },
298
+ devDependencies: {
299
+ "@types/node": "13.9.1",
300
+ typescript: typeScriptVersion,
301
+ },
302
+ };
303
+
304
+ const tmpDirPath = path.join(os.tmpdir(), `osg-${Date.now()}`);
305
+ fse.mkdirpSync(tmpDirPath);
306
+ fs.writeFileSync(path.join(tmpDirPath, "index.ts"), code);
307
+ fs.writeFileSync(
308
+ path.join(tmpDirPath, "package.json"),
309
+ JSON.stringify(packageJSON, null, 2)
310
+ );
311
+ fs.writeFileSync(
312
+ path.join(tmpDirPath, "tsconfig.json"),
313
+ JSON.stringify(tsConfigJSON, null, 2)
314
+ );
315
+ child_process.execSync("npm i", { cwd: tmpDirPath, stdio: "inherit" });
316
+
317
+ const buildOutputPath = path.join(tmpDirPath, "dist");
318
+ // const nccVersion = "0.33.1";
319
+ // child_process.execSync(
320
+ // `npx --yes -p @vercel/ncc@${nccVersion} ncc build ./index.ts -o dist`,
321
+ // { cwd: tmpDirPath, stdio: "inherit" }
322
+ // );
323
+ // TODO - remove if we get ncc build output to work with CRA build
324
+ child_process.execSync(`npx -p typescript@${typeScriptVersion} tsc`, {
325
+ cwd: tmpDirPath,
326
+ stdio: "inherit",
327
+ });
328
+ // Disable ESLint for the compiled file. Mainly for CRA.
329
+ prependFile.sync(
330
+ path.join(buildOutputPath, "index.js"),
331
+ `/* eslint-disable */\n\n`
332
+ );
333
+
334
+ const outdir = path.resolve(opts.outdir);
335
+ if (!fs.existsSync(outdir)) {
336
+ fse.mkdirpSync(outdir);
337
+ }
338
+
339
+ const isGeneratePackageMode = opts.packageName != null;
340
+
341
+ const distOutputPath = isGeneratePackageMode
342
+ ? path.join(outdir, "dist")
343
+ : outdir;
344
+
345
+ fse.copySync(buildOutputPath, distOutputPath);
346
+ fse.removeSync(tmpDirPath);
347
+
348
+ if (isGeneratePackageMode) {
349
+ const packageJSON = {
350
+ name: opts.packageName,
351
+ version: "1.0.0",
352
+ author: opts.packageAuthor ?? "",
353
+ license: "MIT",
354
+ description: opts.packageDescription ?? "",
355
+ main: "./dist/index.js",
356
+ types: "./dist/index.d.ts",
357
+ };
358
+ const gitignoreSource = `/node_modules
359
+ /.pnp
360
+ .pnp.js
361
+
362
+ npm-debug.log*
363
+ yarn-debug.log*
364
+ yarn-error.log*
365
+
366
+ .vscode
367
+ `;
368
+ fs.writeFileSync(
369
+ path.join(outdir, "package.json"),
370
+ JSON.stringify(packageJSON, null, 2)
371
+ );
372
+ fs.writeFileSync(path.join(outdir, ".gitignore"), gitignoreSource);
373
+ }
374
+ }
375
+
376
+ function getSDKCode(
377
+ methods: string,
378
+ throwOnNotOkayRes: boolean,
379
+ cacheType: "memory" | "none"
380
+ ) {
381
+ const useMemoryCache = cacheType === "memory";
382
+ return `import "cross-fetch/polyfill";
383
+ import * as qs from "qs";
384
+ ${useMemoryCache ? `const NodeCache = require("node-cache");` : ""}
385
+
386
+ type Headers = { [k: string]: string };
387
+
388
+ ${
389
+ throwOnNotOkayRes
390
+ ? `class CustomError extends Error {
391
+ constructor(message) {
392
+ super(message);
393
+ this.name = this.constructor.name;
394
+ }
395
+ };
396
+
397
+ export class SDKResponseError extends CustomError {
398
+ error: any;
399
+ status: number;
400
+
401
+ constructor(error, status) {
402
+ super("Bad response");
403
+ this.error = error;
404
+ this.status = status;
405
+ }
406
+ }
407
+ `
408
+ : ""
409
+ }
410
+
411
+ type QueryStringOpts = Parameters<typeof qs.stringify>[1]
412
+
413
+ export class SDK {
414
+ _baseUrl?: string;
415
+ _headers?: Headers;
416
+ _queryStringOpts?: QueryStringOpts;
417
+ ${useMemoryCache ? `_cache: InstanceType<typeof NodeCache>;` : ""}
418
+
419
+ constructor(opts: { baseUrl: string; headers?: Headers, queryStringOpts?: QueryStringOpts }) {
420
+ this.__setBaseUrl(opts.baseUrl);
421
+ this._queryStringOpts = opts.queryStringOpts;
422
+ if (opts?.headers) {
423
+ this._headers = opts.headers ?? {};
424
+ }
425
+ ${
426
+ useMemoryCache
427
+ ? `this._cache = new NodeCache({
428
+ // 10 min
429
+ stdTTL: 600,
430
+ // 5 min
431
+ checkperiod: 300,
432
+ });`
433
+ : ""
434
+ }
435
+ }
436
+
437
+ __setBaseUrl(baseUrl: string) {
438
+ this._baseUrl = baseUrl;
439
+ }
440
+
441
+ __setHeaders(fn: any) {
442
+ this._headers = fn(this._headers);
443
+ }
444
+
445
+ __clearCache() {
446
+ ${useMemoryCache ? `this._cache.flushAll();` : ""}
447
+ }
448
+
449
+ ${methods}
450
+
451
+ async _get(path, params, query, opts) {
452
+ const { hydratedPath } = findParamArguments(path, params);
453
+ const url = \`\${this._baseUrl}\${hydratedPath}\${generateQueryString(query, this._queryStringOpts)}\`;
454
+ const cacheKey = \`GET\${url}\`;
455
+
456
+ ${
457
+ useMemoryCache
458
+ ? `if (opts?.strategy !== "network") {
459
+ const cached = this._cache.get(cacheKey);
460
+ if (cached !== undefined) {
461
+ return cached;
462
+ }
463
+ }`
464
+ : ""
465
+ }
466
+
467
+ let headers = { "Content-Type": "application/json" };
468
+ if (this._headers) {
469
+ headers = { ...headers, ...this._headers };
470
+ }
471
+ if (opts?.headers) {
472
+ headers = { ...headers, ...opts.headers };
473
+ }
474
+
475
+ let res = await fetch(url, { headers });
476
+
477
+ const json = await res.json();
478
+
479
+ if (!res.ok) {
480
+ ${
481
+ throwOnNotOkayRes
482
+ ? `throw new SDKResponseError(json, res.status);`
483
+ : `return { error: json, status: res.status };`
484
+ }
485
+ }
486
+
487
+ const out = { data: json, status: res.status };
488
+ ${useMemoryCache ? `this._cache.set(cacheKey, out);` : ""}
489
+
490
+ return out;
491
+ }
492
+
493
+ async _post(path, params, data, query, opts, requestBodyContentType) {
494
+ const { hydratedPath } = findParamArguments(path, params);
495
+
496
+ let headers = !requestBodyContentType ? {} : { "Content-Type": requestBodyContentType };
497
+ if (this._headers) {
498
+ headers = { ...headers, ...this._headers };
499
+ }
500
+ if (opts?.headers) {
501
+ headers = { ...headers, ...opts.headers };
502
+ }
503
+
504
+ let res = await fetch(
505
+ \`\${this._baseUrl}\${hydratedPath}\${generateQueryString(query, this._queryStringOpts)}\`,
506
+ {
507
+ method: "POST",
508
+ headers,
509
+ body: requestBodyContentType === "application/json" ? JSON.stringify(data) : data,
510
+ }
511
+ );
512
+
513
+ if (res.status === 204) {
514
+ ${
515
+ useMemoryCache
516
+ ? `if (opts?.clearCache !== false) {
517
+ this._cache.flushAll();
518
+ }`
519
+ : ""
520
+ }
521
+ return { data: undefined, status: res.status };
522
+ }
523
+
524
+ const json = await res.json();
525
+
526
+ if (!res.ok) {
527
+ ${
528
+ throwOnNotOkayRes
529
+ ? `throw new SDKResponseError(json, res.status);`
530
+ : `return { error: json, status: res.status };`
531
+ }
532
+ }
533
+
534
+ ${
535
+ useMemoryCache
536
+ ? `if (opts?.clearCache !== false) {
537
+ this._cache.flushAll();
538
+ }`
539
+ : ""
540
+ }
541
+
542
+ return { data: json, status: res.status };
543
+ }
544
+
545
+ async _patch(path, params, data, query, opts, requestBodyContentType) {
546
+ const { hydratedPath } = findParamArguments(path, params);
547
+
548
+ let headers = !requestBodyContentType ? {} : { "Content-Type": requestBodyContentType };
549
+ if (this._headers) {
550
+ headers = { ...headers, ...this._headers };
551
+ }
552
+ if (opts?.headers) {
553
+ headers = { ...headers, ...opts.headers };
554
+ }
555
+
556
+ let res = await fetch(
557
+ \`\${this._baseUrl}\${hydratedPath}\${generateQueryString(query, this._queryStringOpts)}\`,
558
+ {
559
+ method: "PATCH",
560
+ headers,
561
+ body: requestBodyContentType === "application/json" ? JSON.stringify(data) : data,
562
+ }
563
+ );
564
+
565
+ if (res.status === 204) {
566
+ ${
567
+ useMemoryCache
568
+ ? `if (opts?.clearCache !== false) {
569
+ this._cache.flushAll();
570
+ }`
571
+ : ""
572
+ }
573
+ return { data: undefined, status: res.status };
574
+ }
575
+
576
+ const json = await res.json();
577
+
578
+ if (!res.ok) {
579
+ ${
580
+ throwOnNotOkayRes
581
+ ? `throw new SDKResponseError(json, res.status);`
582
+ : `return { error: json, status: res.status };`
583
+ }
584
+ }
585
+
586
+ ${
587
+ useMemoryCache
588
+ ? `if (opts?.clearCache !== false) {
589
+ this._cache.flushAll();
590
+ }`
591
+ : ""
592
+ }
593
+
594
+ return { data: json, status: res.status };
595
+ }
596
+
597
+ async _put(path, params, data, query, opts, requestBodyContentType) {
598
+ const { hydratedPath } = findParamArguments(path, params);
599
+
600
+ let headers = !requestBodyContentType ? {} : { "Content-Type": requestBodyContentType };
601
+ if (this._headers) {
602
+ headers = { ...headers, ...this._headers };
603
+ }
604
+ if (opts?.headers) {
605
+ headers = { ...headers, ...opts.headers };
606
+ }
607
+
608
+ let res = await fetch(
609
+ \`\${this._baseUrl}\${hydratedPath}\${generateQueryString(query, this._queryStringOpts)}\`,
610
+ {
611
+ method: "PUT",
612
+ headers,
613
+ body: requestBodyContentType === "application/json" ? JSON.stringify(data) : data,
614
+ }
615
+ );
616
+
617
+ if (res.status === 204) {
618
+ ${
619
+ useMemoryCache
620
+ ? `if (opts?.clearCache !== false) {
621
+ this._cache.flushAll();
622
+ }`
623
+ : ""
624
+ }
625
+ return { data: undefined, status: res.status };
626
+ }
627
+
628
+ const json = await res.json();
629
+
630
+ if (!res.ok) {
631
+ ${
632
+ throwOnNotOkayRes
633
+ ? `throw new SDKResponseError(json, res.status);`
634
+ : `return { error: json, status: res.status };`
635
+ }
636
+ }
637
+
638
+ ${
639
+ useMemoryCache
640
+ ? `if (opts?.clearCache !== false) {
641
+ this._cache.flushAll();
642
+ }`
643
+ : ""
644
+ }
645
+
646
+ return { data: json, status: res.status };
647
+ }
648
+
649
+ async _delete(path, params, query, opts) {
650
+ const { hydratedPath } = findParamArguments(path, params);
651
+
652
+ let headers = { "Content-Type": "application/json" };
653
+ if (this._headers) {
654
+ headers = { ...headers, ...this._headers };
655
+ }
656
+ if (opts?.headers) {
657
+ headers = { ...headers, ...opts.headers };
658
+ }
659
+
660
+ let res = await fetch(
661
+ \`\${this._baseUrl}\${hydratedPath}\${generateQueryString(query, this._queryStringOpts)}\`,
662
+ { method: "DELETE", headers }
663
+ );
664
+
665
+ if (res.status === 204) {
666
+ ${
667
+ useMemoryCache
668
+ ? `if (opts?.clearCache !== false) {
669
+ this._cache.flushAll();
670
+ }`
671
+ : ""
672
+ }
673
+ return { data: undefined, status: res.status };
674
+ }
675
+
676
+ const json = await res.json();
677
+
678
+ if (!res.ok) {
679
+ ${
680
+ throwOnNotOkayRes
681
+ ? `throw new SDKResponseError(json, res.status);`
682
+ : `return { error: json, status: res.status };`
683
+ }
684
+ }
685
+
686
+ ${
687
+ useMemoryCache
688
+ ? `if (opts?.clearCache !== false) {
689
+ this._cache.flushAll();
690
+ }`
691
+ : ""
692
+ }
693
+
694
+ return { data: json, status: res.status };
695
+ }
696
+ }
697
+
698
+ function findParamArguments(path, obj) {
699
+ let parts = path.split(/\\//);
700
+ let params = parts.filter((p) => p.length > 0 && p[0] === ":");
701
+ let args = params.map((p) => p.substring(1, p.length));
702
+ let out = {};
703
+ let hydratedPath = path;
704
+ if (obj) {
705
+ args.forEach((e) => {
706
+ out[e] = obj[e];
707
+ hydratedPath = hydratedPath.replace(\`:\${e}\`, obj[e]);
708
+ });
709
+ }
710
+
711
+ return { args, out, hydratedPath };
712
+ }
713
+
714
+ function generateQueryString(obj, _opts) {
715
+ let opts = { addQueryPrefix: true, encode: true };
716
+ if (_opts) {
717
+ opts = {...opts, ..._opts};
718
+ }
719
+ return qs.stringify(obj, opts);
720
+ }
721
+ `;
722
+ }
723
+
724
+ function getRequestBodyContentType(operationDef: object, method: string) {
725
+ const content = operationDef?.[method]?.requestBody?.content;
726
+ if (content == null) {
727
+ throw new Error("No content: " + getOperationId(operationDef));
728
+ }
729
+ const keys = Object.keys(content);
730
+ if (keys.length === 0) {
731
+ throw new Error("No content type: " + getOperationId(operationDef));
732
+ }
733
+ if (keys.length > 1) {
734
+ throw new Error(
735
+ "More than 1 content type. Not allowed: " + getOperationId(operationDef)
736
+ );
737
+ }
738
+ return keys[0];
739
+ }
740
+
741
+ async function createRequestBodyType(operationDef: object, method: string) {
742
+ let typeName = capitalize(getOperationId(operationDef[method])) + "Body";
743
+
744
+ if (operationDef?.[method]?.requestBody == null) {
745
+ return { type: "undefined", name: "" };
746
+ }
747
+
748
+ if (
749
+ operationDef?.[method]?.requestBody?.content?.["application/json"] == null
750
+ ) {
751
+ return { type: "any", name: "" };
752
+ }
753
+
754
+ if (
755
+ operationDef?.[method]?.requestBody?.content?.["application/json"]
756
+ ?.schema == null ||
757
+ operationDef[method].requestBody.content["application/json"].schema
758
+ .length === 0
759
+ ) {
760
+ return { type: "object", name: "" };
761
+ }
762
+
763
+ const schema = toJsonSchema(
764
+ operationDef[method].requestBody.content["application/json"].schema
765
+ );
766
+ // Delete title, since if there's a title, json-schema-to-typescript
767
+ // generates references.
768
+ traverse(schema, {
769
+ cb: (schema) => {
770
+ if (schema.title) {
771
+ delete schema.title;
772
+ }
773
+ },
774
+ });
775
+
776
+ return {
777
+ type: (
778
+ await compile(schema, "IRootObject", {
779
+ bannerComment: "",
780
+ declareExternallyReferenced: false,
781
+ })
782
+ )
783
+ .replace(/export interface IRootObject/g, "")
784
+ .replace(/export type IRootObject =/g, "")
785
+ .replace(/};/g, "}")
786
+ .replace(
787
+ `/**\n * The request body is an empty object.\n */\n{}\n`,
788
+ "object"
789
+ ),
790
+ name: typeName,
791
+ };
792
+ }
793
+
794
+ function createParamType(path: string): { name: string; type: string } {
795
+ // TODO - use `parameters` instead
796
+ let params = findParamArguments(path);
797
+
798
+ if (params.args.length === 0) {
799
+ return { type: "null", name: "" };
800
+ }
801
+
802
+ return {
803
+ type: json2ts(JSON.stringify(params.out)).replace(
804
+ "interface IRootObject ",
805
+ ""
806
+ ),
807
+ name: "",
808
+ };
809
+ }
810
+
811
+ async function createQueryType(parameters) {
812
+ if (parameters == null) {
813
+ return "object";
814
+ }
815
+
816
+ const queryParameters = parameters.filter((x) => x.in === "query");
817
+
818
+ if (queryParameters.length === 0) {
819
+ return "object";
820
+ }
821
+
822
+ const schema = queryParameters.reduce(
823
+ (acc: { properties: { [k: string]: any }; required: string[] }, v) => {
824
+ acc.properties[v.name] = v.schema;
825
+ if (v.required) {
826
+ acc.required.push(v.name);
827
+ }
828
+ return acc;
829
+ },
830
+ { properties: {}, required: [] }
831
+ );
832
+
833
+ // Delete title, since if there's a title, json-schema-to-typescript
834
+ // generates references.
835
+ traverse(schema, {
836
+ cb: (schema) => {
837
+ if (schema.title) {
838
+ delete schema.title;
839
+ }
840
+ },
841
+ });
842
+
843
+ return (
844
+ await compile(schema, "IRootObject", {
845
+ bannerComment: "",
846
+ declareExternallyReferenced: false,
847
+ })
848
+ )
849
+ .replace("export interface IRootObject ", "")
850
+ .replace(
851
+ `/**\n * The request body is an empty object.\n */\n{}\n`,
852
+ "object"
853
+ )
854
+ .replace("[k: string]: any;", "");
855
+ }
856
+
857
+ async function createResponseBodyType(
858
+ operationDef: object,
859
+ method: string,
860
+ throwOnNotOkayRes: boolean
861
+ ) {
862
+ let responses = operationDef[method]?.responses ?? {};
863
+ let out = "";
864
+
865
+ for (let [status, response] of Object.entries(responses)) {
866
+ const _schema = (response as any)?.content?.["application/json"]?.schema;
867
+ const schema =
868
+ _schema == null ? _schema : toJsonSchema(_schema, { strictMode: false });
869
+ // Delete title, since if there's a title, json-schema-to-typescript
870
+ // generates references.
871
+ if (schema != null) {
872
+ traverse(schema, {
873
+ cb: (schema) => {
874
+ if (schema.title) {
875
+ delete schema.title;
876
+ }
877
+ },
878
+ });
879
+ }
880
+
881
+ let type =
882
+ schema == null
883
+ ? schema
884
+ : (
885
+ await compile(schema, "IRootObject", {
886
+ bannerComment: "",
887
+ declareExternallyReferenced: false,
888
+ })
889
+ )
890
+ .replace(/export interface IRootObject /g, "")
891
+ .replace("export type IRootObject =", "")
892
+ .trim();
893
+ if (type?.endsWith(";")) {
894
+ type = type.slice(0, -1);
895
+ }
896
+
897
+ const notOk = parseInt(status) >= 400;
898
+
899
+ if (throwOnNotOkayRes) {
900
+ if (notOk) {
901
+ continue;
902
+ } else {
903
+ out += `| {data: ${type}, status: number}`;
904
+ }
905
+ } else {
906
+ if (notOk) {
907
+ out += `| {error: ${type}, status: number}`;
908
+ } else {
909
+ out += `| {data: ${type}, error?: undefined, status: number}`;
910
+ }
911
+ }
912
+ }
913
+
914
+ if (!throwOnNotOkayRes) {
915
+ // TODO?
916
+ // if (!Object.keys(responses).some(x => parseInt(x) >= 400)) {
917
+ out += "| {error: {[k: string]: any}, status: number}";
918
+ // }
919
+ }
920
+
921
+ return out;
922
+ }
923
+
924
+ function getOperationId(x: any) {
925
+ return x?.["x-technicity-sdk-name"] ?? x?.["operationId"];
926
+ }
927
+
928
+ function findParamArguments(path: string) {
929
+ let parts = path.split(/\//);
930
+ let params = parts
931
+ .map((p) => (p.startsWith("{") ? `:${p.slice(1, p.length - 1)}` : p))
932
+ .filter((p) => p.length > 0 && p[0] === ":");
933
+ let args = params.map((p) => p.substring(1, p.length));
934
+ let out = {};
935
+ let hydratedPath = path;
936
+ args.forEach((e) => {
937
+ out[e] = e;
938
+ hydratedPath = hydratedPath.replace(`:${e}`, e);
939
+ });
940
+
941
+ return { args, out, hydratedPath };
942
+ }
943
+
944
+ function convertPath(p: string) {
945
+ return p
946
+ .split(/\//)
947
+ .map((p) => (p.startsWith("{") ? `:${p.slice(1, p.length - 1)}` : p))
948
+ .join("/");
949
+ }
950
+
951
+ function capitalize(s: string) {
952
+ if (typeof s !== "string") return "";
953
+ return s.charAt(0).toUpperCase() + s.slice(1);
954
+ }