@gromlab/api-codegen 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +114 -0
- package/dist/biome_wasm_bg-2mbeh8m8.wasm +0 -0
- package/dist/cli.js +207941 -0
- package/dist/templates/api.ejs +73 -0
- package/dist/templates/data-contract-jsdoc.ejs +37 -0
- package/dist/templates/data-contracts.ejs +40 -0
- package/dist/templates/enum-data-contract.ejs +18 -0
- package/dist/templates/http-client.ejs +250 -0
- package/dist/templates/interface-data-contract.ejs +10 -0
- package/dist/templates/object-field-jsdoc.ejs +28 -0
- package/dist/templates/procedure-call.ejs +140 -0
- package/dist/templates/route-docs.ejs +30 -0
- package/dist/templates/route-name.ejs +43 -0
- package/dist/templates/route-type.ejs +24 -0
- package/dist/templates/route-types.ejs +32 -0
- package/dist/templates/type-data-contract.ejs +15 -0
- package/package.json +61 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<%
|
|
2
|
+
const { apiConfig, routes, utils, config } = it;
|
|
3
|
+
const { info, servers, externalDocs } = apiConfig;
|
|
4
|
+
const { _, require, formatDescription } = utils;
|
|
5
|
+
|
|
6
|
+
const server = (servers && servers[0]) || { url: "" };
|
|
7
|
+
|
|
8
|
+
const descriptionLines = _.compact([
|
|
9
|
+
`@title ${info.title || "No title"}`,
|
|
10
|
+
info.version && `@version ${info.version}`,
|
|
11
|
+
info.license && `@license ${_.compact([
|
|
12
|
+
info.license.name,
|
|
13
|
+
info.license.url && `(${info.license.url})`,
|
|
14
|
+
]).join(" ")}`,
|
|
15
|
+
info.termsOfService && `@termsOfService ${info.termsOfService}`,
|
|
16
|
+
server.url && `@baseUrl ${server.url}`,
|
|
17
|
+
externalDocs.url && `@externalDocs ${externalDocs.url}`,
|
|
18
|
+
info.contact && `@contact ${_.compact([
|
|
19
|
+
info.contact.name,
|
|
20
|
+
info.contact.email && `<${info.contact.email}>`,
|
|
21
|
+
info.contact.url && `(${info.contact.url})`,
|
|
22
|
+
]).join(" ")}`,
|
|
23
|
+
info.description && " ",
|
|
24
|
+
info.description && _.replace(formatDescription(info.description), /\n/g, "\n * "),
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
%>
|
|
28
|
+
|
|
29
|
+
<% if (config.useSwr) { %>
|
|
30
|
+
import useSWR from "swr";
|
|
31
|
+
<% } %>
|
|
32
|
+
|
|
33
|
+
<% if (config.httpClientType === config.constants.HTTP_CLIENT.AXIOS) { %> import type { AxiosRequestConfig, AxiosResponse } from "axios"; <% } %>
|
|
34
|
+
|
|
35
|
+
<% if (descriptionLines.length) { %>
|
|
36
|
+
/**
|
|
37
|
+
<% descriptionLines.forEach((descriptionLine) => { %>
|
|
38
|
+
* <%~ descriptionLine %>
|
|
39
|
+
|
|
40
|
+
<% }) %>
|
|
41
|
+
*/
|
|
42
|
+
<% } %>
|
|
43
|
+
export class <%~ config.apiClassName %><SecurityDataType extends unknown><% if (!config.singleHttpClient) { %> extends HttpClient<SecurityDataType> <% } %> {
|
|
44
|
+
|
|
45
|
+
<% if(config.singleHttpClient) { %>
|
|
46
|
+
http: HttpClient<SecurityDataType>;
|
|
47
|
+
|
|
48
|
+
constructor (http?: HttpClient<SecurityDataType>) {
|
|
49
|
+
this.http = http || new HttpClient<SecurityDataType>();
|
|
50
|
+
}
|
|
51
|
+
<% } %>
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
<% if (routes.outOfModule) { %>
|
|
55
|
+
<% for (const route of routes.outOfModule) { %>
|
|
56
|
+
|
|
57
|
+
<%~ includeFile('./procedure-call.ejs', { ...it, route }) %>
|
|
58
|
+
|
|
59
|
+
<% } %>
|
|
60
|
+
<% } %>
|
|
61
|
+
|
|
62
|
+
<% if (routes.combined) { %>
|
|
63
|
+
<% for (const { routes: combinedRoutes = [], moduleName } of routes.combined) { %>
|
|
64
|
+
<%~ moduleName %> = {
|
|
65
|
+
<% for (const route of combinedRoutes) { %>
|
|
66
|
+
|
|
67
|
+
<%~ includeFile('./procedure-call.ejs', { ...it, route }) %>
|
|
68
|
+
|
|
69
|
+
<% } %>
|
|
70
|
+
}
|
|
71
|
+
<% } %>
|
|
72
|
+
<% } %>
|
|
73
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<%
|
|
2
|
+
const { data, utils } = it;
|
|
3
|
+
const { formatDescription, require, _ } = utils;
|
|
4
|
+
|
|
5
|
+
const stringify = (value) => _.isObject(value) ? JSON.stringify(value) : _.isString(value) ? `"${value}"` : value;
|
|
6
|
+
|
|
7
|
+
const jsDocLines = _.compact([
|
|
8
|
+
data.title,
|
|
9
|
+
data.description && formatDescription(data.description),
|
|
10
|
+
!_.isUndefined(data.deprecated) && data.deprecated && '@deprecated',
|
|
11
|
+
!_.isUndefined(data.format) && `@format ${data.format}`,
|
|
12
|
+
!_.isUndefined(data.minimum) && `@min ${data.minimum}`,
|
|
13
|
+
!_.isUndefined(data.multipleOf) && `@multipleOf ${data.multipleOf}`,
|
|
14
|
+
!_.isUndefined(data.exclusiveMinimum) && `@exclusiveMin ${data.exclusiveMinimum}`,
|
|
15
|
+
!_.isUndefined(data.maximum) && `@max ${data.maximum}`,
|
|
16
|
+
!_.isUndefined(data.minLength) && `@minLength ${data.minLength}`,
|
|
17
|
+
!_.isUndefined(data.maxLength) && `@maxLength ${data.maxLength}`,
|
|
18
|
+
!_.isUndefined(data.exclusiveMaximum) && `@exclusiveMax ${data.exclusiveMaximum}`,
|
|
19
|
+
!_.isUndefined(data.maxItems) && `@maxItems ${data.maxItems}`,
|
|
20
|
+
!_.isUndefined(data.minItems) && `@minItems ${data.minItems}`,
|
|
21
|
+
!_.isUndefined(data.uniqueItems) && `@uniqueItems ${data.uniqueItems}`,
|
|
22
|
+
!_.isUndefined(data.default) && `@default ${stringify(data.default)}`,
|
|
23
|
+
!_.isUndefined(data.pattern) && `@pattern ${data.pattern}`,
|
|
24
|
+
!_.isUndefined(data.example) && `@example ${stringify(data.example)}`
|
|
25
|
+
]).join('\n').split('\n');
|
|
26
|
+
%>
|
|
27
|
+
<% if (jsDocLines.every(_.isEmpty)) { %>
|
|
28
|
+
<% } else if (jsDocLines.length === 1) { %>
|
|
29
|
+
/** <%~ jsDocLines[0] %> */
|
|
30
|
+
<% } else if (jsDocLines.length) { %>
|
|
31
|
+
/**
|
|
32
|
+
<% for (jsDocLine of jsDocLines) { %>
|
|
33
|
+
* <%~ jsDocLine %>
|
|
34
|
+
|
|
35
|
+
<% } %>
|
|
36
|
+
*/
|
|
37
|
+
<% } %>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<%
|
|
2
|
+
const { modelTypes, utils, config } = it;
|
|
3
|
+
const { formatDescription, require, _, Ts } = utils;
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
const buildGenerics = (contract) => {
|
|
7
|
+
if (!contract.genericArgs || !contract.genericArgs.length) return '';
|
|
8
|
+
|
|
9
|
+
return '<' + contract.genericArgs.map(({ name, default: defaultType, extends: extendsType }) => {
|
|
10
|
+
return [
|
|
11
|
+
name,
|
|
12
|
+
extendsType && `extends ${extendsType}`,
|
|
13
|
+
defaultType && `= ${defaultType}`,
|
|
14
|
+
].join('')
|
|
15
|
+
}).join(',') + '>'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const dataContractTemplates = {
|
|
19
|
+
enum: (contract) => {
|
|
20
|
+
return `enum ${contract.name} {\r\n${contract.content} \r\n }`;
|
|
21
|
+
},
|
|
22
|
+
interface: (contract) => {
|
|
23
|
+
return `interface ${contract.name}${buildGenerics(contract)} {\r\n${contract.content}}`;
|
|
24
|
+
},
|
|
25
|
+
type: (contract) => {
|
|
26
|
+
return `type ${contract.name}${buildGenerics(contract)} = ${contract.content}`;
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
%>
|
|
30
|
+
|
|
31
|
+
<% if (config.internalTemplateOptions.addUtilRequiredKeysType) { %>
|
|
32
|
+
type <%~ config.Ts.CodeGenKeyword.UtilRequiredKeys %><T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>
|
|
33
|
+
<% } %>
|
|
34
|
+
|
|
35
|
+
<% for (const contract of modelTypes) { %>
|
|
36
|
+
<%~ includeFile('./data-contract-jsdoc.ejs', { ...it, data: { ...contract, ...contract.typeData } }) %>
|
|
37
|
+
<%~ contract.internal ? '' : 'export'%> <%~ (dataContractTemplates[contract.typeIdentifier] || dataContractTemplates.type)(contract) %>
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
<% } %>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<%
|
|
2
|
+
const { contract, utils, config } = it;
|
|
3
|
+
const { formatDescription, require, _ } = utils;
|
|
4
|
+
const { name, $content } = contract;
|
|
5
|
+
%>
|
|
6
|
+
<% if (config.generateUnionEnums) { %>
|
|
7
|
+
export type <%~ name %> = <%~ _.map($content, ({ value }) => value).join(" | ") %>
|
|
8
|
+
<% } else { %>
|
|
9
|
+
export enum <%~ name %> {
|
|
10
|
+
<%~ _.map($content, ({ key, value, description }) => {
|
|
11
|
+
let formattedDescription = description && formatDescription(description, true);
|
|
12
|
+
return [
|
|
13
|
+
formattedDescription && `/** ${formattedDescription} */`,
|
|
14
|
+
`${key} = ${value}`
|
|
15
|
+
].filter(Boolean).join("\n");
|
|
16
|
+
}).join(",\n") %>
|
|
17
|
+
}
|
|
18
|
+
<% } %>
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
<%
|
|
2
|
+
const { apiConfig, generateResponses, config } = it;
|
|
3
|
+
const baseUrl = apiConfig?.baseUrl || "";
|
|
4
|
+
%>
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Фетчер для SWR
|
|
8
|
+
* Принимает URL и возвращает Promise с данными
|
|
9
|
+
*/
|
|
10
|
+
export const fetcher = <T = any>(url: string): Promise<T> => {
|
|
11
|
+
return fetch(url, {
|
|
12
|
+
headers: {
|
|
13
|
+
"Content-Type": "application/json",
|
|
14
|
+
},
|
|
15
|
+
}).then(res => {
|
|
16
|
+
if (!res.ok) {
|
|
17
|
+
throw new Error(`HTTP Error ${res.status}`);
|
|
18
|
+
}
|
|
19
|
+
return res.json();
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type QueryParamsType = Record<string | number, any>;
|
|
24
|
+
export type ResponseFormat = keyof Omit<Body, "body" | "bodyUsed">;
|
|
25
|
+
|
|
26
|
+
export interface FullRequestParams extends Omit<RequestInit, "body"> {
|
|
27
|
+
/** set parameter to `true` for call `securityWorker` for this request */
|
|
28
|
+
secure?: boolean;
|
|
29
|
+
/** request path */
|
|
30
|
+
path: string;
|
|
31
|
+
/** content type of request body */
|
|
32
|
+
type?: ContentType;
|
|
33
|
+
/** query params */
|
|
34
|
+
query?: QueryParamsType;
|
|
35
|
+
/** format of response (i.e. response.json() -> format: "json") */
|
|
36
|
+
format?: ResponseFormat;
|
|
37
|
+
/** request body */
|
|
38
|
+
body?: unknown;
|
|
39
|
+
/** base url */
|
|
40
|
+
baseUrl?: string;
|
|
41
|
+
/** request cancellation token */
|
|
42
|
+
cancelToken?: CancelToken;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type RequestParams = Omit<FullRequestParams, "body" | "method" | "query" | "path">
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
export interface ApiConfig<SecurityDataType = unknown> {
|
|
49
|
+
baseUrl?: string;
|
|
50
|
+
baseApiParams?: Omit<RequestParams, "baseUrl" | "cancelToken" | "signal">;
|
|
51
|
+
securityWorker?: (securityData: SecurityDataType | null) => Promise<RequestParams | void> | RequestParams | void;
|
|
52
|
+
customFetch?: typeof fetch;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface HttpResponse<D extends unknown, E extends unknown = unknown> extends Response {
|
|
56
|
+
data: D;
|
|
57
|
+
error: E;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
type CancelToken = Symbol | string | number;
|
|
61
|
+
|
|
62
|
+
export enum ContentType {
|
|
63
|
+
Json = "application/json",
|
|
64
|
+
JsonApi = "application/vnd.api+json",
|
|
65
|
+
FormData = "multipart/form-data",
|
|
66
|
+
UrlEncoded = "application/x-www-form-urlencoded",
|
|
67
|
+
Text = "text/plain",
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class HttpClient<SecurityDataType = unknown> {
|
|
71
|
+
public baseUrl: string = "<%~ baseUrl %>";
|
|
72
|
+
private securityData: SecurityDataType | null = null;
|
|
73
|
+
private securityWorker?: ApiConfig<SecurityDataType>["securityWorker"];
|
|
74
|
+
private abortControllers = new Map<CancelToken, AbortController>();
|
|
75
|
+
private customFetch = (...fetchParams: Parameters<typeof fetch>) => fetch(...fetchParams);
|
|
76
|
+
|
|
77
|
+
private baseApiParams: RequestParams = {
|
|
78
|
+
credentials: 'same-origin',
|
|
79
|
+
headers: {},
|
|
80
|
+
redirect: 'follow',
|
|
81
|
+
referrerPolicy: 'no-referrer',
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
constructor(apiConfig: ApiConfig<SecurityDataType> = {}) {
|
|
85
|
+
Object.assign(this, apiConfig);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
public setSecurityData = (data: SecurityDataType | null) => {
|
|
89
|
+
this.securityData = data;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
protected encodeQueryParam(key: string, value: any) {
|
|
93
|
+
const encodedKey = encodeURIComponent(key);
|
|
94
|
+
return `${encodedKey}=${encodeURIComponent(typeof value === "number" ? value : `${value}`)}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
protected addQueryParam(query: QueryParamsType, key: string) {
|
|
98
|
+
return this.encodeQueryParam(key, query[key]);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
protected addArrayQueryParam(query: QueryParamsType, key: string) {
|
|
102
|
+
const value = query[key];
|
|
103
|
+
return value.map((v: any) => this.encodeQueryParam(key, v)).join("&");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
protected toQueryString(rawQuery?: QueryParamsType): string {
|
|
107
|
+
const query = rawQuery || {};
|
|
108
|
+
const keys = Object.keys(query).filter((key) => "undefined" !== typeof query[key]);
|
|
109
|
+
return keys
|
|
110
|
+
.map((key) =>
|
|
111
|
+
Array.isArray(query[key])
|
|
112
|
+
? this.addArrayQueryParam(query, key)
|
|
113
|
+
: this.addQueryParam(query, key),
|
|
114
|
+
)
|
|
115
|
+
.join("&");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
protected addQueryParams(rawQuery?: QueryParamsType): string {
|
|
119
|
+
const queryString = this.toQueryString(rawQuery);
|
|
120
|
+
return queryString ? `?${queryString}` : "";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private contentFormatters: Record<ContentType, (input: any) => any> = {
|
|
124
|
+
[ContentType.Json]: (input:any) => input !== null && (typeof input === "object" || typeof input === "string") ? JSON.stringify(input) : input,
|
|
125
|
+
[ContentType.JsonApi]: (input:any) => input !== null && (typeof input === "object" || typeof input === "string") ? JSON.stringify(input) : input,
|
|
126
|
+
[ContentType.Text]: (input:any) => input !== null && typeof input !== "string" ? JSON.stringify(input) : input,
|
|
127
|
+
[ContentType.FormData]: (input: any) => {
|
|
128
|
+
if (input instanceof FormData) {
|
|
129
|
+
return input;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return Object.keys(input || {}).reduce((formData, key) => {
|
|
133
|
+
const property = input[key];
|
|
134
|
+
formData.append(
|
|
135
|
+
key,
|
|
136
|
+
property instanceof Blob ?
|
|
137
|
+
property :
|
|
138
|
+
typeof property === "object" && property !== null ?
|
|
139
|
+
JSON.stringify(property) :
|
|
140
|
+
`${property}`
|
|
141
|
+
);
|
|
142
|
+
return formData;
|
|
143
|
+
}, new FormData());
|
|
144
|
+
},
|
|
145
|
+
[ContentType.UrlEncoded]: (input: any) => this.toQueryString(input),
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
protected mergeRequestParams(params1: RequestParams, params2?: RequestParams): RequestParams {
|
|
149
|
+
return {
|
|
150
|
+
...this.baseApiParams,
|
|
151
|
+
...params1,
|
|
152
|
+
...(params2 || {}),
|
|
153
|
+
headers: {
|
|
154
|
+
...(this.baseApiParams.headers || {}),
|
|
155
|
+
...(params1.headers || {}),
|
|
156
|
+
...((params2 && params2.headers) || {}),
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
protected createAbortSignal = (cancelToken: CancelToken): AbortSignal | undefined => {
|
|
162
|
+
if (this.abortControllers.has(cancelToken)) {
|
|
163
|
+
const abortController = this.abortControllers.get(cancelToken);
|
|
164
|
+
if (abortController) {
|
|
165
|
+
return abortController.signal;
|
|
166
|
+
}
|
|
167
|
+
return void 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const abortController = new AbortController();
|
|
171
|
+
this.abortControllers.set(cancelToken, abortController);
|
|
172
|
+
return abortController.signal;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
public abortRequest = (cancelToken: CancelToken) => {
|
|
176
|
+
const abortController = this.abortControllers.get(cancelToken)
|
|
177
|
+
|
|
178
|
+
if (abortController) {
|
|
179
|
+
abortController.abort();
|
|
180
|
+
this.abortControllers.delete(cancelToken);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
public request = async <T = any, E = any>({
|
|
185
|
+
body,
|
|
186
|
+
secure,
|
|
187
|
+
path,
|
|
188
|
+
type,
|
|
189
|
+
query,
|
|
190
|
+
format,
|
|
191
|
+
baseUrl,
|
|
192
|
+
cancelToken,
|
|
193
|
+
...params
|
|
194
|
+
<% if (config.unwrapResponseData) { %>
|
|
195
|
+
}: FullRequestParams): Promise<T> => {
|
|
196
|
+
<% } else { %>
|
|
197
|
+
}: FullRequestParams): Promise<HttpResponse<T, E>> => {
|
|
198
|
+
<% } %>
|
|
199
|
+
const secureParams = ((typeof secure === 'boolean' ? secure : this.baseApiParams.secure) && this.securityWorker && await this.securityWorker(this.securityData)) || {};
|
|
200
|
+
const requestParams = this.mergeRequestParams(params, secureParams);
|
|
201
|
+
const queryString = query && this.toQueryString(query);
|
|
202
|
+
const payloadFormatter = this.contentFormatters[type || ContentType.Json];
|
|
203
|
+
const responseFormat = format || requestParams.format;
|
|
204
|
+
|
|
205
|
+
return this.customFetch(
|
|
206
|
+
`${baseUrl || this.baseUrl || ""}${path}${queryString ? `?${queryString}` : ""}`,
|
|
207
|
+
{
|
|
208
|
+
...requestParams,
|
|
209
|
+
headers: {
|
|
210
|
+
...(requestParams.headers || {}),
|
|
211
|
+
...(type && type !== ContentType.FormData ? { "Content-Type": type } : {}),
|
|
212
|
+
},
|
|
213
|
+
signal: (cancelToken ? this.createAbortSignal(cancelToken) : requestParams.signal) || null,
|
|
214
|
+
body: typeof body === "undefined" || body === null ? null : payloadFormatter(body),
|
|
215
|
+
}
|
|
216
|
+
).then(async (response) => {
|
|
217
|
+
const r = response as HttpResponse<T, E>;
|
|
218
|
+
r.data = (null as unknown) as T;
|
|
219
|
+
r.error = (null as unknown) as E;
|
|
220
|
+
|
|
221
|
+
const responseToParse = responseFormat ? response.clone() : response;
|
|
222
|
+
const data = !responseFormat ? r : await responseToParse[responseFormat]()
|
|
223
|
+
.then((data) => {
|
|
224
|
+
if (r.ok) {
|
|
225
|
+
r.data = data;
|
|
226
|
+
} else {
|
|
227
|
+
r.error = data;
|
|
228
|
+
}
|
|
229
|
+
return r;
|
|
230
|
+
})
|
|
231
|
+
.catch((e) => {
|
|
232
|
+
r.error = e;
|
|
233
|
+
return r;
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
if (cancelToken) {
|
|
237
|
+
this.abortControllers.delete(cancelToken);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
<% if (!config.disableThrowOnError) { %>
|
|
241
|
+
if (!response.ok) throw data;
|
|
242
|
+
<% } %>
|
|
243
|
+
<% if (config.unwrapResponseData) { %>
|
|
244
|
+
return data.data;
|
|
245
|
+
<% } else { %>
|
|
246
|
+
return data;
|
|
247
|
+
<% } %>
|
|
248
|
+
});
|
|
249
|
+
};
|
|
250
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<%
|
|
2
|
+
const { contract, utils } = it;
|
|
3
|
+
const { formatDescription, require, _ } = utils;
|
|
4
|
+
%>
|
|
5
|
+
export interface <%~ contract.name %> {
|
|
6
|
+
<% for (const field of contract.$content) { %>
|
|
7
|
+
<%~ includeFile('./object-field-jsdoc.ejs', { ...it, field }) %>
|
|
8
|
+
<%~ field.name %><%~ field.isRequired ? '' : '?' %>: <%~ field.value %><%~ field.isNullable ? ' | null' : ''%>;
|
|
9
|
+
<% } %>
|
|
10
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<%
|
|
2
|
+
const { field, utils } = it;
|
|
3
|
+
const { formatDescription, require, _ } = utils;
|
|
4
|
+
|
|
5
|
+
const comments = _.uniq(
|
|
6
|
+
_.compact([
|
|
7
|
+
field.title,
|
|
8
|
+
field.description,
|
|
9
|
+
field.deprecated && ` * @deprecated`,
|
|
10
|
+
!_.isUndefined(field.format) && `@format ${field.format}`,
|
|
11
|
+
!_.isUndefined(field.minimum) && `@min ${field.minimum}`,
|
|
12
|
+
!_.isUndefined(field.maximum) && `@max ${field.maximum}`,
|
|
13
|
+
!_.isUndefined(field.pattern) && `@pattern ${field.pattern}`,
|
|
14
|
+
!_.isUndefined(field.example) &&
|
|
15
|
+
`@example ${_.isObject(field.example) ? JSON.stringify(field.example) : field.example}`,
|
|
16
|
+
]).reduce((acc, comment) => [...acc, ...comment.split(/\n/g)], []),
|
|
17
|
+
);
|
|
18
|
+
%>
|
|
19
|
+
<% if (comments.length === 1) { %>
|
|
20
|
+
/** <%~ comments[0] %> */
|
|
21
|
+
<% } else if (comments.length) { %>
|
|
22
|
+
/**
|
|
23
|
+
<% comments.forEach(comment => { %>
|
|
24
|
+
* <%~ comment %>
|
|
25
|
+
|
|
26
|
+
<% }) %>
|
|
27
|
+
*/
|
|
28
|
+
<% } %>
|
|
@@ -0,0 +1,140 @@
|
|
|
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("./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
|
+
const isFetchTemplate = config.httpClientType === HTTP_CLIENT.FETCH;
|
|
14
|
+
|
|
15
|
+
const requestConfigParam = {
|
|
16
|
+
name: specificArgNameResolver.resolve(RESERVED_REQ_PARAMS_ARG_NAMES),
|
|
17
|
+
optional: true,
|
|
18
|
+
type: "RequestParams",
|
|
19
|
+
defaultValue: "{}",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const argToTmpl = ({ name, optional, type, defaultValue }) => `${name}${!defaultValue && optional ? '?' : ''}: ${type}${defaultValue ? ` = ${defaultValue}` : ''}`;
|
|
23
|
+
|
|
24
|
+
const rawWrapperArgs = config.extractRequestParams ?
|
|
25
|
+
_.compact([
|
|
26
|
+
requestParams && {
|
|
27
|
+
name: pathParams.length ? `{ ${_.join(pathParamsNames, ", ")}, ...${queryName} }` : queryName,
|
|
28
|
+
optional: false,
|
|
29
|
+
type: getInlineParseContent(requestParams),
|
|
30
|
+
},
|
|
31
|
+
...(!requestParams ? pathParams : []),
|
|
32
|
+
payload,
|
|
33
|
+
requestConfigParam,
|
|
34
|
+
]) :
|
|
35
|
+
_.compact([
|
|
36
|
+
...pathParams,
|
|
37
|
+
query,
|
|
38
|
+
payload,
|
|
39
|
+
requestConfigParam,
|
|
40
|
+
])
|
|
41
|
+
|
|
42
|
+
const wrapperArgs = _
|
|
43
|
+
// Sort by optionality
|
|
44
|
+
.sortBy(rawWrapperArgs, [o => o.optional])
|
|
45
|
+
.map(argToTmpl)
|
|
46
|
+
.join(', ')
|
|
47
|
+
|
|
48
|
+
// RequestParams["type"]
|
|
49
|
+
const requestContentKind = {
|
|
50
|
+
"JSON": "ContentType.Json",
|
|
51
|
+
"JSON_API": "ContentType.JsonApi",
|
|
52
|
+
"URL_ENCODED": "ContentType.UrlEncoded",
|
|
53
|
+
"FORM_DATA": "ContentType.FormData",
|
|
54
|
+
"TEXT": "ContentType.Text",
|
|
55
|
+
}
|
|
56
|
+
// RequestParams["format"]
|
|
57
|
+
const responseContentKind = {
|
|
58
|
+
"JSON": '"json"',
|
|
59
|
+
"IMAGE": '"blob"',
|
|
60
|
+
"FORM_DATA": isFetchTemplate ? '"formData"' : '"document"'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const bodyTmpl = _.get(payload, "name") || null;
|
|
64
|
+
const queryTmpl = (query != null && queryName) || null;
|
|
65
|
+
const bodyContentKindTmpl = requestContentKind[requestBodyInfo.contentKind] || null;
|
|
66
|
+
const responseFormatTmpl = responseContentKind[responseBodyInfo.success && responseBodyInfo.success.schema && responseBodyInfo.success.schema.contentKind] || null;
|
|
67
|
+
const securityTmpl = security ? 'true' : null;
|
|
68
|
+
|
|
69
|
+
const describeReturnType = () => {
|
|
70
|
+
if (!config.toJS) return "";
|
|
71
|
+
|
|
72
|
+
switch(config.httpClientType) {
|
|
73
|
+
case HTTP_CLIENT.AXIOS: {
|
|
74
|
+
return `Promise<AxiosResponse<${type}>>`
|
|
75
|
+
}
|
|
76
|
+
default: {
|
|
77
|
+
return `Promise<HttpResponse<${type}, ${errorType}>`
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const isValidIdentifier = (name) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name);
|
|
83
|
+
|
|
84
|
+
%>
|
|
85
|
+
/**
|
|
86
|
+
<%~ routeDocs.description %>
|
|
87
|
+
|
|
88
|
+
*<% /* Here you can add some other JSDoc tags */ %>
|
|
89
|
+
|
|
90
|
+
<%~ routeDocs.lines %>
|
|
91
|
+
|
|
92
|
+
*/
|
|
93
|
+
<% if (isValidIdentifier(route.routeName.usage)) { %><%~ route.routeName.usage %><%~ route.namespace ? ': ' : ' = ' %><% } else { %>"<%~ route.routeName.usage %>"<%~ route.namespace ? ': ' : ' = ' %><% } %>(<%~ wrapperArgs %>)<%~ config.toJS ? `: ${describeReturnType()}` : "" %> =>
|
|
94
|
+
<%~ config.singleHttpClient ? 'this.http.request' : 'this.request' %><<%~ type %>, <%~ errorType %>>({
|
|
95
|
+
path: `<%~ path %>`,
|
|
96
|
+
method: '<%~ _.upperCase(method) %>',
|
|
97
|
+
<%~ queryTmpl ? `query: ${queryTmpl},` : '' %>
|
|
98
|
+
<%~ bodyTmpl ? `body: ${bodyTmpl},` : '' %>
|
|
99
|
+
<%~ securityTmpl ? `secure: ${securityTmpl},` : '' %>
|
|
100
|
+
<%~ bodyContentKindTmpl ? `type: ${bodyContentKindTmpl},` : '' %>
|
|
101
|
+
<%~ responseFormatTmpl ? `format: ${responseFormatTmpl},` : '' %>
|
|
102
|
+
...<%~ _.get(requestConfigParam, "name") %>,
|
|
103
|
+
})<%~ route.namespace ? ',' : '' %>
|
|
104
|
+
<%
|
|
105
|
+
// Генерируем use* функцию для GET запросов (только если включен флаг useSwr)
|
|
106
|
+
const isGetRequest = _.upperCase(method) === 'GET';
|
|
107
|
+
if (config.useSwr && isGetRequest) {
|
|
108
|
+
const useMethodName = 'use' + _.upperFirst(route.routeName.usage);
|
|
109
|
+
const argsWithoutParams = rawWrapperArgs.filter(arg => arg.name !== requestConfigParam.name);
|
|
110
|
+
const useWrapperArgs = _
|
|
111
|
+
.sortBy(argsWithoutParams, [o => o.optional])
|
|
112
|
+
.map(argToTmpl)
|
|
113
|
+
.join(', ');
|
|
114
|
+
|
|
115
|
+
// Определяем обязательные параметры для проверки
|
|
116
|
+
const requiredArgs = argsWithoutParams.filter(arg => !arg.optional);
|
|
117
|
+
const requiredArgsNames = requiredArgs.map(arg => {
|
|
118
|
+
// Извлекаем имя из деструктуризации типа "{ id, ...query }"
|
|
119
|
+
const match = arg.name.match(/^\{\s*([^,}]+)/);
|
|
120
|
+
return match ? match[1].trim() : arg.name;
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Генерируем условие для проверки всех обязательных параметров
|
|
124
|
+
const hasRequiredArgs = requiredArgsNames.length > 0;
|
|
125
|
+
const conditionCheck = hasRequiredArgs
|
|
126
|
+
? requiredArgsNames.join(' && ')
|
|
127
|
+
: 'true';
|
|
128
|
+
%>
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* SWR hook для <%~ route.routeName.usage %>
|
|
132
|
+
<%~ routeDocs.lines %>
|
|
133
|
+
*/
|
|
134
|
+
<% if (isValidIdentifier(useMethodName)) { %><%~ useMethodName %><%~ route.namespace ? ': ' : ' = ' %><% } else { %>"<%~ useMethodName %>"<%~ route.namespace ? ': ' : ' = ' %><% } %>(<%~ useWrapperArgs %>) => {
|
|
135
|
+
return useSWR<<%~ type %>>(
|
|
136
|
+
<%~ conditionCheck %> ? `<%~ path %>` : null,
|
|
137
|
+
fetcher
|
|
138
|
+
);
|
|
139
|
+
}<%~ route.namespace ? ',' : '' %>
|
|
140
|
+
<% } %>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<%
|
|
2
|
+
const { config, route, utils } = it;
|
|
3
|
+
const { _, formatDescription, fmtToJSDocLine, pascalCase, require } = utils;
|
|
4
|
+
const { raw, request, routeName } = route;
|
|
5
|
+
|
|
6
|
+
const jsDocDescription = raw.description ?
|
|
7
|
+
` * @description ${formatDescription(raw.description, true)}` :
|
|
8
|
+
fmtToJSDocLine('No description', { eol: false });
|
|
9
|
+
const jsDocLines = _.compact([
|
|
10
|
+
_.size(raw.tags) && ` * @tags ${raw.tags.join(", ")}`,
|
|
11
|
+
` * @name ${pascalCase(routeName.usage)}`,
|
|
12
|
+
raw.summary && ` * @summary ${raw.summary}`,
|
|
13
|
+
` * @request ${_.upperCase(request.method)}:${raw.route}`,
|
|
14
|
+
raw.deprecated && ` * @deprecated`,
|
|
15
|
+
routeName.duplicate && ` * @originalName ${routeName.original}`,
|
|
16
|
+
routeName.duplicate && ` * @duplicate`,
|
|
17
|
+
request.security && ` * @secure`,
|
|
18
|
+
...(config.generateResponses && raw.responsesTypes.length
|
|
19
|
+
? raw.responsesTypes.map(
|
|
20
|
+
({ type, status, description, isSuccess }) =>
|
|
21
|
+
` * @response \`${status}\` \`${_.replace(_.replace(type, /\/\*/g, "\\*"), /\*\//g, "*\\")}\` ${description}`,
|
|
22
|
+
)
|
|
23
|
+
: []),
|
|
24
|
+
]).map(str => str.trimEnd()).join("\n");
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
description: jsDocDescription,
|
|
28
|
+
lines: jsDocLines,
|
|
29
|
+
}
|
|
30
|
+
%>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<%
|
|
2
|
+
const { routeInfo, utils } = it;
|
|
3
|
+
const {
|
|
4
|
+
operationId,
|
|
5
|
+
method,
|
|
6
|
+
route,
|
|
7
|
+
moduleName,
|
|
8
|
+
responsesTypes,
|
|
9
|
+
description,
|
|
10
|
+
tags,
|
|
11
|
+
summary,
|
|
12
|
+
pathArgs,
|
|
13
|
+
} = routeInfo;
|
|
14
|
+
const { _, fmtToJSDocLine, require } = utils;
|
|
15
|
+
|
|
16
|
+
const methodAliases = {
|
|
17
|
+
get: (pathName, hasPathInserts) =>
|
|
18
|
+
_.camelCase(`${pathName}_${hasPathInserts ? "detail" : "list"}`),
|
|
19
|
+
post: (pathName, hasPathInserts) => _.camelCase(`${pathName}_create`),
|
|
20
|
+
put: (pathName, hasPathInserts) => _.camelCase(`${pathName}_update`),
|
|
21
|
+
patch: (pathName, hasPathInserts) => _.camelCase(`${pathName}_partial_update`),
|
|
22
|
+
delete: (pathName, hasPathInserts) => _.camelCase(`${pathName}_delete`),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const createCustomOperationId = (method, route, moduleName) => {
|
|
26
|
+
const hasPathInserts = /\{(\w){1,}\}$/g.test(route);
|
|
27
|
+
const splittedRouteBySlash = _.compact(_.replace(route, /\{(\w){1,}\}/g, "").split("/"));
|
|
28
|
+
const routeParts = (splittedRouteBySlash.length > 1
|
|
29
|
+
? splittedRouteBySlash.splice(1)
|
|
30
|
+
: splittedRouteBySlash
|
|
31
|
+
).join("_");
|
|
32
|
+
return routeParts.length > 3 && methodAliases[method]
|
|
33
|
+
? methodAliases[method](routeParts, hasPathInserts)
|
|
34
|
+
: _.camelCase(_.lowerCase(method) + "_" + [moduleName].join("_")) || "index";
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
if (operationId)
|
|
38
|
+
return _.camelCase(operationId);
|
|
39
|
+
if (route === "/")
|
|
40
|
+
return _.camelCase(`${_.lowerCase(method)}Root`);
|
|
41
|
+
|
|
42
|
+
return createCustomOperationId(method, route, moduleName);
|
|
43
|
+
%>
|