@ps-aux/api-client-gen 0.1.2-rc1 → 0.7.0-rc.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,10 @@
1
+ export { g as generateApiClient } from './generateApiClient.mjs';
2
+ import 'path';
3
+ import 'fs';
4
+ import 'fs/promises';
5
+ import 'swagger-typescript-api';
6
+ import 'node:url';
7
+ import 'node:fs/promises';
8
+ import 'node:path';
9
+ import 'node:fs';
10
+ import 'ts-to-zod';
package/package.json CHANGED
@@ -1,28 +1,80 @@
1
1
  {
2
2
  "name": "@ps-aux/api-client-gen",
3
- "version": "0.1.2-rc1",
4
- "main": "dist/index.js",
5
- "module": "dist/index.esm.js",
6
- "types": "dist/index.d.ts",
3
+ "version": "0.7.0-rc.3",
4
+ "main": "dist/index.cjs",
5
+ "type": "module",
7
6
  "bin": {
8
- "gen-api-client": "dist/bin.js"
7
+ "gen-api-client": "dist/bin.cjs"
9
8
  },
10
9
  "scripts": {
11
- "build": "rollup -c",
10
+ "build": "npm run clean && rollup -c && node scripts/ensure-bin-executable.mjs",
11
+ "clean": "rm -rf dist",
12
12
  "dev": "rollup -c --watch",
13
- "pub": "npm test && npm run build && npm publish",
14
- "test": "vitest",
15
- "tc": "tsc"
13
+ "lint": "eslint",
14
+ "pack:tarball": "npm run build && npm pack",
15
+ "prepublishOnly": "echo \"Publishing disabled for this repo. Use CI workflow.\" && exit 1",
16
+ "test": "npm run test:generation && npm run test:generated-code && npm run tc:after-gen",
17
+ "test:generation": "vitest run test/generateApiClient.spec.ts test/gen-open-api/*/generate*.spec.ts test/gen-open-api/*/integration.spec.ts",
18
+ "test:generated-code": "vitest run test/gen-open-api/swagger-ts-api/contract.spec.ts test/gen-open-api/v-next/contract.spec.ts",
19
+ "tc": "echo 'No typecheck'",
20
+ "tc:after-gen": "tsc",
21
+ "tc:v-next-compat": "tsc -p tsconfig.v-next-compat.json"
16
22
  },
17
- "author": "",
18
- "license": "ISC",
23
+ "author": "psaux",
24
+ "license": "MIT",
19
25
  "dependencies": {
20
- "axios": "1.5.0",
21
- "cosmiconfig": "8.3.6",
26
+ "cosmiconfig": "9.0.0",
27
+ "eta": "^2.2.0",
28
+ "prettier": "^3.8.1",
22
29
  "swagger-typescript-api": "13.0.23",
23
- "ts-to-zod": "3.15.0"
30
+ "ts-to-zod": "5.1.0",
31
+ "zod": "^4.3.6"
24
32
  },
25
33
  "devDependencies": {
26
- "vitest": "^3.1.1"
27
- }
34
+ "@local/open-api": "file:../../shared/open-api"
35
+ },
36
+ "description": "CLI and programmatic generator for OpenAPI clients with optional Zod schemas.",
37
+ "keywords": [
38
+ "openapi",
39
+ "client",
40
+ "generator",
41
+ "swagger",
42
+ "zod",
43
+ "typescript",
44
+ "cli"
45
+ ],
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/ps-aux/api-tests.git",
49
+ "directory": "packages/api-client-generator"
50
+ },
51
+ "homepage": "https://github.com/ps-aux/api-tests",
52
+ "bugs": {
53
+ "url": "https://github.com/ps-aux/api-tests/issues"
54
+ },
55
+ "engines": {
56
+ "node": ">=20.0.0"
57
+ },
58
+ "publishConfig": {
59
+ "access": "public"
60
+ },
61
+ "files": [
62
+ "dist",
63
+ "templates",
64
+ "README.md"
65
+ ],
66
+ "exports": {
67
+ ".": {
68
+ "import": {
69
+ "types": "./dist/index.d.mts",
70
+ "default": "./dist/index.mjs"
71
+ },
72
+ "require": {
73
+ "types": "./dist/index.d.cts",
74
+ "default": "./dist/index.cjs"
75
+ },
76
+ "default": "./dist/index.mjs"
77
+ }
78
+ },
79
+ "sideEffects": false
28
80
  }
package/templates/api.ejs CHANGED
@@ -26,8 +26,6 @@ const descriptionLines = _.compact([
26
26
 
27
27
  %>
28
28
 
29
- <% if (config.httpClientType === config.constants.HTTP_CLIENT.AXIOS) { %> import type { AxiosRequestConfig, AxiosResponse } from "axios"; <% } %>
30
-
31
29
  <% if (descriptionLines.length) { %>
32
30
  /**
33
31
  <% descriptionLines.forEach((descriptionLine) => { %>
@@ -36,13 +34,8 @@ const descriptionLines = _.compact([
36
34
  <% }) %>
37
35
  */
38
36
  <% } %>
39
- export class <%~ config.apiClassName %><RequestParams>{
40
-
41
- http: HttpClient<RequestParams>;
42
-
43
- constructor (http: HttpClient<RequestParams>) {
44
- this.http = http;
45
- }
37
+ export class <%~ config.apiClassName %><RequestParams = never>{
38
+ constructor (private http: HttpClient<RequestParams>) {}
46
39
 
47
40
  <% for (const { routes: combinedRoutes = [], moduleName } of routes.combined) { %>
48
41
  <%~ moduleName %> = {
@@ -1,20 +1,44 @@
1
- export type ContentType = {}
1
+ export type KnownRequestContentType =
2
+ | 'application/json'
3
+ | 'multipart/form-data'
4
+ | 'application/x-www-form-urlencoded'
2
5
 
3
- export const ContentType = {
4
- Json: 'application/json',
5
- FormData: 'multipart/form-data',
6
- }
6
+ export type RequestContentType = KnownRequestContentType | string
7
+
8
+ export type QueryValue =
9
+ | string
10
+ | number
11
+ | boolean
12
+ | null
13
+ | undefined
14
+ | QueryValue[]
15
+ | Record<string, any>
16
+ // Empty schema support
17
+ | unknown
18
+
19
+ export type QueryParams = Record<string, QueryValue>
7
20
 
8
21
  export type Request = {
9
22
  path: string
10
23
  method: 'GET' | 'POST' | 'PUT' | 'DELETE'
11
24
  format?: 'json' | 'document'
12
- query?: any
25
+ headers?: Record<string, string>
26
+ query?: QueryParams
13
27
  body?: any
14
- type?: string
15
- secure?: boolean
28
+ requestContentType?: RequestContentType
16
29
  }
17
30
 
18
- export type HttpClient<RequestParams> = {
19
- request: <Data>(req: Request, params?: RequestParams) => Promise<Data>
31
+ export type HttpResponse<Data> = {
32
+ body: Data
33
+ headers: Record<string, string | string[]>
34
+ status: number
35
+ }
36
+
37
+ export type QuerySerializer = (params: QueryParams) => string
38
+
39
+ export type HttpClient<RequestParams = never> = {
40
+ request: <Data>(
41
+ req: Request,
42
+ params?: RequestParams
43
+ ) => Promise<HttpResponse<Data>>
20
44
  }
@@ -2,16 +2,14 @@
2
2
  const { utils, route, config } = it;
3
3
  const { requestBodyInfo, responseBodyInfo, specificArgNameResolver } = route;
4
4
  const { _, getInlineParseContent, getParseContent, parseSchema, getComponentByRef, require } = utils;
5
- const { parameters, path, method, payload, query, formData, security, requestParams } = route.request;
5
+ const { parameters, path, method, payload, query, formData, requestParams } = route.request;
6
6
  const { type, errorType, contentTypes } = route.response;
7
- const { HTTP_CLIENT, RESERVED_REQ_PARAMS_ARG_NAMES } = config.constants;
7
+ const { RESERVED_REQ_PARAMS_ARG_NAMES } = config.constants;
8
8
  const routeDocs = includeFile("@base/route-docs", { config, route, utils });
9
9
  const queryName = (query && query.name) || "query";
10
10
  const pathParams = _.values(parameters);
11
11
  const pathParamsNames = _.map(pathParams, "name");
12
12
 
13
- const isFetchTemplate = config.httpClientType === HTTP_CLIENT.FETCH;
14
-
15
13
  const requestConfigParam = {
16
14
  name: specificArgNameResolver.resolve(RESERVED_REQ_PARAMS_ARG_NAMES),
17
15
  optional: true,
@@ -44,47 +42,58 @@ const wrapperArgs = _
44
42
  .map(argToTmpl)
45
43
  .join(', ')
46
44
 
47
- // RequestParams["type"]
45
+ // Request["requestContentType"]
48
46
  const requestContentKind = {
49
- "JSON": "ContentType.Json",
50
- "URL_ENCODED": "ContentType.UrlEncoded",
51
- "FORM_DATA": "ContentType.FormData",
52
- "TEXT": "ContentType.Text",
47
+ "JSON": '"application/json"',
48
+ "URL_ENCODED": '"application/x-www-form-urlencoded"',
49
+ "FORM_DATA": '"multipart/form-data"',
50
+ "TEXT": '"text/plain"',
53
51
  }
54
52
  // RequestParams["format"]
55
53
  const responseContentKind = {
56
54
  "JSON": '"json"',
57
55
  "IMAGE": '"blob"',
58
- "FORM_DATA": isFetchTemplate ? '"formData"' : '"document"'
56
+ "FORM_DATA": '"document"'
59
57
  }
60
58
 
61
59
  const bodyTmpl = _.get(payload, "name") || null;
62
60
  const queryTmpl = (query != null && queryName) || null;
63
61
  const bodyContentKindTmpl = requestContentKind[requestBodyInfo.contentKind] || null;
64
- const responseFormatTmpl = responseContentKind[responseBodyInfo.success && responseBodyInfo.success.schema && responseBodyInfo.success.schema.contentKind] || null;
65
- const securityTmpl = security ? 'true' : null;
62
+ const successResponseSchema = _.get(responseBodyInfo, "success.schema") || null;
63
+ const hasBinaryResponseSchema = _.some(
64
+ _.values(_.get(successResponseSchema, "content", {})),
65
+ contentItem => {
66
+ const schema = _.get(contentItem, "schema");
67
+ return (
68
+ _.get(schema, "type") === "string" &&
69
+ _.get(schema, "format") === "binary"
70
+ );
71
+ }
72
+ );
73
+ // TODO understand this more. Right know we believe the agent it is needed.
74
+ const hasFileResponseType = typeof type === "string" &&
75
+ /(^|[^a-zA-Z0-9_])File([^a-zA-Z0-9_]|$)/.test(type);
76
+ const responseFormatByContentKind = responseContentKind[
77
+ responseBodyInfo.success &&
78
+ responseBodyInfo.success.schema &&
79
+ responseBodyInfo.success.schema.contentKind
80
+ ] || null;
81
+ const responseFormatTmpl = hasBinaryResponseSchema || hasFileResponseType
82
+ ? '"document"'
83
+ : responseFormatByContentKind;
84
+ const routeDocLines = routeDocs.lines ? `${routeDocs.lines}\n` : '';
66
85
 
67
86
  const describeReturnType = () => {
68
87
  if (!config.toJS) return "";
69
-
70
- switch(config.httpClientType) {
71
- case HTTP_CLIENT.AXIOS: {
72
- return `Promise<AxiosResponse<${type}>>`
73
- }
74
- default: {
75
- return `Promise<HttpResponse<${type}, ${errorType}>`
76
- }
77
- }
88
+ return `Promise<${type}>`
78
89
  }
79
90
 
80
91
  %>
81
92
  /**
93
+ <% if (route.raw.description) { %>
82
94
  <%~ routeDocs.description %>
83
-
84
- *<% /* Here you can add some other JSDoc tags */ %>
85
-
86
- <%~ routeDocs.lines %>
87
-
95
+ <% } %>
96
+ <%~ routeDocLines %>
88
97
  */
89
98
  <%~ route.routeName.usage %><%~ route.namespace ? ': ' : ' = ' %>(<%~ wrapperArgs %>)<%~ config.toJS ? `: ${describeReturnType()}` : "" %> =>
90
99
  <%~ config.singleHttpClient ? 'this.http.request' : 'this.request' %><<%~ type %>>({
@@ -92,7 +101,6 @@ const describeReturnType = () => {
92
101
  method: '<%~ _.upperCase(method) %>',
93
102
  <%~ queryTmpl ? `query: ${queryTmpl},` : '' %>
94
103
  <%~ bodyTmpl ? `body: ${bodyTmpl},` : '' %>
95
- <%~ securityTmpl ? `secure: ${securityTmpl},` : '' %>
96
- <%~ bodyContentKindTmpl ? `type: ${bodyContentKindTmpl},` : '' %>
104
+ <%~ bodyContentKindTmpl ? `requestContentType: ${bodyContentKindTmpl},` : '' %>
97
105
  <%~ responseFormatTmpl ? `format: ${responseFormatTmpl},` : '' %>
98
- }, params)<%~ route.namespace ? ',' : '' %>
106
+ }, params).then(res => res.body)<%~ route.namespace ? ',' : '' %>
package/dist/bin.esm.js DELETED
@@ -1,22 +0,0 @@
1
- #!/bin/env node
2
- import { g as generateApiClient } from './generateApiClient-QyDbHHKj.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 DELETED
@@ -1,24 +0,0 @@
1
- #!/bin/env node
2
- 'use strict';
3
-
4
- var generateApiClient = require('./generateApiClient-C11lxAxn.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
- generateApiClient.generateApiClient(profileConf).catch(console.error);
@@ -1 +0,0 @@
1
- export declare const downloadSpec: (path: string, url: string) => Promise<void>;
@@ -1,188 +0,0 @@
1
- 'use strict';
2
-
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');
9
-
10
- function _interopNamespaceDefault(e) {
11
- var n = Object.create(null);
12
- if (e) {
13
- Object.keys(e).forEach(function (k) {
14
- if (k !== 'default') {
15
- var d = Object.getOwnPropertyDescriptor(e, k);
16
- Object.defineProperty(n, k, d.get ? d : {
17
- enumerable: true,
18
- get: function () { return e[k]; }
19
- });
20
- }
21
- });
22
- }
23
- n.default = e;
24
- return Object.freeze(n);
25
- }
26
-
27
- var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs$1);
28
-
29
- const downloadSpec = async (path, url) => {
30
- const res = await axios({
31
- url,
32
- method: 'GET',
33
- responseType: 'arraybuffer',
34
- }).catch(err => {
35
- throw err.toString();
36
- });
37
- const content = res.data.toString();
38
- await fs.writeFile(path, format(content));
39
- };
40
- const format = (content) => JSON.stringify(JSON.parse(content), null, 4);
41
-
42
- const generateOpenApiModel = async ({ input, isNode, responseWrapper, name, outputDir }, log) => {
43
- log(`Will generate API client name=${name} to ${outputDir}, node=${isNode}`);
44
- const specialMapping = isNode
45
- ? {
46
- File: '{file: Buffer | stream.Readable, name: string}',
47
- }
48
- : {};
49
- const fileName = 'index';
50
- const dstFile = Path.join(outputDir, `${fileName}.ts`);
51
- await swaggerTypescriptApi.generateApi({
52
- name: 'index',
53
- output: outputDir,
54
- input: input,
55
- // modular: true,
56
- httpClientType: 'axios',
57
- templates: Path.resolve(getThisScriptDirname(), '../templates'),
58
- defaultResponseAsSuccess: true,
59
- // generateRouteTypes: true,
60
- singleHttpClient: true,
61
- // extractRequestBody: true,
62
- cleanOutput: true,
63
- moduleNameFirstTag: true,
64
- apiClassName: capitalize(name) + 'Api',
65
- // @ts-ignore
66
- codeGenConstructs: (struct) => ({
67
- // @ts-ignore
68
- Keyword: specialMapping,
69
- }),
70
- // extractRequestParams: true,
71
- });
72
- const addImports = [];
73
- const replaces = [];
74
- if (isNode) {
75
- addImports.push('import stream from \'node:stream\'');
76
- }
77
- if (responseWrapper) {
78
- log(`Will use response wrapper '${JSON.stringify(responseWrapper)}'`);
79
- if (responseWrapper.import)
80
- addImports.push(responseWrapper.import);
81
- replaces.push(['Promise', responseWrapper.symbol]);
82
- }
83
- // Remove unnecessary generics
84
- replaces.push(['<SecurityDataType extends unknown>', '']);
85
- replaces.push(['<SecurityDataType>', '']);
86
- log(`Will modify the outputs ${JSON.stringify({
87
- addImports,
88
- replaces,
89
- })}`);
90
- await modifyOutput(dstFile, {
91
- addImports,
92
- replaces,
93
- });
94
- return dstFile;
95
- };
96
- const modifyOutput = async (path, cmd) => {
97
- let content = await fs.readFile(path).then((r) => r.toString());
98
- if (cmd.addImports.length) {
99
- content = cmd.addImports.join('\n') + '\n' + content;
100
- }
101
- cmd.replaces.forEach(([from, to]) => {
102
- content = content.replaceAll(from, to);
103
- });
104
- await fs.writeFile(path, content);
105
- };
106
- const getThisScriptDirname = () => {
107
- // Might be problem in ESM mode
108
- return __dirname;
109
- };
110
- const capitalize = (str) => {
111
- return str[0].toUpperCase() + str.substring(1);
112
- };
113
-
114
- // TODO needed?
115
- const removeGenericTypes = (code) => {
116
- const regex = /export (interface|enum|type) [^<]* \{\n(.*\n)*?}/mg;
117
- const matches = code.matchAll(regex);
118
- const types = Array.from(matches).map(m => m[0]);
119
- return types.join('\n');
120
- };
121
- const generateSchemas = (inputFile, outputFile, opts = {}) => {
122
- const code = fs$1.readFileSync(inputFile).toString();
123
- const { getZodSchemasFile } = tsToZod.generate({
124
- sourceText: removeGenericTypes(code),
125
- // inputOutputMappings: {
126
- //
127
- // },
128
- customJSDocFormatTypes: {
129
- // Custom mapping
130
- // 'date-time': 'blablaj' - regex
131
- }
132
- });
133
- const fileName = Path.basename(inputFile);
134
- let f = getZodSchemasFile(`./${fileName.replace('.ts', '')}`);
135
- // Add import from the api model - TODO find a way how to define on generate
136
- f = f.replaceAll('"./index";', '"./client"');
137
- // Backend sends nulls not undefined - should be properly set in the OpenAPI TS types generation!
138
- f = f.replaceAll('.optional()', '.nullable()');
139
- if (opts.localDateTimes) {
140
- f = f.replaceAll('z.string().datetime()', 'z.string().datetime({local: true})');
141
- }
142
- fs$1.writeFileSync(outputFile, f);
143
- };
144
-
145
- const generateApiClient = async ({ dstDir, apiName, env, srcSpec, responseWrapper, zodSchemas, }, log = console.log) => {
146
- if (!srcSpec)
147
- throw new Error(`Url or path ('srcSpec' not specified`);
148
- if (!apiName)
149
- throw new Error('apiName not specified');
150
- if (!dstDir)
151
- throw new Error('dstDir not specified');
152
- if (!env)
153
- throw new Error('env not specified');
154
- const dir = Path.resolve(dstDir, apiName);
155
- if (!fs__namespace.existsSync(dir)) {
156
- log(`Creating dir ${dir}`);
157
- fs__namespace.mkdirSync(dir, {
158
- recursive: true,
159
- });
160
- }
161
- const clientDir = Path.resolve(dir, 'client');
162
- const specPath = await getSpecPath(srcSpec, dir, log);
163
- const clientFile = await generateOpenApiModel({
164
- name: apiName,
165
- input: specPath,
166
- outputDir: clientDir,
167
- isNode: env === 'node',
168
- responseWrapper,
169
- }, log);
170
- console.log('client file', clientFile);
171
- if (zodSchemas?.enabled === false)
172
- return;
173
- log('Generating Zod schemas');
174
- generateSchemas(Path.resolve(dir, clientFile), Path.resolve(dir, './zod.ts'), zodSchemas);
175
- };
176
- const getSpecPath = async (urlOrPath, dir, log) => {
177
- if (!urlOrPath.startsWith('http')) {
178
- if (!fs__namespace.existsSync(urlOrPath))
179
- throw new Error(`Spec file ${urlOrPath} does not exists`);
180
- return urlOrPath;
181
- }
182
- const specPath = Path.resolve(dir, `spec.json`);
183
- log(`Will download the API spec from ${urlOrPath} to ${specPath}`);
184
- await downloadSpec(specPath, urlOrPath);
185
- return specPath;
186
- };
187
-
188
- exports.generateApiClient = generateApiClient;