@ps-aux/api-client-gen 0.0.3 → 0.0.4

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,22 @@
1
+ #!/bin/env node
2
+ import { generateApiClient } from './index.esm.js';
3
+ import { cosmiconfigSync } from 'cosmiconfig';
4
+ import 'path';
5
+ import 'fs';
6
+ import 'axios';
7
+ import 'fs/promises';
8
+ import 'swagger-typescript-api';
9
+ import 'ts-to-zod';
10
+
11
+ const profile = process.argv[2];
12
+ if (!profile)
13
+ throw new Error(`No profile specified`);
14
+ const conf = cosmiconfigSync('api-client-gen')
15
+ .search();
16
+ // TODO validate config
17
+ if (!conf)
18
+ throw new Error(`No config provided`);
19
+ const profileConf = conf.config[profile];
20
+ if (!profile)
21
+ throw new Error(`Profile ${profile} not present in the configuration`);
22
+ generateApiClient(profileConf).catch(console.error);
package/dist/bin.js ADDED
@@ -0,0 +1,24 @@
1
+ #!/bin/env node
2
+ 'use strict';
3
+
4
+ var index = require('./index.js');
5
+ var cosmiconfig = require('cosmiconfig');
6
+ require('path');
7
+ require('fs');
8
+ require('axios');
9
+ require('fs/promises');
10
+ require('swagger-typescript-api');
11
+ require('ts-to-zod');
12
+
13
+ const profile = process.argv[2];
14
+ if (!profile)
15
+ throw new Error(`No profile specified`);
16
+ const conf = cosmiconfig.cosmiconfigSync('api-client-gen')
17
+ .search();
18
+ // TODO validate config
19
+ if (!conf)
20
+ throw new Error(`No config provided`);
21
+ const profileConf = conf.config[profile];
22
+ if (!profile)
23
+ throw new Error(`Profile ${profile} not present in the configuration`);
24
+ index.generateApiClient(profileConf).catch(console.error);
@@ -1 +1 @@
1
- export declare const downloadSpec: (path: string, url: string) => Promise<unknown>;
1
+ export declare const downloadSpec: (path: string, url: string) => Promise<void>;
@@ -0,0 +1,12 @@
1
+ export type Params = {
2
+ srcSpec: string;
3
+ dstDir: string;
4
+ apiName: string;
5
+ env: 'browser' | 'node';
6
+ responseWrapper?: {
7
+ symbol: string;
8
+ import?: string;
9
+ };
10
+ zodSchemas?: boolean;
11
+ };
12
+ export declare const generateApiClient: ({ dstDir, apiName, env, srcSpec, responseWrapper, zodSchemas, }: Params, log?: (msg: string) => void) => Promise<void>;
@@ -1 +1,11 @@
1
- export declare const generateOpenApiModel: (name: string, input: string, outputDir: string, log: (msg: string) => void, isNode?: boolean) => Promise<void>;
1
+ export type GenerateOpenApiModelCmd = {
2
+ name: string;
3
+ input: string;
4
+ outputDir: string;
5
+ isNode?: boolean;
6
+ responseWrapper?: {
7
+ symbol: string;
8
+ import?: string;
9
+ };
10
+ };
11
+ export declare const generateOpenApiModel: ({ input, isNode, responseWrapper, name, outputDir }: GenerateOpenApiModelCmd, log: (msg: string) => void) => Promise<string>;
@@ -0,0 +1 @@
1
+ export declare const generateSchemas: (inputFile: string, outputFile: string) => void;
package/dist/index.d.ts CHANGED
@@ -1,5 +1 @@
1
- export { generateModel } from "./gen-model";
2
- declare const _default: {
3
- generateModel: ({ dstDir, apiName, env, srcSpec, }: import("./gen-model").Params, log?: (msg: string) => void) => Promise<void>;
4
- };
5
- export default _default;
1
+ export { generateApiClient } from "./generateApiClient";
package/dist/index.esm.js CHANGED
@@ -1,70 +1,34 @@
1
1
  import Path from 'path';
2
- import fs from 'fs';
2
+ import fs$1 from 'fs';
3
3
  import axios from 'axios';
4
+ import fs from 'fs/promises';
4
5
  import { generateApi } from 'swagger-typescript-api';
5
- import fs$1 from 'fs/promises';
6
+ import { generate } from 'ts-to-zod';
6
7
 
7
- /******************************************************************************
8
- Copyright (c) Microsoft Corporation.
9
-
10
- Permission to use, copy, modify, and/or distribute this software for any
11
- purpose with or without fee is hereby granted.
12
-
13
- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
14
- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
15
- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
16
- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
17
- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
18
- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
19
- PERFORMANCE OF THIS SOFTWARE.
20
- ***************************************************************************** */
21
- /* global Reflect, Promise, SuppressedError, Symbol */
22
-
23
-
24
- function __awaiter(thisArg, _arguments, P, generator) {
25
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
26
- return new (P || (P = Promise))(function (resolve, reject) {
27
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
28
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
29
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
30
- step((generator = generator.apply(thisArg, _arguments || [])).next());
31
- });
32
- }
33
-
34
- typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
35
- var e = new Error(message);
36
- return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
37
- };
38
-
39
- const downloadSpec = (path, url) => __awaiter(void 0, void 0, void 0, function* () {
40
- const res = yield axios({
8
+ const downloadSpec = async (path, url) => {
9
+ const res = await axios({
41
10
  url,
42
11
  method: 'GET',
43
- responseType: 'stream'
12
+ responseType: 'arraybuffer',
44
13
  }).catch(err => {
45
14
  throw err.toString();
46
15
  });
47
- const writer = fs.createWriteStream(path);
48
- // TODO Format to readable JSON
49
- yield res.data.pipe(writer);
50
- return new Promise((resolve, reject) => {
51
- writer.on('finish', resolve);
52
- writer.on('error', err => {
53
- reject(err);
54
- });
55
- });
56
- });
16
+ const content = res.data.toString();
17
+ await fs.writeFile(path, format(content));
18
+ };
19
+ const format = (content) => JSON.stringify(JSON.parse(content), null, 4);
57
20
 
58
- const generateOpenApiModel = (name, input, outputDir, log, isNode = false) => __awaiter(void 0, void 0, void 0, function* () {
21
+ const generateOpenApiModel = async ({ input, isNode, responseWrapper, name, outputDir }, log) => {
59
22
  log(`Will generate API client name=${name} to ${outputDir}, node=${isNode}`);
60
23
  const specialMapping = isNode
61
24
  ? {
62
25
  File: '{file: Buffer | stream.Readable, name: string}',
63
26
  }
64
27
  : {};
65
- const dstFile = Path.join(outputDir, name + '.ts');
66
- yield generateApi({
67
- name,
28
+ const fileName = 'index';
29
+ const dstFile = Path.join(outputDir, `${fileName}.ts`);
30
+ await generateApi({
31
+ name: 'index',
68
32
  output: outputDir,
69
33
  input: input,
70
34
  // modular: true,
@@ -76,6 +40,7 @@ const generateOpenApiModel = (name, input, outputDir, log, isNode = false) => __
76
40
  // extractRequestBody: true,
77
41
  cleanOutput: true,
78
42
  moduleNameFirstTag: true,
43
+ apiClassName: capitalize(name) + 'Api',
79
44
  // @ts-ignore
80
45
  codeGenConstructs: (struct) => ({
81
46
  // @ts-ignore
@@ -83,19 +48,67 @@ const generateOpenApiModel = (name, input, outputDir, log, isNode = false) => __
83
48
  }),
84
49
  // extractRequestParams: true,
85
50
  });
51
+ const addImports = [];
52
+ const replaces = [];
86
53
  if (isNode) {
87
- let content = yield fs$1.readFile(dstFile).then((r) => r.toString());
88
- // Naive solution
89
- content = 'import stream from \'node:stream\'\n' + content;
90
- yield fs$1.writeFile(dstFile, content);
54
+ addImports.push('import stream from \'node:stream\'');
91
55
  }
92
- });
56
+ if (responseWrapper) {
57
+ log(`Will use response wrapper '${JSON.stringify(responseWrapper)}'`);
58
+ if (responseWrapper.import)
59
+ addImports.push(responseWrapper.import);
60
+ replaces.push(['Promise', responseWrapper.symbol]);
61
+ }
62
+ // Remove unnecessary generics
63
+ replaces.push(['<SecurityDataType extends unknown>', '']);
64
+ replaces.push(['<SecurityDataType>', '']);
65
+ log(`Will modify the outputs ${JSON.stringify({
66
+ addImports,
67
+ replaces,
68
+ })}`);
69
+ await modifyOutput(dstFile, {
70
+ addImports,
71
+ replaces,
72
+ });
73
+ return dstFile;
74
+ };
75
+ const modifyOutput = async (path, cmd) => {
76
+ let content = await fs.readFile(path).then((r) => r.toString());
77
+ if (cmd.addImports.length) {
78
+ content = cmd.addImports.join('\n') + '\n' + content;
79
+ }
80
+ cmd.replaces.forEach(([from, to]) => {
81
+ content = content.replaceAll(from, to);
82
+ });
83
+ await fs.writeFile(path, content);
84
+ };
93
85
  const getThisScriptDirname = () => {
94
86
  // Might be problem in ESM mode
95
87
  return __dirname;
96
88
  };
89
+ const capitalize = (str) => {
90
+ return str[0].toUpperCase() + str.substring(1);
91
+ };
97
92
 
98
- const generateModel = ({ dstDir, apiName, env, srcSpec, }, log = console.log) => __awaiter(void 0, void 0, void 0, function* () {
93
+ const removeGenericTypes = (code) => {
94
+ const regex = /export (interface|enum|type) [^<]* \{\n(.*\n)*?}/mg;
95
+ const matches = code.matchAll(regex);
96
+ const types = Array.from(matches).map(m => m[0]);
97
+ return types.join('\n');
98
+ };
99
+ const generateSchemas = (inputFile, outputFile) => {
100
+ const code = fs$1.readFileSync(inputFile).toString();
101
+ const { getZodSchemasFile } = generate({
102
+ sourceText: removeGenericTypes(code),
103
+ });
104
+ const fileName = Path.basename(inputFile);
105
+ let f = getZodSchemasFile(`./${fileName.replace('.ts', '')}`);
106
+ // Backend sends nulls not undefined - should be properly set in the OpenAPI TS types generation!
107
+ f = f.replaceAll('.optional()', '.nullable()');
108
+ fs$1.writeFileSync(outputFile, f);
109
+ };
110
+
111
+ const generateApiClient = async ({ dstDir, apiName, env, srcSpec, responseWrapper, zodSchemas, }, log = console.log) => {
99
112
  if (!srcSpec)
100
113
  throw new Error(`Url or path ('srcSpec' not specified`);
101
114
  if (!apiName)
@@ -105,34 +118,36 @@ const generateModel = ({ dstDir, apiName, env, srcSpec, }, log = console.log) =>
105
118
  if (!env)
106
119
  throw new Error('env not specified');
107
120
  const dir = Path.resolve(dstDir, apiName);
108
- if (!fs.existsSync(dir)) {
121
+ if (!fs$1.existsSync(dir)) {
109
122
  log(`Creating dir ${dir}`);
110
- fs.mkdirSync(dir, {
123
+ fs$1.mkdirSync(dir, {
111
124
  recursive: true,
112
125
  });
113
126
  }
114
127
  const clientDir = Path.resolve(dir, 'client');
115
- const specPath = yield getSpecPath(srcSpec, dir, log);
116
- yield generateOpenApiModel(apiName + '-api', specPath, clientDir, log, env === 'node');
117
- // TODO does not seem to currently generate all schemas
118
- // generateSchemas(
119
- // Path.resolve(modelDir, 'data-contracts.ts'),
120
- // Path.resolve(dir, 'schemas.ts')
121
- // )
122
- });
123
- const getSpecPath = (urlOrPath, dir, log) => __awaiter(void 0, void 0, void 0, function* () {
128
+ const specPath = await getSpecPath(srcSpec, dir, log);
129
+ const clientFile = await generateOpenApiModel({
130
+ name: apiName,
131
+ input: specPath,
132
+ outputDir: clientDir,
133
+ isNode: env === 'node',
134
+ responseWrapper,
135
+ }, log);
136
+ if (zodSchemas === false)
137
+ return;
138
+ log('Generating Zod schemas');
139
+ generateSchemas(Path.resolve(dir, clientFile), Path.resolve(dir, './zod.ts'));
140
+ };
141
+ const getSpecPath = async (urlOrPath, dir, log) => {
124
142
  if (!urlOrPath.startsWith('http')) {
125
- if (!fs.existsSync(urlOrPath))
143
+ if (!fs$1.existsSync(urlOrPath))
126
144
  throw new Error(`Spec file ${urlOrPath} does not exists`);
127
145
  return urlOrPath;
128
146
  }
129
147
  const specPath = Path.resolve(dir, `spec.json`);
130
148
  log(`Will download the API spec from ${urlOrPath} to ${specPath}`);
131
- yield downloadSpec(specPath, urlOrPath);
149
+ await downloadSpec(specPath, urlOrPath);
132
150
  return specPath;
133
- });
134
-
135
- // Bcs of mixing for ESM and CommonJS - todo fix somehow
136
- var index = { generateModel };
151
+ };
137
152
 
138
- export { index as default, generateModel };
153
+ export { generateApiClient };
package/dist/index.js CHANGED
@@ -1,143 +1,155 @@
1
- (function (global, factory) {
2
- typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('path'), require('fs'), require('axios'), require('swagger-typescript-api'), require('fs/promises')) :
3
- typeof define === 'function' && define.amd ? define(['exports', 'path', 'fs', 'axios', 'swagger-typescript-api', 'fs/promises'], factory) :
4
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.MyLibrary = {}, global.Path, global.fs, global.axios, global.swaggerTypescriptApi, global.fs$1));
5
- })(this, (function (exports, Path, fs, axios, swaggerTypescriptApi, fs$1) { 'use strict';
1
+ 'use strict';
6
2
 
7
- /******************************************************************************
8
- Copyright (c) Microsoft Corporation.
9
-
10
- Permission to use, copy, modify, and/or distribute this software for any
11
- purpose with or without fee is hereby granted.
12
-
13
- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
14
- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
15
- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
16
- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
17
- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
18
- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
19
- PERFORMANCE OF THIS SOFTWARE.
20
- ***************************************************************************** */
21
- /* global Reflect, Promise, SuppressedError, Symbol */
22
-
23
-
24
- function __awaiter(thisArg, _arguments, P, generator) {
25
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
26
- return new (P || (P = Promise))(function (resolve, reject) {
27
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
28
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
29
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
30
- step((generator = generator.apply(thisArg, _arguments || [])).next());
31
- });
32
- }
33
-
34
- typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
35
- var e = new Error(message);
36
- return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
37
- };
3
+ var Path = require('path');
4
+ var fs$1 = require('fs');
5
+ var axios = require('axios');
6
+ var fs = require('fs/promises');
7
+ var swaggerTypescriptApi = require('swagger-typescript-api');
8
+ var tsToZod = require('ts-to-zod');
38
9
 
39
- const downloadSpec = (path, url) => __awaiter(void 0, void 0, void 0, function* () {
40
- const res = yield axios({
41
- url,
42
- method: 'GET',
43
- responseType: 'stream'
44
- }).catch(err => {
45
- throw err.toString();
46
- });
47
- const writer = fs.createWriteStream(path);
48
- // TODO Format to readable JSON
49
- yield res.data.pipe(writer);
50
- return new Promise((resolve, reject) => {
51
- writer.on('finish', resolve);
52
- writer.on('error', err => {
53
- reject(err);
54
- });
55
- });
10
+ const downloadSpec = async (path, url) => {
11
+ const res = await axios({
12
+ url,
13
+ method: 'GET',
14
+ responseType: 'arraybuffer',
15
+ }).catch(err => {
16
+ throw err.toString();
56
17
  });
18
+ const content = res.data.toString();
19
+ await fs.writeFile(path, format(content));
20
+ };
21
+ const format = (content) => JSON.stringify(JSON.parse(content), null, 4);
57
22
 
58
- const generateOpenApiModel = (name, input, outputDir, log, isNode = false) => __awaiter(void 0, void 0, void 0, function* () {
59
- log(`Will generate API client name=${name} to ${outputDir}, node=${isNode}`);
60
- const specialMapping = isNode
61
- ? {
62
- File: '{file: Buffer | stream.Readable, name: string}',
63
- }
64
- : {};
65
- const dstFile = Path.join(outputDir, name + '.ts');
66
- yield swaggerTypescriptApi.generateApi({
67
- name,
68
- output: outputDir,
69
- input: input,
70
- // modular: true,
71
- httpClientType: 'axios',
72
- templates: Path.resolve(getThisScriptDirname(), '../templates'),
73
- defaultResponseAsSuccess: true,
74
- // generateRouteTypes: true,
75
- singleHttpClient: true,
76
- // extractRequestBody: true,
77
- cleanOutput: true,
78
- moduleNameFirstTag: true,
79
- // @ts-ignore
80
- codeGenConstructs: (struct) => ({
81
- // @ts-ignore
82
- Keyword: specialMapping,
83
- }),
84
- // extractRequestParams: true,
85
- });
86
- if (isNode) {
87
- let content = yield fs$1.readFile(dstFile).then((r) => r.toString());
88
- // Naive solution
89
- content = 'import stream from \'node:stream\'\n' + content;
90
- yield fs$1.writeFile(dstFile, content);
23
+ const generateOpenApiModel = async ({ input, isNode, responseWrapper, name, outputDir }, log) => {
24
+ log(`Will generate API client name=${name} to ${outputDir}, node=${isNode}`);
25
+ const specialMapping = isNode
26
+ ? {
27
+ File: '{file: Buffer | stream.Readable, name: string}',
91
28
  }
29
+ : {};
30
+ const fileName = 'index';
31
+ const dstFile = Path.join(outputDir, `${fileName}.ts`);
32
+ await swaggerTypescriptApi.generateApi({
33
+ name: 'index',
34
+ output: outputDir,
35
+ input: input,
36
+ // modular: true,
37
+ httpClientType: 'axios',
38
+ templates: Path.resolve(getThisScriptDirname(), '../templates'),
39
+ defaultResponseAsSuccess: true,
40
+ // generateRouteTypes: true,
41
+ singleHttpClient: true,
42
+ // extractRequestBody: true,
43
+ cleanOutput: true,
44
+ moduleNameFirstTag: true,
45
+ apiClassName: capitalize(name) + 'Api',
46
+ // @ts-ignore
47
+ codeGenConstructs: (struct) => ({
48
+ // @ts-ignore
49
+ Keyword: specialMapping,
50
+ }),
51
+ // extractRequestParams: true,
92
52
  });
93
- const getThisScriptDirname = () => {
94
- // Might be problem in ESM mode
95
- return __dirname;
96
- };
97
-
98
- const generateModel = ({ dstDir, apiName, env, srcSpec, }, log = console.log) => __awaiter(void 0, void 0, void 0, function* () {
99
- if (!srcSpec)
100
- throw new Error(`Url or path ('srcSpec' not specified`);
101
- if (!apiName)
102
- throw new Error('apiName not specified');
103
- if (!dstDir)
104
- throw new Error('dstDir not specified');
105
- if (!env)
106
- throw new Error('env not specified');
107
- const dir = Path.resolve(dstDir, apiName);
108
- if (!fs.existsSync(dir)) {
109
- log(`Creating dir ${dir}`);
110
- fs.mkdirSync(dir, {
111
- recursive: true,
112
- });
113
- }
114
- const clientDir = Path.resolve(dir, 'client');
115
- const specPath = yield getSpecPath(srcSpec, dir, log);
116
- yield generateOpenApiModel(apiName + '-api', specPath, clientDir, log, env === 'node');
117
- // TODO does not seem to currently generate all schemas
118
- // generateSchemas(
119
- // Path.resolve(modelDir, 'data-contracts.ts'),
120
- // Path.resolve(dir, 'schemas.ts')
121
- // )
53
+ const addImports = [];
54
+ const replaces = [];
55
+ if (isNode) {
56
+ addImports.push('import stream from \'node:stream\'');
57
+ }
58
+ if (responseWrapper) {
59
+ log(`Will use response wrapper '${JSON.stringify(responseWrapper)}'`);
60
+ if (responseWrapper.import)
61
+ addImports.push(responseWrapper.import);
62
+ replaces.push(['Promise', responseWrapper.symbol]);
63
+ }
64
+ // Remove unnecessary generics
65
+ replaces.push(['<SecurityDataType extends unknown>', '']);
66
+ replaces.push(['<SecurityDataType>', '']);
67
+ log(`Will modify the outputs ${JSON.stringify({
68
+ addImports,
69
+ replaces,
70
+ })}`);
71
+ await modifyOutput(dstFile, {
72
+ addImports,
73
+ replaces,
122
74
  });
123
- const getSpecPath = (urlOrPath, dir, log) => __awaiter(void 0, void 0, void 0, function* () {
124
- if (!urlOrPath.startsWith('http')) {
125
- if (!fs.existsSync(urlOrPath))
126
- throw new Error(`Spec file ${urlOrPath} does not exists`);
127
- return urlOrPath;
128
- }
129
- const specPath = Path.resolve(dir, `spec.json`);
130
- log(`Will download the API spec from ${urlOrPath} to ${specPath}`);
131
- yield downloadSpec(specPath, urlOrPath);
132
- return specPath;
75
+ return dstFile;
76
+ };
77
+ const modifyOutput = async (path, cmd) => {
78
+ let content = await fs.readFile(path).then((r) => r.toString());
79
+ if (cmd.addImports.length) {
80
+ content = cmd.addImports.join('\n') + '\n' + content;
81
+ }
82
+ cmd.replaces.forEach(([from, to]) => {
83
+ content = content.replaceAll(from, to);
133
84
  });
85
+ await fs.writeFile(path, content);
86
+ };
87
+ const getThisScriptDirname = () => {
88
+ // Might be problem in ESM mode
89
+ return __dirname;
90
+ };
91
+ const capitalize = (str) => {
92
+ return str[0].toUpperCase() + str.substring(1);
93
+ };
134
94
 
135
- // Bcs of mixing for ESM and CommonJS - todo fix somehow
136
- var index = { generateModel };
137
-
138
- exports.default = index;
139
- exports.generateModel = generateModel;
95
+ const removeGenericTypes = (code) => {
96
+ const regex = /export (interface|enum|type) [^<]* \{\n(.*\n)*?}/mg;
97
+ const matches = code.matchAll(regex);
98
+ const types = Array.from(matches).map(m => m[0]);
99
+ return types.join('\n');
100
+ };
101
+ const generateSchemas = (inputFile, outputFile) => {
102
+ const code = fs$1.readFileSync(inputFile).toString();
103
+ const { getZodSchemasFile } = tsToZod.generate({
104
+ sourceText: removeGenericTypes(code),
105
+ });
106
+ const fileName = Path.basename(inputFile);
107
+ let f = getZodSchemasFile(`./${fileName.replace('.ts', '')}`);
108
+ // Backend sends nulls not undefined - should be properly set in the OpenAPI TS types generation!
109
+ f = f.replaceAll('.optional()', '.nullable()');
110
+ fs$1.writeFileSync(outputFile, f);
111
+ };
140
112
 
141
- Object.defineProperty(exports, '__esModule', { value: true });
113
+ const generateApiClient = async ({ dstDir, apiName, env, srcSpec, responseWrapper, zodSchemas, }, log = console.log) => {
114
+ if (!srcSpec)
115
+ throw new Error(`Url or path ('srcSpec' not specified`);
116
+ if (!apiName)
117
+ throw new Error('apiName not specified');
118
+ if (!dstDir)
119
+ throw new Error('dstDir not specified');
120
+ if (!env)
121
+ throw new Error('env not specified');
122
+ const dir = Path.resolve(dstDir, apiName);
123
+ if (!fs$1.existsSync(dir)) {
124
+ log(`Creating dir ${dir}`);
125
+ fs$1.mkdirSync(dir, {
126
+ recursive: true,
127
+ });
128
+ }
129
+ const clientDir = Path.resolve(dir, 'client');
130
+ const specPath = await getSpecPath(srcSpec, dir, log);
131
+ const clientFile = await generateOpenApiModel({
132
+ name: apiName,
133
+ input: specPath,
134
+ outputDir: clientDir,
135
+ isNode: env === 'node',
136
+ responseWrapper,
137
+ }, log);
138
+ if (zodSchemas === false)
139
+ return;
140
+ log('Generating Zod schemas');
141
+ generateSchemas(Path.resolve(dir, clientFile), Path.resolve(dir, './zod.ts'));
142
+ };
143
+ const getSpecPath = async (urlOrPath, dir, log) => {
144
+ if (!urlOrPath.startsWith('http')) {
145
+ if (!fs$1.existsSync(urlOrPath))
146
+ throw new Error(`Spec file ${urlOrPath} does not exists`);
147
+ return urlOrPath;
148
+ }
149
+ const specPath = Path.resolve(dir, `spec.json`);
150
+ log(`Will download the API spec from ${urlOrPath} to ${specPath}`);
151
+ await downloadSpec(specPath, urlOrPath);
152
+ return specPath;
153
+ };
142
154
 
143
- }));
155
+ exports.generateApiClient = generateApiClient;
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "@ps-aux/api-client-gen",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "main": "dist/index.js",
5
+ "module": "dist/index.esm.js",
5
6
  "types": "dist/index.d.ts",
6
7
  "bin": {
7
- "gen-api-client": "dist/go.js"
8
+ "gen-api-client": "dist/bin.js"
8
9
  },
9
10
  "scripts": {
10
11
  "build": "rollup -c",
12
+ "dev": "rollup -c --watch",
11
13
  "tc": "tsc",
12
14
  "eh": "foo"
13
15
  },
@@ -16,10 +18,7 @@
16
18
  "dependencies": {
17
19
  "axios": "^1.5.0",
18
20
  "cosmiconfig": "^8.3.6",
19
- "swagger-typescript-api": "^13.0.3"
20
- },
21
- "devDependencies": {
22
- "rollup": "^4.1.4",
23
- "rollup-plugin-typescript2": "^0.36.0"
21
+ "swagger-typescript-api": "^13.0.3",
22
+ "ts-to-zod": "^3.2.0"
24
23
  }
25
24
  }
package/rollup.config.mjs CHANGED
@@ -1,26 +1,40 @@
1
1
  import typescript from 'rollup-plugin-typescript2'
2
+ import resolve from '@rollup/plugin-node-resolve'
3
+
4
+ const external = ['axios', 'cosmiconfig', 'form-data', 'swagger-typescript-api', 'ts-to-zod']
2
5
 
3
6
  export default [{
4
- input: 'src/index.ts',
7
+ input:{
8
+ index: 'src/index.ts',
9
+ bin: 'src/go.ts'
10
+ },
5
11
  output: [
6
12
  {
7
- file: 'dist/index.js',
8
- format: 'umd',
9
- name: 'MyLibrary',
13
+ // file: 'dist/index.js',
14
+ dir: 'dist',
15
+ entryFileNames: '[name].js',
16
+ format: 'cjs',
10
17
  },
11
18
  {
12
- file: 'dist/index.esm.js',
19
+ // file: 'dist/index.esm.js',
20
+ dir: 'dist',
21
+ entryFileNames: '[name].esm.js',
13
22
  format: 'es',
14
23
  },
15
24
  ],
16
- plugins: [typescript()],
17
- }, {
18
- input: 'src/go.ts',
19
- output: [
20
- {
21
- file: 'dist/go.js',
22
- format: 'cjs',
23
- }],
24
-
25
- plugins: [typescript()],
26
- }]
25
+ plugins: [typescript(), resolve()],
26
+ external,
27
+ },
28
+ // {
29
+ // input: 'src/go.ts',
30
+ // output: [
31
+ // {
32
+ // file: 'dist/go.js',
33
+ // format: 'cjs',
34
+ // }],
35
+ //
36
+ // plugins: [typescript(),
37
+ // // resolve(), commonJs(), json()
38
+ // ],
39
+ // }
40
+ ]
@@ -1,24 +1,22 @@
1
1
  import axios from 'axios'
2
- import fs from 'fs'
2
+ import fs from 'fs/promises'
3
3
 
4
4
  export const downloadSpec = async (path: string, url: string) => {
5
5
  const res = await axios({
6
6
  url,
7
7
  method: 'GET',
8
- responseType: 'stream'
8
+ responseType: 'arraybuffer',
9
9
  }).catch(err => {
10
10
  throw err.toString()
11
11
  })
12
12
 
13
- const writer = fs.createWriteStream(path)
13
+ const content = res.data.toString()
14
14
 
15
- // TODO Format to readable JSON
16
- await res.data.pipe(writer)
17
-
18
- return new Promise((resolve, reject) => {
19
- writer.on('finish', resolve)
20
- writer.on('error', err => {
21
- reject(err)
22
- })
23
- })
15
+ await fs.writeFile(path, format(content))
24
16
  }
17
+
18
+ const format = (content: string): string =>
19
+ JSON.stringify(
20
+ JSON.parse(content), null, 4,
21
+ )
22
+
@@ -2,20 +2,28 @@ import Path from 'path'
2
2
  import fs from 'fs'
3
3
  import { downloadSpec } from './downloadSpec'
4
4
  import { generateOpenApiModel } from './generateOpenApiModel'
5
+ import { generateSchemas } from './generateSchemas'
5
6
 
6
7
  export type Params = {
7
8
  srcSpec: string, // path or http(s) URL
8
9
  dstDir: string
9
10
  apiName: string
10
11
  env: 'browser' | 'node'
12
+ responseWrapper?: {
13
+ symbol: string
14
+ import?: string
15
+ },
16
+ zodSchemas?: boolean
11
17
  }
12
18
 
13
- export const generateModel = async ({
14
- dstDir,
15
- apiName,
16
- env,
17
- srcSpec,
18
- }: Params, log: (msg: string) => void = console.log): Promise<void> => {
19
+ export const generateApiClient = async ({
20
+ dstDir,
21
+ apiName,
22
+ env,
23
+ srcSpec,
24
+ responseWrapper,
25
+ zodSchemas,
26
+ }: Params, log: (msg: string) => void = console.log): Promise<void> => {
19
27
  if (!srcSpec) throw new Error(`Url or path ('srcSpec' not specified`)
20
28
  if (!apiName) throw new Error('apiName not specified')
21
29
  if (!dstDir) throw new Error('dstDir not specified')
@@ -32,19 +40,23 @@ export const generateModel = async ({
32
40
 
33
41
  const specPath = await getSpecPath(srcSpec, dir, log)
34
42
 
35
- await generateOpenApiModel(
36
- apiName + '-api',
37
- specPath,
38
- clientDir,
43
+ const clientFile = await generateOpenApiModel(
44
+ {
45
+ name: apiName,
46
+ input: specPath,
47
+ outputDir: clientDir,
48
+ isNode: env === 'node',
49
+ responseWrapper,
50
+ },
39
51
  log,
40
- env === 'node',
41
52
  )
42
53
 
43
- // TODO does not seem to currently generate all schemas
44
- // generateSchemas(
45
- // Path.resolve(modelDir, 'data-contracts.ts'),
46
- // Path.resolve(dir, 'schemas.ts')
47
- // )
54
+ if (zodSchemas === false)
55
+ return
56
+
57
+
58
+ log('Generating Zod schemas')
59
+ generateSchemas(Path.resolve(dir, clientFile), Path.resolve(dir, './zod.ts'))
48
60
  }
49
61
 
50
62
  const getSpecPath = async (urlOrPath: string, dir: string, log: (str: string) => void): Promise<string> => {
@@ -2,13 +2,20 @@ import { generateApi } from 'swagger-typescript-api'
2
2
  import Path from 'path'
3
3
  import fs from 'fs/promises'
4
4
 
5
- export const generateOpenApiModel = async (
5
+ export type GenerateOpenApiModelCmd = {
6
6
  name: string,
7
7
  input: string,
8
8
  outputDir: string,
9
- log: (msg: string) => void,
10
- isNode = false,
11
- ): Promise<void> => {
9
+ isNode?: boolean
10
+ responseWrapper?: {
11
+ symbol: string
12
+ import?: string
13
+ },
14
+ }
15
+
16
+ export const generateOpenApiModel = async ({ input, isNode, responseWrapper, name, outputDir }: GenerateOpenApiModelCmd,
17
+ log: (msg: string) => void,
18
+ ): Promise<string> => {
12
19
 
13
20
  log(`Will generate API client name=${name} to ${outputDir}, node=${isNode}`)
14
21
  const specialMapping = isNode
@@ -17,10 +24,11 @@ export const generateOpenApiModel = async (
17
24
  }
18
25
  : {}
19
26
 
20
- const dstFile = Path.join(outputDir, name + '.ts')
27
+ const fileName = 'index'
28
+ const dstFile = Path.join(outputDir, `${fileName}.ts`)
21
29
 
22
30
  await generateApi({
23
- name,
31
+ name: 'index',
24
32
  output: outputDir,
25
33
  input: input,
26
34
  // modular: true,
@@ -32,6 +40,7 @@ export const generateOpenApiModel = async (
32
40
  // extractRequestBody: true,
33
41
  cleanOutput: true,
34
42
  moduleNameFirstTag: true,
43
+ apiClassName: capitalize(name) + 'Api',
35
44
  // @ts-ignore
36
45
  codeGenConstructs: (struct) => ({
37
46
  // @ts-ignore
@@ -40,16 +49,66 @@ export const generateOpenApiModel = async (
40
49
  // extractRequestParams: true,
41
50
  })
42
51
 
52
+ const addImports: string[] = []
53
+ const replaces: [string, string][] = []
54
+
43
55
  if (isNode) {
44
- let content = await fs.readFile(dstFile).then((r) => r.toString())
45
- // Naive solution
46
- content = 'import stream from \'node:stream\'\n' + content
56
+ addImports.push('import stream from \'node:stream\'')
57
+ }
47
58
 
48
- await fs.writeFile(dstFile, content)
59
+
60
+ if (responseWrapper) {
61
+ log(`Will use response wrapper '${JSON.stringify(responseWrapper)}'`)
62
+ if (responseWrapper.import)
63
+ addImports.push(responseWrapper.import)
64
+
65
+ replaces.push(['Promise', responseWrapper.symbol])
49
66
  }
67
+
68
+
69
+ // Remove unnecessary generics
70
+ replaces.push(['<SecurityDataType extends unknown>', ''])
71
+ replaces.push(['<SecurityDataType>', ''])
72
+
73
+
74
+ log(`Will modify the outputs ${JSON.stringify({
75
+ addImports,
76
+ replaces,
77
+ })}`)
78
+ await modifyOutput(dstFile, {
79
+ addImports,
80
+ replaces,
81
+ })
82
+
83
+ return dstFile
84
+ }
85
+
86
+ const modifyOutput = async (path: string, cmd: {
87
+ addImports: string[]
88
+ replaces: [string, string][]
89
+ }) => {
90
+
91
+
92
+ let content = await fs.readFile(path).then((r) => r.toString())
93
+
94
+ if (cmd.addImports.length) {
95
+ content = cmd.addImports.join('\n') + '\n' + content
96
+ }
97
+
98
+ cmd.replaces.forEach(([from, to]) => {
99
+ content = content.replaceAll(from, to)
100
+ })
101
+
102
+
103
+ await fs.writeFile(path, content)
50
104
  }
51
105
 
52
106
  const getThisScriptDirname = (): string => {
53
107
  // Might be problem in ESM mode
54
108
  return __dirname
55
109
  }
110
+
111
+
112
+ const capitalize = (str: string): string => {
113
+ return str[0].toUpperCase() + str.substring(1)
114
+ }
@@ -0,0 +1,28 @@
1
+ import fs from 'fs'
2
+ import {generate} from 'ts-to-zod'
3
+ import Path from 'path'
4
+
5
+ const removeGenericTypes = (code: string): string => {
6
+ const regex = /export (interface|enum|type) [^<]* \{\n(.*\n)*?}/mg
7
+ const matches = code.matchAll(regex)
8
+ const types = Array.from(matches).map(m => m[0])
9
+
10
+ return types.join('\n')
11
+ }
12
+
13
+ export const generateSchemas = (inputFile: string, outputFile: string) => {
14
+ const code = fs.readFileSync(inputFile).toString()
15
+
16
+ const {getZodSchemasFile} = generate({
17
+ sourceText: removeGenericTypes(code),
18
+ })
19
+
20
+ const fileName = Path.basename(inputFile)
21
+
22
+ let f = getZodSchemasFile(`./${fileName.replace('.ts', '')}`)
23
+
24
+ // Backend sends nulls not undefined - should be properly set in the OpenAPI TS types generation!
25
+ f = f.replaceAll('.optional()', '.nullable()')
26
+ fs.writeFileSync(outputFile, f)
27
+
28
+ }
package/src/go.ts CHANGED
@@ -1,10 +1,7 @@
1
1
  #!/bin/env node
2
- import { generateModel } from './gen-model'
2
+ import { generateApiClient } from './generateApiClient'
3
3
  import { cosmiconfigSync } from 'cosmiconfig'
4
4
 
5
-
6
- const arg = process.argv
7
-
8
5
  const profile = process.argv[2]
9
6
 
10
7
  if (!profile)
@@ -23,4 +20,4 @@ const profileConf = conf.config[profile]
23
20
  if (!profile)
24
21
  throw new Error(`Profile ${profile} not present in the configuration`)
25
22
 
26
- generateModel(profileConf).catch(console.error)
23
+ generateApiClient(profileConf).catch(console.error)
package/src/index.ts CHANGED
@@ -1,8 +1,3 @@
1
- import { generateModel } from "./gen-model";
2
-
3
- export { generateModel } from "./gen-model";
4
-
5
- // Bcs of mixing for ESM and CommonJS - todo fix somehow
6
- export default { generateModel };
1
+ export { generateApiClient } from "./generateApiClient";
7
2
 
8
3
 
@@ -1,5 +1,3 @@
1
- // Use this file also as a symlink for http-client.eta file
2
-
3
1
  export type ContentType = {}
4
2
 
5
3
  export type RequestParams = {
@@ -14,14 +12,13 @@ export const ContentType = {
14
12
  export type Request = {
15
13
  path: string
16
14
  method: 'GET' | 'POST' | 'PUT' | 'DELETE'
17
- format?: 'json'
15
+ format?: 'json' | 'document'
18
16
  query?: any
19
17
  body?: any
20
18
  type?: string
21
19
  secure?: boolean
22
- fileDownload?: boolean
23
20
  }
24
21
 
25
- export type HttpClient<Any = any> = {
22
+ export type HttpClient = {
26
23
  request: <Data, A = any>(req: Request) => Promise<Data>
27
24
  }
package/dist/foo.d.ts DELETED
@@ -1 +0,0 @@
1
- export declare const foo = "123";
@@ -1,7 +0,0 @@
1
- export type Params = {
2
- srcSpec: string;
3
- dstDir: string;
4
- apiName: string;
5
- env: 'browser' | 'node';
6
- };
7
- export declare const generateModel: ({ dstDir, apiName, env, srcSpec, }: Params, log?: (msg: string) => void) => Promise<void>;
package/dist/go DELETED
@@ -1,4 +0,0 @@
1
- #!/bin/env bash
2
-
3
-
4
- echo "gogo"
package/dist/go.js DELETED
@@ -1,151 +0,0 @@
1
- #!/bin/env node
2
- 'use strict';
3
-
4
- var Path = require('path');
5
- var fs = require('fs');
6
- var axios = require('axios');
7
- var swaggerTypescriptApi = require('swagger-typescript-api');
8
- var fs$1 = require('fs/promises');
9
- var cosmiconfig = require('cosmiconfig');
10
-
11
- /******************************************************************************
12
- Copyright (c) Microsoft Corporation.
13
-
14
- Permission to use, copy, modify, and/or distribute this software for any
15
- purpose with or without fee is hereby granted.
16
-
17
- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
18
- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
19
- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
20
- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
21
- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
22
- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
23
- PERFORMANCE OF THIS SOFTWARE.
24
- ***************************************************************************** */
25
- /* global Reflect, Promise, SuppressedError, Symbol */
26
-
27
-
28
- function __awaiter(thisArg, _arguments, P, generator) {
29
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
30
- return new (P || (P = Promise))(function (resolve, reject) {
31
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
32
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
33
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
34
- step((generator = generator.apply(thisArg, _arguments || [])).next());
35
- });
36
- }
37
-
38
- typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
39
- var e = new Error(message);
40
- return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
41
- };
42
-
43
- const downloadSpec = (path, url) => __awaiter(void 0, void 0, void 0, function* () {
44
- const res = yield axios({
45
- url,
46
- method: 'GET',
47
- responseType: 'stream'
48
- }).catch(err => {
49
- throw err.toString();
50
- });
51
- const writer = fs.createWriteStream(path);
52
- // TODO Format to readable JSON
53
- yield res.data.pipe(writer);
54
- return new Promise((resolve, reject) => {
55
- writer.on('finish', resolve);
56
- writer.on('error', err => {
57
- reject(err);
58
- });
59
- });
60
- });
61
-
62
- const generateOpenApiModel = (name, input, outputDir, log, isNode = false) => __awaiter(void 0, void 0, void 0, function* () {
63
- log(`Will generate API client name=${name} to ${outputDir}, node=${isNode}`);
64
- const specialMapping = isNode
65
- ? {
66
- File: '{file: Buffer | stream.Readable, name: string}',
67
- }
68
- : {};
69
- const dstFile = Path.join(outputDir, name + '.ts');
70
- yield swaggerTypescriptApi.generateApi({
71
- name,
72
- output: outputDir,
73
- input: input,
74
- // modular: true,
75
- httpClientType: 'axios',
76
- templates: Path.resolve(getThisScriptDirname(), '../templates'),
77
- defaultResponseAsSuccess: true,
78
- // generateRouteTypes: true,
79
- singleHttpClient: true,
80
- // extractRequestBody: true,
81
- cleanOutput: true,
82
- moduleNameFirstTag: true,
83
- // @ts-ignore
84
- codeGenConstructs: (struct) => ({
85
- // @ts-ignore
86
- Keyword: specialMapping,
87
- }),
88
- // extractRequestParams: true,
89
- });
90
- if (isNode) {
91
- let content = yield fs$1.readFile(dstFile).then((r) => r.toString());
92
- // Naive solution
93
- content = 'import stream from \'node:stream\'\n' + content;
94
- yield fs$1.writeFile(dstFile, content);
95
- }
96
- });
97
- const getThisScriptDirname = () => {
98
- // Might be problem in ESM mode
99
- return __dirname;
100
- };
101
-
102
- const generateModel = ({ dstDir, apiName, env, srcSpec, }, log = console.log) => __awaiter(void 0, void 0, void 0, function* () {
103
- if (!srcSpec)
104
- throw new Error(`Url or path ('srcSpec' not specified`);
105
- if (!apiName)
106
- throw new Error('apiName not specified');
107
- if (!dstDir)
108
- throw new Error('dstDir not specified');
109
- if (!env)
110
- throw new Error('env not specified');
111
- const dir = Path.resolve(dstDir, apiName);
112
- if (!fs.existsSync(dir)) {
113
- log(`Creating dir ${dir}`);
114
- fs.mkdirSync(dir, {
115
- recursive: true,
116
- });
117
- }
118
- const clientDir = Path.resolve(dir, 'client');
119
- const specPath = yield getSpecPath(srcSpec, dir, log);
120
- yield generateOpenApiModel(apiName + '-api', specPath, clientDir, log, env === 'node');
121
- // TODO does not seem to currently generate all schemas
122
- // generateSchemas(
123
- // Path.resolve(modelDir, 'data-contracts.ts'),
124
- // Path.resolve(dir, 'schemas.ts')
125
- // )
126
- });
127
- const getSpecPath = (urlOrPath, dir, log) => __awaiter(void 0, void 0, void 0, function* () {
128
- if (!urlOrPath.startsWith('http')) {
129
- if (!fs.existsSync(urlOrPath))
130
- throw new Error(`Spec file ${urlOrPath} does not exists`);
131
- return urlOrPath;
132
- }
133
- const specPath = Path.resolve(dir, `spec.json`);
134
- log(`Will download the API spec from ${urlOrPath} to ${specPath}`);
135
- yield downloadSpec(specPath, urlOrPath);
136
- return specPath;
137
- });
138
-
139
- process.argv;
140
- const profile = process.argv[2];
141
- if (!profile)
142
- throw new Error(`No profile specified`);
143
- const conf = cosmiconfig.cosmiconfigSync('api-client-gen')
144
- .search();
145
- // TODO validate config
146
- if (!conf)
147
- throw new Error(`No config provided`);
148
- const profileConf = conf.config[profile];
149
- if (!profile)
150
- throw new Error(`Profile ${profile} not present in the configuration`);
151
- generateModel(profileConf).catch(console.error);