@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
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
<%
|
|
2
|
+
const { utils, route, config } = it;
|
|
3
|
+
const { requestBodyInfo, responseBodyInfo, specificArgNameResolver } = route;
|
|
4
|
+
const { _, getInlineParseContent, getParseContent, parseSchema, getComponentByRef, require } = utils;
|
|
5
|
+
const { parameters, path, method, payload, query, formData, security, requestParams } = route.request;
|
|
6
|
+
const { type, errorType, contentTypes } = route.response;
|
|
7
|
+
const { HTTP_CLIENT, RESERVED_REQ_PARAMS_ARG_NAMES } = config.constants;
|
|
8
|
+
const routeDocs = includeFile("@base/route-docs", { config, route, utils });
|
|
9
|
+
const queryName = (query && query.name) || "query";
|
|
10
|
+
const pathParams = _.values(parameters);
|
|
11
|
+
const pathParamsNames = _.map(pathParams, "name");
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
const isFetchTemplate = config.httpClientType === HTTP_CLIENT.FETCH;
|
|
15
|
+
|
|
16
|
+
const requestConfigParam = {
|
|
17
|
+
name: specificArgNameResolver.resolve(RESERVED_REQ_PARAMS_ARG_NAMES),
|
|
18
|
+
optional: true,
|
|
19
|
+
type: "RequestParams",
|
|
20
|
+
defaultValue: "{}",
|
|
21
|
+
}
|
|
22
|
+
const argToTmpl = ({ name, optional, type, defaultValue }) => {
|
|
23
|
+
return `${name}${optional ? '?' : ''}: ${name === "data" ? `DeepOmitReadOnly<${type}>` :type}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
const rawWrapperArgs = config.extractRequestParams ?
|
|
28
|
+
_.compact([
|
|
29
|
+
requestParams && {
|
|
30
|
+
name: pathParams.length ? `{ ${_.join(pathParamsNames, ", ")}, ...${queryName} }` : queryName,
|
|
31
|
+
optional: false,
|
|
32
|
+
type: getInlineParseContent(requestParams),
|
|
33
|
+
},
|
|
34
|
+
...(!requestParams ? pathParams : []),
|
|
35
|
+
payload,
|
|
36
|
+
requestConfigParam,
|
|
37
|
+
]) :
|
|
38
|
+
_.compact([
|
|
39
|
+
...pathParams,
|
|
40
|
+
query,
|
|
41
|
+
payload,
|
|
42
|
+
requestConfigParam,
|
|
43
|
+
])
|
|
44
|
+
|
|
45
|
+
const wrapperArgs = _
|
|
46
|
+
// Sort by optionality
|
|
47
|
+
.sortBy(rawWrapperArgs, [o => o.optional])
|
|
48
|
+
.map(argToTmpl)
|
|
49
|
+
.join('; ');
|
|
50
|
+
|
|
51
|
+
const isOptialnalVariabels = _
|
|
52
|
+
// Find optional value
|
|
53
|
+
.filter(rawWrapperArgs, o => o.optional).length === rawWrapperArgs.length;
|
|
54
|
+
|
|
55
|
+
const conditionalVriablesText = isOptialnalVariabels? "variables?" : "variables";
|
|
56
|
+
const repalceTarget = "${" + conditionalVriablesText + ".";
|
|
57
|
+
const pathWithVariables = path.replace(/\$\{/g, repalceTarget);
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
// RequestParams["type"]
|
|
62
|
+
const requestContentKind = {
|
|
63
|
+
"JSON": "ContentType.Json",
|
|
64
|
+
"URL_ENCODED": "ContentType.UrlEncoded",
|
|
65
|
+
"FORM_DATA": "ContentType.FormData",
|
|
66
|
+
"TEXT": "ContentType.Text",
|
|
67
|
+
}
|
|
68
|
+
// RequestParams["format"]
|
|
69
|
+
const responseContentKind = {
|
|
70
|
+
"JSON": '"json"',
|
|
71
|
+
"IMAGE": '"blob"',
|
|
72
|
+
"FORM_DATA": isFetchTemplate ? '"formData"' : '"document"'
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const explodedParams = (properties) => {
|
|
76
|
+
return Object.keys(properties).filter((key) => {
|
|
77
|
+
const item = properties[key]
|
|
78
|
+
return item.type === "array" && item.explode === true
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const bodyTmpl = _.get(payload, "name") || null;
|
|
83
|
+
const queryTmpl = (query != null && queryName) || null;
|
|
84
|
+
const bodyContentKindTmpl = requestContentKind[requestBodyInfo.contentKind] || null;
|
|
85
|
+
const responseFormatTmpl = responseContentKind[responseBodyInfo.success && responseBodyInfo.success.schema && responseBodyInfo.success.schema.contentKind] || null;
|
|
86
|
+
const securityTmpl = security ? 'true' : null;
|
|
87
|
+
const paramsSerializerTmpl = (() => {
|
|
88
|
+
if (!requestParams?.properties) return;
|
|
89
|
+
const exploded = explodedParams(requestParams.properties);
|
|
90
|
+
|
|
91
|
+
if (!exploded.length) return;
|
|
92
|
+
const configs = exploded.map((key) => `"${key}" : "repeat"`)
|
|
93
|
+
|
|
94
|
+
return `paramsSerializerBy({ ${configs.join(",")} })`
|
|
95
|
+
})()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
const describeReturnType = () => {
|
|
100
|
+
if (!config.toJS) return "";
|
|
101
|
+
|
|
102
|
+
switch(config.httpClientType) {
|
|
103
|
+
case HTTP_CLIENT.AXIOS: {
|
|
104
|
+
return `Promise<AxiosResponse<${type}>>`
|
|
105
|
+
}
|
|
106
|
+
default: {
|
|
107
|
+
return `Promise<HttpResponse<${type}, ${errorType}>`
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
%>
|
|
113
|
+
/**
|
|
114
|
+
<%~ routeDocs.description %>
|
|
115
|
+
|
|
116
|
+
*<% /* Here you can add some other JSDoc tags */ %>
|
|
117
|
+
|
|
118
|
+
<%~ routeDocs.lines %>
|
|
119
|
+
|
|
120
|
+
*/
|
|
121
|
+
<%~ route.routeName.usage %> = (<%~ conditionalVriablesText %> :{<%~ wrapperArgs %>})<%~ config.toJS ? `: ${describeReturnType()}` : "" %> =>
|
|
122
|
+
<%~ config.singleHttpClient ? 'this.http.request' : 'this.request' %><<%~ type %>, <%~ errorType %>>({
|
|
123
|
+
path: `<%~ pathWithVariables %>`,
|
|
124
|
+
method: '<%~ _.upperCase(method) %>',
|
|
125
|
+
<%~ queryTmpl ? `query: ${conditionalVriablesText}.${queryTmpl},` : '' %>
|
|
126
|
+
<%~ bodyTmpl ? `body: ${conditionalVriablesText}.${bodyTmpl},` : '' %>
|
|
127
|
+
<%~ securityTmpl ? `secure: ${securityTmpl},` : '' %>
|
|
128
|
+
<%~ bodyContentKindTmpl ? `type: ${bodyContentKindTmpl},` : '' %>
|
|
129
|
+
<%~ responseFormatTmpl ? `format: ${responseFormatTmpl},` : '' %>
|
|
130
|
+
<%~ paramsSerializerTmpl ? `paramsSerializer: ${paramsSerializerTmpl},` : '' %>
|
|
131
|
+
...<%~ `${conditionalVriablesText}`%>.<%~ _.get(requestConfigParam, "name") %>,
|
|
132
|
+
})
|
|
133
|
+
|
|
@@ -0,0 +1,164 @@
|
|
|
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
|
+
const projectRootPath = process.env.PWD;
|
|
10
|
+
const myTemeplatePath = `../my`
|
|
11
|
+
const reactQueryTemplatePath = `${myTemeplatePath}/react-query-hook.eta`
|
|
12
|
+
const reactQuerKeyTemplatePath = `${myTemeplatePath}/react-query-key.eta`
|
|
13
|
+
|
|
14
|
+
const apiClassName = classNameCase(route.moduleName) + 'Api';
|
|
15
|
+
const paginations = myConfig?.paginations;
|
|
16
|
+
const instancePath = myConfig?.instancePath || "@/configs/fetch/fetch-extend";
|
|
17
|
+
|
|
18
|
+
const apiInstanceName = route.moduleName + "Api";
|
|
19
|
+
const queryKeyName = "QUERY_KEY_" + _.upperCase(apiClassName).replace(/ /g, '_');
|
|
20
|
+
|
|
21
|
+
const hasPaginationKeyword = (queryString, keywords = paginationKeywords ) => {
|
|
22
|
+
if (!myConfig?.includeReactInfiniteQuery) return false;
|
|
23
|
+
const keywordUnion = keywords.map(str => `.*${str}.*`).join("|");
|
|
24
|
+
const rgxSting = keywords.map(str => `(${keywordUnion})`).join("");
|
|
25
|
+
const rgx = new RegExp(rgxSting);
|
|
26
|
+
return rgx.test(queryString);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const upperSnakeCase = (str) => _.upperCase(str).replace(/ /g, '_');
|
|
30
|
+
|
|
31
|
+
const removeModuleName = (str) => str.replace(route.moduleName, '');
|
|
32
|
+
|
|
33
|
+
const getConfigByRoute = (route) => {
|
|
34
|
+
const { specificArgNameResolver } = route;
|
|
35
|
+
const { parameters, path, method, payload, query, formData, security, requestParams } = route.request;
|
|
36
|
+
const pathParams = _.values(parameters);
|
|
37
|
+
const pathParamsNames = _.map(pathParams, "name");
|
|
38
|
+
const queryName = (query && query.name) || "query";
|
|
39
|
+
|
|
40
|
+
const requestConfigParam = {
|
|
41
|
+
name: specificArgNameResolver.resolve(RESERVED_REQ_PARAMS_ARG_NAMES),
|
|
42
|
+
optional: true,
|
|
43
|
+
type: "RequestParams",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const argToTmpl = ({ name, optional, type, defaultValue }) => `${name}${!defaultValue && optional ? '?' : ''}: ${type}`;
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
const rawWrapperArgs = config.extractRequestParams
|
|
50
|
+
? _.compact([
|
|
51
|
+
requestParams && {
|
|
52
|
+
name: pathParams.length ? `{ ${_.join(pathParamsNames, ", ")}, ...${queryName} }` : queryName,
|
|
53
|
+
optional: false,
|
|
54
|
+
type: getInlineParseContent(requestParams),
|
|
55
|
+
},
|
|
56
|
+
...(!requestParams ? pathParams : []),
|
|
57
|
+
payload,
|
|
58
|
+
requestConfigParam,
|
|
59
|
+
])
|
|
60
|
+
: _.compact([
|
|
61
|
+
...pathParams,
|
|
62
|
+
query,
|
|
63
|
+
payload,
|
|
64
|
+
requestConfigParam,
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
const wrapperArgs = _
|
|
68
|
+
// Sort by optionality
|
|
69
|
+
.sortBy(rawWrapperArgs, [o => o.optional])
|
|
70
|
+
.map(argToTmpl)
|
|
71
|
+
.join('; ');
|
|
72
|
+
|
|
73
|
+
const functionName = route.routeName.usage;
|
|
74
|
+
const hookVariant = _.upperCase(method) === "GET" ? "Query" : "Mutation";
|
|
75
|
+
const key = upperSnakeCase(functionName);
|
|
76
|
+
const methodKey = upperSnakeCase(removeModuleName(functionName));
|
|
77
|
+
const pagination = paginations.find(d => !!query?.type && hasPaginationKeyword(query?.type.split("\n").join(""), d.keywords));
|
|
78
|
+
|
|
79
|
+
const isQuery = hookVariant === "Query";
|
|
80
|
+
const isMutation = hookVariant === "Mutation";
|
|
81
|
+
const hasPagination = !!pagination;
|
|
82
|
+
|
|
83
|
+
const isOptialnalVariabels = _
|
|
84
|
+
// Find optional value
|
|
85
|
+
.filter(rawWrapperArgs, o => o.optional).length === rawWrapperArgs.length;
|
|
86
|
+
|
|
87
|
+
const conditionalVriablesText = isOptialnalVariabels ? "variables?" : "variables";
|
|
88
|
+
const repalceTarget = "${" + conditionalVriablesText + ".";
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
conditions: {
|
|
92
|
+
hasPagination,
|
|
93
|
+
isQuery,
|
|
94
|
+
isMutation,
|
|
95
|
+
isOptialnalVariabels,
|
|
96
|
+
},
|
|
97
|
+
data: {
|
|
98
|
+
rawWrapperArgs,
|
|
99
|
+
wrapperArgs,
|
|
100
|
+
queryKeyName,
|
|
101
|
+
functionName,
|
|
102
|
+
apiInstanceName,
|
|
103
|
+
apiClassName,
|
|
104
|
+
hookVariant,
|
|
105
|
+
key,
|
|
106
|
+
methodKey,
|
|
107
|
+
pagination,
|
|
108
|
+
},
|
|
109
|
+
utils: {
|
|
110
|
+
upperSnakeCase,
|
|
111
|
+
removeModuleName,
|
|
112
|
+
argToTmpl,
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const dataForReactHookTemplate = {
|
|
118
|
+
getConfigByRoute,
|
|
119
|
+
queryKeyName,
|
|
120
|
+
apiClassName,
|
|
121
|
+
apiInstanceName,
|
|
122
|
+
};
|
|
123
|
+
%>
|
|
124
|
+
|
|
125
|
+
import { useQuery, useMutation, useInfiniteQuery } from '@tanstack/react-query';
|
|
126
|
+
import { QueryHookParams, InfiniteQueryHookParams, MutationHookParams, Parameter, RequestFnReturn } from "../@types/react-query-type";
|
|
127
|
+
import { paramsSerializerBy } from "../@utils/param-serializer-by";
|
|
128
|
+
import fetchExtended from "<%= instancePath %>";
|
|
129
|
+
|
|
130
|
+
import { HttpClient, RequestParams, ContentType, HttpResponse } from "../@<%~ config.fileNames.httpClient %>";
|
|
131
|
+
import { DeepOmitReadOnly } from '../@types/util-types';
|
|
132
|
+
|
|
133
|
+
<% if (dataContracts.length) { %>
|
|
134
|
+
import { <%~ dataContracts.join(", ") %> } from "../@types/<%~ config.fileNames.dataContracts %>";
|
|
135
|
+
<% } %>
|
|
136
|
+
|
|
137
|
+
export class <%= apiClassName %><SecurityDataType = unknown><% if (!config.singleHttpClient) { %> extends HttpClient<SecurityDataType> <% } %> {
|
|
138
|
+
<% if (config.singleHttpClient) { %>
|
|
139
|
+
http: HttpClient<SecurityDataType>;
|
|
140
|
+
|
|
141
|
+
constructor(http: HttpClient<SecurityDataType>) {
|
|
142
|
+
this.http = http;
|
|
143
|
+
}
|
|
144
|
+
<% } %>
|
|
145
|
+
|
|
146
|
+
<% routes.forEach((route) => { %>
|
|
147
|
+
<%~ includeFile('./procedure-call.eta', { ...it, route }) %>
|
|
148
|
+
<% }) %>
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export const <%= apiInstanceName %> = new <%= apiClassName %>({ customFetch: fetchExtended });
|
|
152
|
+
|
|
153
|
+
<% if (myConfig.includeReactQuery) { %>
|
|
154
|
+
//<%~ myConfig.QUERY_HOOK_INDICATOR %>
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* QUERY_KEYS
|
|
158
|
+
*/
|
|
159
|
+
<%~ includeFile(reactQuerKeyTemplatePath, { ...it, route, dataFromApiTemplate: dataForReactHookTemplate }) %>
|
|
160
|
+
|
|
161
|
+
<% routes.forEach((route) => { %>
|
|
162
|
+
<%~ includeFile(reactQueryTemplatePath, { ...it, route, dataFromApiTemplate: dataForReactHookTemplate }) %>
|
|
163
|
+
<% }) %>
|
|
164
|
+
<% } %>
|
|
@@ -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,224 @@
|
|
|
1
|
+
<%
|
|
2
|
+
const { apiConfig, generateResponses, config } = it;
|
|
3
|
+
%>
|
|
4
|
+
|
|
5
|
+
export type QueryParamsType = Record<string | number, any>;
|
|
6
|
+
export type ResponseFormat = keyof Omit<Body, "body" | "bodyUsed">;
|
|
7
|
+
|
|
8
|
+
export interface FullRequestParams extends Omit<RequestInit, "body"> {
|
|
9
|
+
/** set parameter to `true` for call `securityWorker` for this request */
|
|
10
|
+
secure?: boolean;
|
|
11
|
+
/** request path */
|
|
12
|
+
path: string;
|
|
13
|
+
/** content type of request body */
|
|
14
|
+
type?: ContentType;
|
|
15
|
+
/** query params */
|
|
16
|
+
query?: QueryParamsType;
|
|
17
|
+
/** format of response (i.e. response.json() -> format: "json") */
|
|
18
|
+
format?: ResponseFormat;
|
|
19
|
+
/** request body */
|
|
20
|
+
body?: unknown;
|
|
21
|
+
/** base url */
|
|
22
|
+
baseUrl?: string;
|
|
23
|
+
/** request cancellation token */
|
|
24
|
+
cancelToken?: CancelToken;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type RequestParams = Omit<FullRequestParams, "body" | "method" | "query" | "path">
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
export interface ApiConfig<SecurityDataType = unknown> {
|
|
31
|
+
baseUrl?: string;
|
|
32
|
+
baseApiParams?: Omit<RequestParams, "baseUrl" | "cancelToken" | "signal">;
|
|
33
|
+
securityWorker?: (securityData: SecurityDataType | null) => Promise<RequestParams | void> | RequestParams | void;
|
|
34
|
+
customFetch?: typeof fetch;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface HttpResponse<D extends unknown, E extends unknown = unknown> extends Response {
|
|
38
|
+
data: D;
|
|
39
|
+
error: E;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type CancelToken = Symbol | string | number;
|
|
43
|
+
|
|
44
|
+
export enum ContentType {
|
|
45
|
+
Json = "application/json",
|
|
46
|
+
FormData = "multipart/form-data",
|
|
47
|
+
UrlEncoded = "application/x-www-form-urlencoded",
|
|
48
|
+
Text = "text/plain",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class HttpClient<SecurityDataType = unknown> {
|
|
52
|
+
public baseUrl: string = "<%~ apiConfig.baseUrl %>";
|
|
53
|
+
private securityData: SecurityDataType | null = null;
|
|
54
|
+
private securityWorker?: ApiConfig<SecurityDataType>["securityWorker"];
|
|
55
|
+
private abortControllers = new Map<CancelToken, AbortController>();
|
|
56
|
+
private customFetch = (...fetchParams: Parameters<typeof fetch>) => fetch(...fetchParams);
|
|
57
|
+
|
|
58
|
+
private baseApiParams: RequestParams = {
|
|
59
|
+
credentials: 'same-origin',
|
|
60
|
+
headers: {},
|
|
61
|
+
redirect: 'follow',
|
|
62
|
+
referrerPolicy: 'no-referrer',
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
constructor(apiConfig: ApiConfig<SecurityDataType> = {}) {
|
|
66
|
+
Object.assign(this, apiConfig);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public setSecurityData = (data: SecurityDataType | null) => {
|
|
70
|
+
this.securityData = data;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
protected encodeQueryParam(key: string, value: any) {
|
|
74
|
+
const encodedKey = encodeURIComponent(key);
|
|
75
|
+
return `${encodedKey}=${encodeURIComponent(typeof value === "number" ? value : `${value}`)}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
protected addQueryParam(query: QueryParamsType, key: string) {
|
|
79
|
+
return this.encodeQueryParam(key, query[key]);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
protected addArrayQueryParam(query: QueryParamsType, key: string) {
|
|
83
|
+
const value = query[key];
|
|
84
|
+
return value.map((v: any) => this.encodeQueryParam(key, v)).join("&");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
protected toQueryString(rawQuery?: QueryParamsType): string {
|
|
88
|
+
const query = rawQuery || {};
|
|
89
|
+
const keys = Object.keys(query).filter((key) => "undefined" !== typeof query[key]);
|
|
90
|
+
return keys
|
|
91
|
+
.map((key) =>
|
|
92
|
+
Array.isArray(query[key])
|
|
93
|
+
? this.addArrayQueryParam(query, key)
|
|
94
|
+
: this.addQueryParam(query, key),
|
|
95
|
+
)
|
|
96
|
+
.join("&");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
protected addQueryParams(rawQuery?: QueryParamsType): string {
|
|
100
|
+
const queryString = this.toQueryString(rawQuery);
|
|
101
|
+
return queryString ? `?${queryString}` : "";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private contentFormatters: Record<ContentType, (input: any) => any> = {
|
|
105
|
+
[ContentType.Json]: (input:any) => input !== null && (typeof input === "object" || typeof input === "string") ? JSON.stringify(input) : input,
|
|
106
|
+
[ContentType.Text]: (input:any) => input !== null && typeof input !== "string" ? JSON.stringify(input) : input,
|
|
107
|
+
[ContentType.FormData]: (input: any) =>
|
|
108
|
+
Object.keys(input || {}).reduce((formData, key) => {
|
|
109
|
+
const property = input[key];
|
|
110
|
+
formData.append(
|
|
111
|
+
key,
|
|
112
|
+
property instanceof Blob ?
|
|
113
|
+
property :
|
|
114
|
+
typeof property === "object" && property !== null ?
|
|
115
|
+
JSON.stringify(property) :
|
|
116
|
+
`${property}`
|
|
117
|
+
);
|
|
118
|
+
return formData;
|
|
119
|
+
}, new FormData()),
|
|
120
|
+
[ContentType.UrlEncoded]: (input: any) => this.toQueryString(input),
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
protected mergeRequestParams(params1: RequestParams, params2?: RequestParams): RequestParams {
|
|
124
|
+
return {
|
|
125
|
+
...this.baseApiParams,
|
|
126
|
+
...params1,
|
|
127
|
+
...(params2 || {}),
|
|
128
|
+
headers: {
|
|
129
|
+
...(this.baseApiParams.headers || {}),
|
|
130
|
+
...(params1.headers || {}),
|
|
131
|
+
...((params2 && params2.headers) || {}),
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
protected createAbortSignal = (cancelToken: CancelToken): AbortSignal | undefined => {
|
|
137
|
+
if (this.abortControllers.has(cancelToken)) {
|
|
138
|
+
const abortController = this.abortControllers.get(cancelToken);
|
|
139
|
+
if (abortController) {
|
|
140
|
+
return abortController.signal;
|
|
141
|
+
}
|
|
142
|
+
return void 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const abortController = new AbortController();
|
|
146
|
+
this.abortControllers.set(cancelToken, abortController);
|
|
147
|
+
return abortController.signal;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
public abortRequest = (cancelToken: CancelToken) => {
|
|
151
|
+
const abortController = this.abortControllers.get(cancelToken)
|
|
152
|
+
|
|
153
|
+
if (abortController) {
|
|
154
|
+
abortController.abort();
|
|
155
|
+
this.abortControllers.delete(cancelToken);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
public request = async <T = any, E = any>({
|
|
160
|
+
body,
|
|
161
|
+
secure,
|
|
162
|
+
path,
|
|
163
|
+
type,
|
|
164
|
+
query,
|
|
165
|
+
format,
|
|
166
|
+
baseUrl,
|
|
167
|
+
cancelToken,
|
|
168
|
+
...params
|
|
169
|
+
<% if (config.unwrapResponseData) { %>
|
|
170
|
+
}: FullRequestParams): Promise<T> => {
|
|
171
|
+
<% } else { %>
|
|
172
|
+
}: FullRequestParams): Promise<HttpResponse<T, E>> => {
|
|
173
|
+
<% } %>
|
|
174
|
+
const secureParams = ((typeof secure === 'boolean' ? secure : this.baseApiParams.secure) && this.securityWorker && await this.securityWorker(this.securityData)) || {};
|
|
175
|
+
const requestParams = this.mergeRequestParams(params, secureParams);
|
|
176
|
+
const queryString = query && this.toQueryString(query);
|
|
177
|
+
const payloadFormatter = this.contentFormatters[type || ContentType.Json];
|
|
178
|
+
const responseFormat = format || requestParams.format;
|
|
179
|
+
|
|
180
|
+
return this.customFetch(
|
|
181
|
+
`${baseUrl || this.baseUrl || ""}${path}${queryString ? `?${queryString}` : ""}`,
|
|
182
|
+
{
|
|
183
|
+
...requestParams,
|
|
184
|
+
headers: {
|
|
185
|
+
...(requestParams.headers || {}),
|
|
186
|
+
...(type && type !== ContentType.FormData ? { "Content-Type": type } : {}),
|
|
187
|
+
},
|
|
188
|
+
signal: cancelToken ? this.createAbortSignal(cancelToken) : requestParams.signal,
|
|
189
|
+
body: typeof body === "undefined" || body === null ? null : payloadFormatter(body),
|
|
190
|
+
}
|
|
191
|
+
).then(async (response) => {
|
|
192
|
+
const r = response as HttpResponse<T, E>;
|
|
193
|
+
r.data = (null as unknown) as T;
|
|
194
|
+
r.error = (null as unknown) as E;
|
|
195
|
+
|
|
196
|
+
const data = !responseFormat ? r : await response[responseFormat]()
|
|
197
|
+
.then((data) => {
|
|
198
|
+
if (r.ok) {
|
|
199
|
+
r.data = data;
|
|
200
|
+
} else {
|
|
201
|
+
r.error = data;
|
|
202
|
+
}
|
|
203
|
+
return r;
|
|
204
|
+
})
|
|
205
|
+
.catch((e) => {
|
|
206
|
+
r.error = e;
|
|
207
|
+
return r;
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
if (cancelToken) {
|
|
211
|
+
this.abortControllers.delete(cancelToken);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
<% if (!config.disableThrowOnError) { %>
|
|
215
|
+
if (!response.ok) throw data;
|
|
216
|
+
<% } %>
|
|
217
|
+
<% if (config.unwrapResponseData) { %>
|
|
218
|
+
return data.data;
|
|
219
|
+
<% } else { %>
|
|
220
|
+
return data;
|
|
221
|
+
<% } %>
|
|
222
|
+
});
|
|
223
|
+
};
|
|
224
|
+
}
|