@toktokhan-dev/cli-plugin-gen-api-react-query 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +62 -0
- package/dist/index.js +196 -0
- package/package.json +40 -0
- package/templates/custom-axios/api.eta +176 -0
- package/templates/custom-axios/data-contracts.eta +76 -0
- package/templates/custom-axios/http-client.eta +144 -0
- package/templates/custom-axios/procedure-call.eta +133 -0
- package/templates/custom-fetch/api.eta +164 -0
- package/templates/custom-fetch/data-contracts.eta +76 -0
- package/templates/custom-fetch/http-client.eta +224 -0
- package/templates/custom-fetch/procedure-call.eta +151 -0
- package/templates/default/README.md +17 -0
- package/templates/default/base/README.md +8 -0
- package/templates/default/base/data-contract-jsdoc.ejs +37 -0
- package/templates/default/base/data-contracts.ejs +28 -0
- package/templates/default/base/enum-data-contract.ejs +12 -0
- package/templates/default/base/http-client.ejs +3 -0
- package/templates/default/base/http-clients/axios-http-client.ejs +138 -0
- package/templates/default/base/http-clients/fetch-http-client.ejs +224 -0
- package/templates/default/base/interface-data-contract.ejs +10 -0
- package/templates/default/base/object-field-jsdoc.ejs +28 -0
- package/templates/default/base/route-docs.ejs +30 -0
- package/templates/default/base/route-name.ejs +43 -0
- package/templates/default/base/route-type.ejs +22 -0
- package/templates/default/base/type-data-contract.ejs +15 -0
- package/templates/default/default/README.md +7 -0
- package/templates/default/default/api.ejs +65 -0
- package/templates/default/default/procedure-call.ejs +100 -0
- package/templates/default/default/route-types.ejs +28 -0
- package/templates/default/modular/README.md +7 -0
- package/templates/default/modular/api.ejs +28 -0
- package/templates/default/modular/procedure-call.ejs +100 -0
- package/templates/default/modular/route-types.ejs +18 -0
- package/templates/my/param-serializer-by.eta +41 -0
- package/templates/my/react-query-hook.eta +171 -0
- package/templates/my/react-query-key.eta +46 -0
- package/templates/my/react-query-type.eta +61 -0
- package/templates/my/util-types.eta +32 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as _toktokhan_dev_cli from '@toktokhan-dev/cli';
|
|
2
|
+
import { HttpClientType } from 'swagger-typescript-api';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `Swagger` 의 json 을 조회하여 타입정의와 `api class`, `react-query` 관련 모듈을 생성합니다.
|
|
6
|
+
* `axios` 를 사용하는 환경에서 사용가능합니다.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
type GenerateFn = (param: {
|
|
11
|
+
apiInstanceName: string;
|
|
12
|
+
functionName: string;
|
|
13
|
+
pagination: {
|
|
14
|
+
keywords: string[];
|
|
15
|
+
nextKey: string;
|
|
16
|
+
};
|
|
17
|
+
}) => string;
|
|
18
|
+
interface PaginationConfig {
|
|
19
|
+
/** api 의 queryParams key 에 keywords 가 포함되어 있는 항목만 생성됩니다. 키워드 배열은 AND 연산으로써 사용됩니다. */
|
|
20
|
+
keywords: string[];
|
|
21
|
+
/** InfiniteQuery 의 nextPage 와 nextPageParam 을 구하는 함수를 작성하기 위해 사용됩니다. */
|
|
22
|
+
nextKey: string;
|
|
23
|
+
/**
|
|
24
|
+
* InfiniteQuery 의 initialPage 를 커스텀하기 위해 사용됩니다.
|
|
25
|
+
*/
|
|
26
|
+
initialPageParam?: string | GenerateFn;
|
|
27
|
+
/** InfiniteQuery 의 nextPage 를 구하는 함수를 커스텀하기 위해 사용됩니다. */
|
|
28
|
+
getNextPage?: string | GenerateFn;
|
|
29
|
+
/** InfiniteQuery 의 nextPageParam 을 구하는 함수를 커스텀하기 위해 사용됩니다. */
|
|
30
|
+
getNextPageParam?: string | GenerateFn;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* @category Types
|
|
34
|
+
*/
|
|
35
|
+
interface GenerateSwaggerApiConfig {
|
|
36
|
+
/** 조회할 스웨거의 url 혹은 file(yaml, json) 경로 입니다. 통상적으로
|
|
37
|
+
* 백앤드 개발자에게 공유받은 api-swagger-url 의 '/openapi.json' 경로에 해당합니다.
|
|
38
|
+
*/
|
|
39
|
+
swaggerSchemaUrl: string;
|
|
40
|
+
/** 생성될 파일들이 위치할 경로입니다. */
|
|
41
|
+
output: string;
|
|
42
|
+
/** 생성되는 코드의 React Query 포함 여부 입니다.
|
|
43
|
+
* 해당 옵션이 false 일경우 infiniteQuery 를 포함한 모든 Query 가 생성되지 않습니다. */
|
|
44
|
+
includeReactQuery: boolean;
|
|
45
|
+
/** 생성되는 코드의 InfiniteQuery 포함 여부 입니다. */
|
|
46
|
+
includeReactInfiniteQuery: boolean;
|
|
47
|
+
/** Api 의 axios 혹은 fetch 요청 instance 주소입니다. */
|
|
48
|
+
instancePath: string;
|
|
49
|
+
/** http client 타입입니다. */
|
|
50
|
+
httpClientType: HttpClientType;
|
|
51
|
+
/**
|
|
52
|
+
* infiniteQuery 를 생성할 함수 필터 목록 입니다.
|
|
53
|
+
* */
|
|
54
|
+
paginations: PaginationConfig[];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* @category Commands
|
|
58
|
+
*/
|
|
59
|
+
declare const genApi: _toktokhan_dev_cli.MyCommand<GenerateSwaggerApiConfig, "gen:api">;
|
|
60
|
+
|
|
61
|
+
export { type GenerateFn, type GenerateSwaggerApiConfig, type PaginationConfig, genApi };
|
|
62
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { defineCommand } from '@toktokhan-dev/cli';
|
|
2
|
+
import { createPackageRoot, prettierString, prettierFile, withLoading, cwd } from '@toktokhan-dev/node';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { generateApi } from 'swagger-typescript-api';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import camelCase from 'lodash/camelCase.js';
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
// -- Shims --
|
|
10
|
+
import cjsUrl from 'node:url';
|
|
11
|
+
import cjsPath from 'node:path';
|
|
12
|
+
import cjsModule from 'node:module';
|
|
13
|
+
const __filename = cjsUrl.fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = cjsPath.dirname(__filename);
|
|
15
|
+
const require = cjsModule.createRequire(import.meta.url);
|
|
16
|
+
const packageRoot = createPackageRoot(__dirname);
|
|
17
|
+
|
|
18
|
+
const GENERATE_SWAGGER_DATA = {
|
|
19
|
+
CUSTOM_AXIOS_TEMPLATE_FOLDER: packageRoot('templates/custom-axios'),
|
|
20
|
+
CUSTOM_FETCH_TEMPLATE_FOLDER: packageRoot('templates/custom-fetch'),
|
|
21
|
+
EXTRA_TEMPLATE_FOLTER: packageRoot('templates/my'),
|
|
22
|
+
TYPE_FILE: ['react-query-type.ts', 'data-contracts.ts', 'util-types.ts'],
|
|
23
|
+
UTIL_FILE: ['param-serializer-by.ts'],
|
|
24
|
+
QUERY_HOOK_INDICATOR: '@indicator-for-query-hook',
|
|
25
|
+
AXIOS_DEFAULT_INSTANCE_PATH: '@apis/_axios/instance',
|
|
26
|
+
FETCH_DEFAULT_INSTANCE_PATH: '@/configs/fetch/fetch-extend',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const { EXTRA_TEMPLATE_FOLTER, CUSTOM_AXIOS_TEMPLATE_FOLDER, CUSTOM_FETCH_TEMPLATE_FOLDER, QUERY_HOOK_INDICATOR: QUERY_HOOK_INDICATOR$1, } = GENERATE_SWAGGER_DATA;
|
|
30
|
+
const parseSwagger = (config) => generateApi({
|
|
31
|
+
templates: config.httpClientType === 'axios' ?
|
|
32
|
+
CUSTOM_AXIOS_TEMPLATE_FOLDER
|
|
33
|
+
: CUSTOM_FETCH_TEMPLATE_FOLDER,
|
|
34
|
+
modular: true,
|
|
35
|
+
moduleNameFirstTag: true,
|
|
36
|
+
extractEnums: true,
|
|
37
|
+
addReadonly: true,
|
|
38
|
+
unwrapResponseData: true,
|
|
39
|
+
url: config.swaggerSchemaUrl,
|
|
40
|
+
input: config.swaggerSchemaUrl,
|
|
41
|
+
httpClientType: config.httpClientType, // "axios" or "fetch"
|
|
42
|
+
typeSuffix: 'Type',
|
|
43
|
+
prettier: {
|
|
44
|
+
printWidth: 120,
|
|
45
|
+
},
|
|
46
|
+
extraTemplates: [
|
|
47
|
+
{
|
|
48
|
+
name: 'react-query-type.ts', //
|
|
49
|
+
path: path.resolve(EXTRA_TEMPLATE_FOLTER, 'react-query-type.eta'),
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'util-types.ts', //
|
|
53
|
+
path: path.resolve(EXTRA_TEMPLATE_FOLTER, 'util-types.eta'),
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'param-serializer-by.ts', //
|
|
57
|
+
path: path.resolve(EXTRA_TEMPLATE_FOLTER, 'param-serializer-by.eta'),
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
hooks: {
|
|
61
|
+
onPrepareConfig: (defaultConfig) => {
|
|
62
|
+
return {
|
|
63
|
+
...defaultConfig,
|
|
64
|
+
myConfig: { QUERY_HOOK_INDICATOR: QUERY_HOOK_INDICATOR$1, ...config },
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const { TYPE_FILE, UTIL_FILE, QUERY_HOOK_INDICATOR } = GENERATE_SWAGGER_DATA;
|
|
71
|
+
const writeSwaggerApiFile = (params) => {
|
|
72
|
+
const { input, output, spinner } = params;
|
|
73
|
+
input.files.forEach(async ({ fileName, fileContent: content, fileExtension }) => {
|
|
74
|
+
const name = fileName + fileExtension;
|
|
75
|
+
try {
|
|
76
|
+
const isTypeFile = TYPE_FILE.includes(name);
|
|
77
|
+
const isUtilFile = UTIL_FILE.includes(name);
|
|
78
|
+
const isHttpClient = name === 'http-client.ts';
|
|
79
|
+
const isApiFile = content?.includes(QUERY_HOOK_INDICATOR);
|
|
80
|
+
const filename = name.replace('.ts', '');
|
|
81
|
+
const getTargetFolder = () => {
|
|
82
|
+
if (isUtilFile)
|
|
83
|
+
return path.resolve(output, '@utils');
|
|
84
|
+
if (isTypeFile)
|
|
85
|
+
return path.resolve(output, '@types');
|
|
86
|
+
if (isHttpClient)
|
|
87
|
+
return path.resolve(output, `@${filename}`);
|
|
88
|
+
return path.resolve(output, filename);
|
|
89
|
+
};
|
|
90
|
+
const targetFolder = getTargetFolder();
|
|
91
|
+
fs.mkdirSync(targetFolder, { recursive: true });
|
|
92
|
+
if (spinner)
|
|
93
|
+
spinner.info(`generated: ${targetFolder}`);
|
|
94
|
+
if (isHttpClient) {
|
|
95
|
+
generate(path.resolve(targetFolder, 'index.ts'), content);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (isApiFile) {
|
|
99
|
+
const { apiContents, hookContents } = spilitHookContents(filename, content);
|
|
100
|
+
genreatePretty(path.resolve(targetFolder, `${filename}.api.ts`), apiContents);
|
|
101
|
+
genreatePretty(path.resolve(targetFolder, `${filename}.query.ts`), hookContents);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
generate(path.resolve(targetFolder, name), content);
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
console.error(err);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
async function genreatePretty(path, contents) {
|
|
112
|
+
generate(path, await prettierString(contents, {
|
|
113
|
+
parser: 'babel-ts',
|
|
114
|
+
plugins: ['prettier-plugin-organize-imports'],
|
|
115
|
+
}));
|
|
116
|
+
await prettierFile(path, { parser: 'typescript' });
|
|
117
|
+
}
|
|
118
|
+
function generate(path, contents) {
|
|
119
|
+
fs.writeFileSync(path, contents);
|
|
120
|
+
}
|
|
121
|
+
function spilitHookContents(filename, content) {
|
|
122
|
+
const [_apiContent, _hookContent] = content.split(QUERY_HOOK_INDICATOR);
|
|
123
|
+
const lastImport = getLastImportLine(content);
|
|
124
|
+
const lines = content.split('\n');
|
|
125
|
+
const importArea = [
|
|
126
|
+
`import { ${camelCase(filename)}Api } from './${filename}.api';`,
|
|
127
|
+
...lines.slice(0, lastImport),
|
|
128
|
+
].join('\n');
|
|
129
|
+
return {
|
|
130
|
+
apiContents: _apiContent,
|
|
131
|
+
hookContents: importArea + _hookContent,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function getLastImportLine(content) {
|
|
135
|
+
return (Math.max(...content
|
|
136
|
+
.split('\n')
|
|
137
|
+
.map((line, idx) => ({ idx, has: /from ('|").*('|");/.test(line) }))
|
|
138
|
+
.filter(({ has }) => has)
|
|
139
|
+
.map(({ idx }) => idx)) + 1);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @category Commands
|
|
144
|
+
*/
|
|
145
|
+
const genApi = defineCommand({
|
|
146
|
+
name: 'gen:api',
|
|
147
|
+
description: 'swagger schema 를 기반으로 api 를 생성합니다.',
|
|
148
|
+
cliOptions: [],
|
|
149
|
+
default: {
|
|
150
|
+
swaggerSchemaUrl: '',
|
|
151
|
+
output: 'src/generated/apis',
|
|
152
|
+
includeReactQuery: true,
|
|
153
|
+
includeReactInfiniteQuery: true,
|
|
154
|
+
httpClientType: 'axios',
|
|
155
|
+
instancePath: '@apis/_axios/instance',
|
|
156
|
+
paginations: [
|
|
157
|
+
{
|
|
158
|
+
keywords: ['cursor'],
|
|
159
|
+
nextKey: 'cursor',
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
run: async (config) => {
|
|
164
|
+
console.log(config);
|
|
165
|
+
const isWebUrl = (string) => string.startsWith('http://') || string.startsWith('https://');
|
|
166
|
+
const coverPath = (config) => {
|
|
167
|
+
const { httpClientType, swaggerSchemaUrl, output } = config;
|
|
168
|
+
const { AXIOS_DEFAULT_INSTANCE_PATH, FETCH_DEFAULT_INSTANCE_PATH } = GENERATE_SWAGGER_DATA;
|
|
169
|
+
const instancePath = config.instancePath ||
|
|
170
|
+
(httpClientType === 'axios' ?
|
|
171
|
+
AXIOS_DEFAULT_INSTANCE_PATH
|
|
172
|
+
: FETCH_DEFAULT_INSTANCE_PATH);
|
|
173
|
+
return {
|
|
174
|
+
...config,
|
|
175
|
+
instancePath,
|
|
176
|
+
swaggerSchemaUrl: isWebUrl(swaggerSchemaUrl) ? swaggerSchemaUrl : cwd(swaggerSchemaUrl),
|
|
177
|
+
output: cwd(output),
|
|
178
|
+
};
|
|
179
|
+
};
|
|
180
|
+
const covered = coverPath(config);
|
|
181
|
+
const parsed = await withLoading(`Parse Swagger`, covered.swaggerSchemaUrl, () => {
|
|
182
|
+
return parseSwagger(covered);
|
|
183
|
+
});
|
|
184
|
+
if (!parsed) {
|
|
185
|
+
console.error('Failed to generate api: swagger parse error.');
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
withLoading('Write Swagger API', //
|
|
189
|
+
covered.output, (spinner) => {
|
|
190
|
+
writeSwaggerApiFile({ input: parsed, output: covered.output, spinner });
|
|
191
|
+
});
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
export { genApi };
|
|
196
|
+
//# sourceMappingURL=index.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@toktokhan-dev/cli-plugin-gen-api-react-query",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"templates",
|
|
10
|
+
"!dist/*.map"
|
|
11
|
+
],
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"keywords": [],
|
|
19
|
+
"author": "",
|
|
20
|
+
"license": "ISC",
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/lodash": "^4.17.0",
|
|
23
|
+
"@toktokhan-dev/ts-config": "0.0.1"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"eta": "^3.4.0",
|
|
27
|
+
"lodash": "^4.17.21",
|
|
28
|
+
"prettier-plugin-organize-imports": "^3.2.4",
|
|
29
|
+
"swagger-typescript-api": "^13.0.3",
|
|
30
|
+
"@toktokhan-dev/cli": "0.0.1",
|
|
31
|
+
"@toktokhan-dev/node": "0.0.1"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "rollup -c",
|
|
35
|
+
"build:watch": "rollup -c --watch",
|
|
36
|
+
"api-extractor": "api-extractor run --local --verbose",
|
|
37
|
+
"play": "pnpm build:watch & node --watch play/playground.js",
|
|
38
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
<%
|
|
2
|
+
const { utils, route, config, modelTypes, myConfig } = it;
|
|
3
|
+
const { _, classNameCase, pascalCase, require } = utils;
|
|
4
|
+
const { RESERVED_REQ_PARAMS_ARG_NAMES } = config.constants;
|
|
5
|
+
|
|
6
|
+
const routes = route.routes;
|
|
7
|
+
const dataContracts = _.map(modelTypes, "name");
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
const projectRootPath = process.env.PWD;
|
|
11
|
+
const myTemeplatePath = `../my`
|
|
12
|
+
const reactQueryTemplatePath = `${myTemeplatePath}/react-query-hook.eta`
|
|
13
|
+
const reactQuerKeyTemplatePath = `${myTemeplatePath}/react-query-key.eta`
|
|
14
|
+
|
|
15
|
+
const apiClassName = classNameCase(route.moduleName) + 'Api';
|
|
16
|
+
const paginations = myConfig?.paginations
|
|
17
|
+
const instancePath = myConfig?.instancePath || "@apis/_axios/instance.ts"
|
|
18
|
+
|
|
19
|
+
const apiInstanceName =route.moduleName + "Api";
|
|
20
|
+
const queryKeyName = "QUERY_KEY_" + _.upperCase(apiClassName).replace(/ /g, '_');
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
const hasPaginationKeyword = (queryString, keywords = paginationKeywords ) => {
|
|
25
|
+
if(!myConfig?.includeReactInfiniteQuery) return false;
|
|
26
|
+
const keywordUnion = keywords.map(str => `.*${str}.*`).join("|");
|
|
27
|
+
const rgxSting = keywords.map(str => `(${keywordUnion})`).join("");
|
|
28
|
+
const rgx = new RegExp(rgxSting);
|
|
29
|
+
return rgx.test(queryString);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
const upperSnakeCase = (str) => _.upperCase(str).replace(/ /g, '_');
|
|
34
|
+
|
|
35
|
+
const removeModuleName = (str) => str.replace(route.moduleName,'');
|
|
36
|
+
|
|
37
|
+
const getConfigByRoute = (route) => {
|
|
38
|
+
const { specificArgNameResolver } = route
|
|
39
|
+
const { parameters, path, method, payload, query, formData, security, requestParams } = route.request;
|
|
40
|
+
const pathParams = _.values(parameters);
|
|
41
|
+
const pathParamsNames = _.map(pathParams, "name");
|
|
42
|
+
const queryName = (query && query.name) || "query";
|
|
43
|
+
|
|
44
|
+
const requestConfigParam = {
|
|
45
|
+
name: specificArgNameResolver.resolve(RESERVED_REQ_PARAMS_ARG_NAMES),
|
|
46
|
+
optional: true,
|
|
47
|
+
type: "RequestParams",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const argToTmpl = ({ name, optional, type, defaultValue }) => `${name}${!defaultValue && optional ? '?' : ''}: ${type}`;
|
|
51
|
+
|
|
52
|
+
const rawWrapperArgs = config.extractRequestParams ?
|
|
53
|
+
_.compact([
|
|
54
|
+
requestParams && {
|
|
55
|
+
name: pathParams.length ? `{ ${_.join(pathParamsNames, ", ")}, ...${queryName} }` : queryName,
|
|
56
|
+
optional: false,
|
|
57
|
+
type: getInlineParseContent(requestParams),
|
|
58
|
+
},
|
|
59
|
+
...(!requestParams ? pathParams : []),
|
|
60
|
+
payload,
|
|
61
|
+
requestConfigParam,
|
|
62
|
+
]) :
|
|
63
|
+
_.compact([
|
|
64
|
+
...pathParams,
|
|
65
|
+
query,
|
|
66
|
+
payload,
|
|
67
|
+
requestConfigParam,
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
const wrapperArgs = _
|
|
71
|
+
// Sort by optionality
|
|
72
|
+
.sortBy(rawWrapperArgs, [o => o.optional])
|
|
73
|
+
.map(argToTmpl)
|
|
74
|
+
.join('; ')
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
const functionName = route.routeName.usage;
|
|
78
|
+
const hookVariant = _.upperCase(method) === "GET" ? "Query" : "Mutation"
|
|
79
|
+
const key = upperSnakeCase(functionName);
|
|
80
|
+
const methodKey = upperSnakeCase(removeModuleName(functionName));
|
|
81
|
+
const pagination = paginations.find(d => !!query?.type && hasPaginationKeyword(query?.type.split("\n").join(""), d.keywords));
|
|
82
|
+
|
|
83
|
+
const isQuery = hookVariant === "Query";
|
|
84
|
+
const isMutation = hookVariant === "Mutation";
|
|
85
|
+
const hasPagination = !!pagination;
|
|
86
|
+
|
|
87
|
+
const isOptialnalVariabels = _
|
|
88
|
+
// Find optional value
|
|
89
|
+
.filter(rawWrapperArgs, o => o.optional).length === rawWrapperArgs.length;
|
|
90
|
+
|
|
91
|
+
const conditionalVriablesText = isOptialnalVariabels? "variables?" : "variables";
|
|
92
|
+
const repalceTarget = "${" + conditionalVriablesText + ".";
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
conditions:{
|
|
97
|
+
hasPagination,
|
|
98
|
+
isQuery,
|
|
99
|
+
isMutation,
|
|
100
|
+
isOptialnalVariabels,
|
|
101
|
+
},
|
|
102
|
+
data: {
|
|
103
|
+
rawWrapperArgs,
|
|
104
|
+
wrapperArgs,
|
|
105
|
+
queryKeyName,
|
|
106
|
+
functionName,
|
|
107
|
+
apiInstanceName,
|
|
108
|
+
apiClassName,
|
|
109
|
+
hookVariant,
|
|
110
|
+
key,
|
|
111
|
+
methodKey,
|
|
112
|
+
pagination,
|
|
113
|
+
},
|
|
114
|
+
utils: {
|
|
115
|
+
upperSnakeCase,
|
|
116
|
+
removeModuleName,
|
|
117
|
+
argToTmpl,
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const dataForReactHookTemplate = {
|
|
123
|
+
getConfigByRoute,
|
|
124
|
+
queryKeyName,
|
|
125
|
+
apiClassName,
|
|
126
|
+
apiInstanceName,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
%>
|
|
130
|
+
|
|
131
|
+
<% if (config.httpClientType === config.constants.HTTP_CLIENT.AXIOS) { %> import { AxiosRequestConfig, AxiosResponse } from "axios"; <% } %>
|
|
132
|
+
|
|
133
|
+
import { AxiosError } from 'axios';
|
|
134
|
+
import { useQuery, useMutation, useInfiniteQuery } from '@tanstack/react-query';
|
|
135
|
+
import { QueryHookParams, InfiniteQueryHookParams, MutationHookParams, Parameter, RequestFnReturn } from "../@types/react-query-type";
|
|
136
|
+
import { paramsSerializerBy } "../@utils/param-serializer-by"
|
|
137
|
+
|
|
138
|
+
import instance from "<%~ instancePath %>";
|
|
139
|
+
import { HttpClient, RequestParams, ContentType, HttpResponse } from "../@<%~ config.fileNames.httpClient %>";
|
|
140
|
+
import { DeepOmitReadOnly } from '../@types/util-types';
|
|
141
|
+
|
|
142
|
+
<% if (dataContracts.length) { %>
|
|
143
|
+
import { <%~ dataContracts.join(", ") %> } from "../@types/<%~ config.fileNames.dataContracts %>"
|
|
144
|
+
<% } %>
|
|
145
|
+
|
|
146
|
+
export class <%= apiClassName %><SecurityDataType = unknown><% if (!config.singleHttpClient) { %> extends HttpClient<SecurityDataType> <% } %> {
|
|
147
|
+
<% if(config.singleHttpClient) { %>
|
|
148
|
+
http: HttpClient<SecurityDataType>;
|
|
149
|
+
|
|
150
|
+
constructor (http: HttpClient<SecurityDataType>) {
|
|
151
|
+
this.http = http;
|
|
152
|
+
}
|
|
153
|
+
<% } %>
|
|
154
|
+
|
|
155
|
+
<% routes.forEach((route) => { %>
|
|
156
|
+
<%~ includeFile('./procedure-call.eta', { ...it, route }) %>
|
|
157
|
+
<% }) %>
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
export const <%= apiInstanceName %> = new <%= apiClassName %>({ instance })
|
|
162
|
+
|
|
163
|
+
<% if(myConfig.includeReactQuery) { %>
|
|
164
|
+
//<%~ myConfig.QUERY_HOOK_INDICATOR %>
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* QUERY_KEYS
|
|
168
|
+
*/
|
|
169
|
+
<%~ includeFile(reactQuerKeyTemplatePath, { ...it, route , dataFromApiTemplate:dataForReactHookTemplate}) %>
|
|
170
|
+
|
|
171
|
+
<% routes.forEach((route) => { %>
|
|
172
|
+
<%~ includeFile(reactQueryTemplatePath, { ...it, route , dataFromApiTemplate:dataForReactHookTemplate}) %>
|
|
173
|
+
<% }) %>
|
|
174
|
+
<% } %>
|
|
175
|
+
|
|
176
|
+
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<%
|
|
2
|
+
const { modelTypes, utils } = it;
|
|
3
|
+
const { formatDescription, require, _ } = utils;
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
const dataContractTemplates = {
|
|
7
|
+
enum: (contract) => {
|
|
8
|
+
const enumNames = contract['x-enumNames'];
|
|
9
|
+
|
|
10
|
+
if (!contract.enum || !enumNames) {
|
|
11
|
+
console.warn("Warning: 'enum' or 'x-enum' not found in contract:", {
|
|
12
|
+
name: contract.name,
|
|
13
|
+
enum: contract.enum?.join(", "),
|
|
14
|
+
"x-enumNames": enumNames?.join(", ")
|
|
15
|
+
});
|
|
16
|
+
const comment = (() => {
|
|
17
|
+
if (!contract.enum?.length) return 'Enum Values Missing'
|
|
18
|
+
if (!enumNames?.length) return 'X-enumName Values Missing'
|
|
19
|
+
return ''
|
|
20
|
+
})()
|
|
21
|
+
return `type ${contract.name} = ${contract?.enum?.map(e => `"${e}"`).join("|") || "string"}; // ${comment}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (contract.enum.length !== enumNames.length) {
|
|
25
|
+
console.warn("Warning: Length mismatch between 'enum' and 'x-enumNames' in contract:", {
|
|
26
|
+
name: contract.name,
|
|
27
|
+
enum: contract.enum?.join(", "),
|
|
28
|
+
"x-enumNames": enumNames?.join(", ")
|
|
29
|
+
});
|
|
30
|
+
return `type ${contract.name} = ${contract.enum?.map(e => `"${e}"`).join("|")}; // Length mismatch`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const mapName = contract.name + "Map";
|
|
34
|
+
const map = (contract.enum || []).map((key, idx) => {
|
|
35
|
+
const keyName = typeof key === 'number' ? key : `"${key}"`;
|
|
36
|
+
return `${keyName} : "${enumNames?.[idx]}"`;
|
|
37
|
+
}).join(", ");
|
|
38
|
+
|
|
39
|
+
return `type ${contract.name} = keyof typeof ${mapName}; export const ${mapName} = {\r\n${map} \r\n } as const`;
|
|
40
|
+
|
|
41
|
+
},
|
|
42
|
+
interface: (contract) => {
|
|
43
|
+
return `interface ${contract.name} {\r\n${contract.content}}`;
|
|
44
|
+
},
|
|
45
|
+
type: (contract) => {
|
|
46
|
+
return `type ${contract.name} = ${contract.content}`;
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const createDescription = (contract) => {
|
|
51
|
+
if (!contract.typeData) return _.compact([contract.description]);
|
|
52
|
+
|
|
53
|
+
return _.compact([
|
|
54
|
+
contract.description && formatDescription(contract.description),
|
|
55
|
+
!_.isUndefined(contract.typeData.format) && `@format ${contract.typeData.format}`,
|
|
56
|
+
!_.isUndefined(contract.typeData.minimum) && `@min ${contract.typeData.minimum}`,
|
|
57
|
+
!_.isUndefined(contract.typeData.maximum) && `@max ${contract.typeData.maximum}`,
|
|
58
|
+
!_.isUndefined(contract.typeData.pattern) && `@pattern ${contract.typeData.pattern}`,
|
|
59
|
+
!_.isUndefined(contract.typeData.example) && `@example ${
|
|
60
|
+
_.isObject(contract.typeData.example) ? JSON.stringify(contract.typeData.example) : contract.typeData.example
|
|
61
|
+
}`,
|
|
62
|
+
]);
|
|
63
|
+
}
|
|
64
|
+
%>
|
|
65
|
+
<% modelTypes.forEach((contract) => { %>
|
|
66
|
+
<% const description = createDescription(contract); %>
|
|
67
|
+
<% if (description.length) { %>
|
|
68
|
+
/**
|
|
69
|
+
<%~ description.map(part => `* ${part}`).join("\n") %>
|
|
70
|
+
|
|
71
|
+
*/
|
|
72
|
+
<% } %>
|
|
73
|
+
export <%~ (dataContractTemplates[contract.typeIdentifier] || dataContractTemplates.type)(contract) %>
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
<% }) %>
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
<%
|
|
2
|
+
const { apiConfig, generateResponses, config } = it;
|
|
3
|
+
%>
|
|
4
|
+
|
|
5
|
+
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, ResponseType, HeadersDefaults } from "axios";
|
|
6
|
+
|
|
7
|
+
export type QueryParamsType = Record<string | number, any>;
|
|
8
|
+
|
|
9
|
+
export interface FullRequestParams extends Omit<AxiosRequestConfig, "data" | "params" | "url" | "responseType"> {
|
|
10
|
+
/** set parameter to `true` for call `securityWorker` for this request */
|
|
11
|
+
secure?: boolean;
|
|
12
|
+
/** request path */
|
|
13
|
+
path: string;
|
|
14
|
+
/** content type of request body */
|
|
15
|
+
type?: ContentType;
|
|
16
|
+
/** query params */
|
|
17
|
+
query?: QueryParamsType;
|
|
18
|
+
/** format of response (i.e. response.json() -> format: "json") */
|
|
19
|
+
format?: ResponseType;
|
|
20
|
+
/** request body */
|
|
21
|
+
body?: unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type RequestParams = Omit<FullRequestParams, "body" | "method" | "query" | "path">;
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
export interface ApiConfig<SecurityDataType = unknown> {
|
|
28
|
+
securityWorker?: (securityData: SecurityDataType | null) => Promise<AxiosRequestConfig | void> | AxiosRequestConfig | void;
|
|
29
|
+
secure?: boolean;
|
|
30
|
+
format?: ResponseType;
|
|
31
|
+
instance? : AxiosInstance;
|
|
32
|
+
axiosConfig?: Omit<AxiosRequestConfig, 'data' | 'cancelToken'>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export enum ContentType {
|
|
36
|
+
Json = "application/json",
|
|
37
|
+
FormData = "multipart/form-data",
|
|
38
|
+
UrlEncoded = "application/x-www-form-urlencoded",
|
|
39
|
+
Text = "text/plain",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class HttpClient<SecurityDataType = unknown> {
|
|
43
|
+
public instance: AxiosInstance;
|
|
44
|
+
private securityData: SecurityDataType | null = null;
|
|
45
|
+
private securityWorker?: ApiConfig<SecurityDataType>["securityWorker"];
|
|
46
|
+
private secure?: boolean;
|
|
47
|
+
private format?: ResponseType;
|
|
48
|
+
|
|
49
|
+
constructor({ securityWorker, secure, format, instance, axiosConfig }: ApiConfig<SecurityDataType> = {}) {
|
|
50
|
+
this.instance = instance || axios.create({
|
|
51
|
+
...axiosConfig,
|
|
52
|
+
baseURL: axiosConfig?.baseURL|| ""
|
|
53
|
+
});
|
|
54
|
+
this.secure = secure;
|
|
55
|
+
this.format = format;
|
|
56
|
+
this.securityWorker = securityWorker;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
public setSecurityData = (data: SecurityDataType | null) => {
|
|
60
|
+
this.securityData = data
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
protected mergeRequestParams(params1: AxiosRequestConfig, params2?: AxiosRequestConfig): AxiosRequestConfig {
|
|
64
|
+
const method = params1.method || (params2 && params2.method)
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
...this.instance.defaults,
|
|
68
|
+
...params1,
|
|
69
|
+
...(params2 || {}),
|
|
70
|
+
headers: {
|
|
71
|
+
...((method && this.instance.defaults.headers[method.toLowerCase() as keyof HeadersDefaults]) || {}),
|
|
72
|
+
...(params1.headers || {}),
|
|
73
|
+
...((params2 && params2.headers) || {}),
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
protected stringifyFormItem(formItem: unknown) {
|
|
79
|
+
if (typeof formItem === "object" && formItem !== null) {
|
|
80
|
+
return JSON.stringify(formItem);
|
|
81
|
+
} else {
|
|
82
|
+
return `${formItem}`;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
protected createFormData(input: Record<string, unknown>): FormData {
|
|
87
|
+
return Object.keys(input || {}).reduce((formData, key) => {
|
|
88
|
+
const property = input[key];
|
|
89
|
+
const propertyContent: any[] = (property instanceof Array) ? property : [property]
|
|
90
|
+
|
|
91
|
+
for (const formItem of propertyContent) {
|
|
92
|
+
const isFileType = formItem instanceof Blob || formItem instanceof File;
|
|
93
|
+
formData.append(
|
|
94
|
+
key,
|
|
95
|
+
isFileType ? formItem : this.stringifyFormItem(formItem)
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return formData;
|
|
100
|
+
}, new FormData());
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
public request = async <T = any, _E = any>({
|
|
104
|
+
secure,
|
|
105
|
+
path,
|
|
106
|
+
type,
|
|
107
|
+
query,
|
|
108
|
+
format,
|
|
109
|
+
body,
|
|
110
|
+
...params
|
|
111
|
+
<% if (config.unwrapResponseData) { %>
|
|
112
|
+
}: FullRequestParams): Promise<T> => {
|
|
113
|
+
<% } else { %>
|
|
114
|
+
}: FullRequestParams): Promise<AxiosResponse<T>> => {
|
|
115
|
+
<% } %>
|
|
116
|
+
const secureParams = ((typeof secure === 'boolean' ? secure : this.secure) && this.securityWorker && (await this.securityWorker(this.securityData))) || {};
|
|
117
|
+
const requestParams = this.mergeRequestParams(params, secureParams);
|
|
118
|
+
const responseFormat = (format || this.format) || undefined;
|
|
119
|
+
|
|
120
|
+
if (type === ContentType.FormData && body && body !== null && typeof body === "object") {
|
|
121
|
+
body = this.createFormData(body as Record<string, unknown>);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (type === ContentType.Text && body && body !== null && typeof body !== "string") {
|
|
125
|
+
body = JSON.stringify(body);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return this.instance.request({
|
|
129
|
+
...requestParams,
|
|
130
|
+
headers: {
|
|
131
|
+
...(requestParams.headers || {}),
|
|
132
|
+
...(type && type !== ContentType.FormData ? { "Content-Type": type } : {}),
|
|
133
|
+
},
|
|
134
|
+
params: query,
|
|
135
|
+
responseType: responseFormat,
|
|
136
|
+
data: body,
|
|
137
|
+
url: path,
|
|
138
|
+
<% if (config.unwrapResponseData) { %>
|
|
139
|
+
}).then(response => response.data);
|
|
140
|
+
<% } else { %>
|
|
141
|
+
});
|
|
142
|
+
<% } %>
|
|
143
|
+
};
|
|
144
|
+
}
|