massimo-cli 0.0.1 → 0.1.2
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 +201 -0
- package/NOTICE +13 -0
- package/README.md +13 -0
- package/eslint.config.js +3 -0
- package/help/help.txt +79 -0
- package/index.d.ts +18 -0
- package/index.js +537 -0
- package/index.test-d.ts +15 -0
- package/lib/errors.js +6 -0
- package/lib/frontend-openapi-generator.js +450 -0
- package/lib/get-type.js +146 -0
- package/lib/graphql-generator.js +136 -0
- package/lib/openapi-common.js +281 -0
- package/lib/openapi-generator.js +153 -0
- package/lib/responses-writer.js +91 -0
- package/lib/utils.js +119 -0
- package/package.json +77 -8
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import { generateOperationId } from '@platformatic/massimo'
|
|
2
|
+
import camelcase from 'camelcase'
|
|
3
|
+
import CodeBlockWriter from 'code-block-writer'
|
|
4
|
+
import { writeOperations } from './openapi-common.js'
|
|
5
|
+
import {
|
|
6
|
+
capitalize,
|
|
7
|
+
getAllResponseCodes,
|
|
8
|
+
getResponseContentType,
|
|
9
|
+
getResponseTypes,
|
|
10
|
+
is200JsonResponse
|
|
11
|
+
} from './utils.js'
|
|
12
|
+
|
|
13
|
+
export function processFrontendOpenAPI ({
|
|
14
|
+
schema,
|
|
15
|
+
name,
|
|
16
|
+
language,
|
|
17
|
+
fullResponse,
|
|
18
|
+
fullRequest,
|
|
19
|
+
logger,
|
|
20
|
+
withCredentials,
|
|
21
|
+
propsOptional
|
|
22
|
+
}) {
|
|
23
|
+
return {
|
|
24
|
+
types: generateTypesFromOpenAPI({ schema, name, fullResponse, fullRequest, propsOptional }),
|
|
25
|
+
implementation: generateFrontendImplementationFromOpenAPI({
|
|
26
|
+
schema,
|
|
27
|
+
name,
|
|
28
|
+
language,
|
|
29
|
+
fullResponse,
|
|
30
|
+
fullRequest,
|
|
31
|
+
logger,
|
|
32
|
+
withCredentials
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function generateFrontendImplementationFromOpenAPI ({
|
|
38
|
+
schema,
|
|
39
|
+
name,
|
|
40
|
+
language,
|
|
41
|
+
fullResponse,
|
|
42
|
+
fullRequest,
|
|
43
|
+
logger,
|
|
44
|
+
withCredentials
|
|
45
|
+
}) {
|
|
46
|
+
const camelCaseName = capitalize(camelcase(name))
|
|
47
|
+
const { paths } = schema
|
|
48
|
+
const generatedOperationIds = []
|
|
49
|
+
const operations = Object.entries(paths).flatMap(([path, methods]) => {
|
|
50
|
+
return Object.entries(methods).map(([method, operation]) => {
|
|
51
|
+
const opId = generateOperationId(path, method, operation, generatedOperationIds)
|
|
52
|
+
return {
|
|
53
|
+
path,
|
|
54
|
+
method,
|
|
55
|
+
operation: {
|
|
56
|
+
...operation,
|
|
57
|
+
operationId: opId
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const writer = new CodeBlockWriter({
|
|
64
|
+
indentNumberOfSpaces: 2,
|
|
65
|
+
useTabs: false,
|
|
66
|
+
useSingleQuote: true
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
writer.write('// This client was generated by Platformatic from an OpenAPI specification.')
|
|
70
|
+
writer.blankLine()
|
|
71
|
+
|
|
72
|
+
const isTsLang = language === 'ts'
|
|
73
|
+
writer.conditionalWriteLine(isTsLang, `import type { ${camelCaseName} } from './${name}-types'`)
|
|
74
|
+
writer.conditionalWriteLine(isTsLang, `import type * as Types from './${name}-types'`)
|
|
75
|
+
writer.blankLineIfLastNot()
|
|
76
|
+
|
|
77
|
+
writer.writeLine('// The base URL for the API. This can be overridden by calling `setBaseUrl`.')
|
|
78
|
+
writer.writeLine("let baseUrl = ''")
|
|
79
|
+
writer.writeLine(
|
|
80
|
+
'// The default headers to send within each request. This can be overridden by calling `setDefaultHeaders`.'
|
|
81
|
+
)
|
|
82
|
+
writer.writeLine('let defaultHeaders = {}')
|
|
83
|
+
writer.writeLine('// The additional parameters you want to pass to the `fetch` instance.')
|
|
84
|
+
writer.writeLine('let defaultFetchParams = {}')
|
|
85
|
+
writer.writeLine("const defaultJsonType = { 'Content-type': 'application/json; charset=utf-8' }")
|
|
86
|
+
writer.newLine()
|
|
87
|
+
if (isTsLang) {
|
|
88
|
+
writer.write('function sanitizeUrl(url: string) : string ').block(() => {
|
|
89
|
+
writer.writeLine("if (url.endsWith('/')) { return url.slice(0, -1) } else { return url }")
|
|
90
|
+
})
|
|
91
|
+
writer.writeLine('export const setBaseUrl = (newUrl: string) : void => { baseUrl = sanitizeUrl(newUrl) }')
|
|
92
|
+
writer.newLine()
|
|
93
|
+
writer.writeLine('export const setDefaultHeaders = (headers: object): void => { defaultHeaders = headers }')
|
|
94
|
+
writer.newLine()
|
|
95
|
+
writer.writeLine(
|
|
96
|
+
'export const setDefaultFetchParams = (fetchParams: RequestInit): void => { defaultFetchParams = fetchParams }'
|
|
97
|
+
)
|
|
98
|
+
writer.newLine()
|
|
99
|
+
|
|
100
|
+
writer.writeLine('type JSON = Record<string, unknown>')
|
|
101
|
+
writer.writeLine('/* @ts-ignore - potential unused variable */')
|
|
102
|
+
writer.write('function headersToJSON(headers: Headers): JSON ').block(() => {
|
|
103
|
+
writer.writeLine('const output: JSON = {}')
|
|
104
|
+
writer.write('headers.forEach((value, key) => ').inlineBlock(() => {
|
|
105
|
+
writer.write('output[key] = value')
|
|
106
|
+
})
|
|
107
|
+
writer.write(')')
|
|
108
|
+
writer.writeLine('return output')
|
|
109
|
+
})
|
|
110
|
+
} else {
|
|
111
|
+
writer.write('function sanitizeUrl(url)').block(() => {
|
|
112
|
+
writer.writeLine("if (url.endsWith('/')) { return url.slice(0, -1) } else { return url }")
|
|
113
|
+
})
|
|
114
|
+
writer.writeLine(`/** @type {import('./${name}-types.d.ts').${camelCaseName}['setBaseUrl']} */`)
|
|
115
|
+
writer.writeLine('export const setBaseUrl = (newUrl) => { baseUrl = sanitizeUrl(newUrl) }')
|
|
116
|
+
writer.newLine()
|
|
117
|
+
writer.writeLine(`/** @type {import('./${name}-types.d.ts').${camelCaseName}['setDefaultHeaders']} */`)
|
|
118
|
+
writer.writeLine('export const setDefaultHeaders = (headers) => { defaultHeaders = headers }')
|
|
119
|
+
writer.newLine()
|
|
120
|
+
writer.writeLine(`/** @type {import('./${name}-types.d.ts').${camelCaseName}['setDefaultFetchParams']} */`)
|
|
121
|
+
writer.writeLine('export const setDefaultFetchParams = (fetchParams) => { defaultFetchParams = fetchParams }')
|
|
122
|
+
writer.newLine()
|
|
123
|
+
writer.write('function headersToJSON(headers) ').block(() => {
|
|
124
|
+
writer.writeLine('const output = {}')
|
|
125
|
+
writer.write('headers.forEach((value, key) => ').inlineBlock(() => {
|
|
126
|
+
writer.write('output[key] = value')
|
|
127
|
+
})
|
|
128
|
+
writer.write(')')
|
|
129
|
+
writer.writeLine('return output')
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
writer.blankLine()
|
|
133
|
+
const allOperations = []
|
|
134
|
+
const originalFullResponse = fullResponse
|
|
135
|
+
let currentFullResponse = originalFullResponse
|
|
136
|
+
function getQueryParamsString (operationParams) {
|
|
137
|
+
return operationParams.filter(p => p.in === 'query').map(p => p.name)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function getHeaderParams (operationParams) {
|
|
141
|
+
return operationParams.filter(p => p.in === 'header').map(p => p.name)
|
|
142
|
+
}
|
|
143
|
+
for (const operation of operations) {
|
|
144
|
+
const { operationId, responses } = operation.operation
|
|
145
|
+
const camelCaseOperationId = camelcase(operationId)
|
|
146
|
+
const operationRequestName = `${capitalize(camelCaseOperationId)}Request`
|
|
147
|
+
const operationResponseName = `${capitalize(camelCaseOperationId)}Responses`
|
|
148
|
+
const underscoredOperationId = `_${operationId}`
|
|
149
|
+
let queryParams = []
|
|
150
|
+
let headerParams = []
|
|
151
|
+
if (operation.operation.parameters) {
|
|
152
|
+
queryParams = getQueryParamsString(operation.operation.parameters)
|
|
153
|
+
headerParams = getHeaderParams(operation.operation.parameters)
|
|
154
|
+
}
|
|
155
|
+
allOperations.push(operationId)
|
|
156
|
+
const { method, path } = operation
|
|
157
|
+
const isGetMethod = method === 'get'
|
|
158
|
+
|
|
159
|
+
// Only dealing with success responses
|
|
160
|
+
const successResponses = Object.entries(responses).filter(([s]) => s.startsWith('2'))
|
|
161
|
+
/* c8 ignore next 3 */
|
|
162
|
+
if (successResponses.length !== 1) {
|
|
163
|
+
currentFullResponse = true
|
|
164
|
+
} else {
|
|
165
|
+
// check if is empty response
|
|
166
|
+
if (getResponseContentType(successResponses[0][1]) === null) {
|
|
167
|
+
if (!currentFullResponse) {
|
|
168
|
+
logger.warn(`Full response has been forced due to a schema with empty response for ${operationResponseName}`)
|
|
169
|
+
}
|
|
170
|
+
currentFullResponse = true
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (isTsLang) {
|
|
174
|
+
// Write
|
|
175
|
+
//
|
|
176
|
+
// ```ts
|
|
177
|
+
// export const getMovies:Api['getMovies'] = async (request) => {
|
|
178
|
+
// ```
|
|
179
|
+
writer.write(
|
|
180
|
+
`const ${underscoredOperationId} = async (url: string, request: Types.${operationRequestName}): Promise<Types.${operationResponseName}> =>`
|
|
181
|
+
)
|
|
182
|
+
} else {
|
|
183
|
+
writer.write(`async function ${underscoredOperationId} (url, request)`)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
writer.block(() => {
|
|
187
|
+
const req = fullRequest ? 'request.path' : 'request'
|
|
188
|
+
// Transform
|
|
189
|
+
// /organizations/{orgId}/members/{memberId}
|
|
190
|
+
// to
|
|
191
|
+
// /organizations/${request.orgId}/members/${request.memberId}
|
|
192
|
+
const stringLiteralPath = path.replace(/\{/gm, '${' + req + "['").replace(/\}/gm, "']}")
|
|
193
|
+
// GET methods need query strings instead of JSON bodies
|
|
194
|
+
if (queryParams.length) {
|
|
195
|
+
// query parameters should be appended to the url
|
|
196
|
+
const quotedParams = queryParams.map(qp => `'${qp}'`)
|
|
197
|
+
let queryParametersType = ''
|
|
198
|
+
if (isTsLang) {
|
|
199
|
+
queryParametersType = `: (keyof NonNullable<Types.${fullRequest ? `${operationRequestName}['query']` : operationRequestName}>)[]`
|
|
200
|
+
}
|
|
201
|
+
writer.writeLine(`const queryParameters${queryParametersType} = [${quotedParams.join(', ')}]`)
|
|
202
|
+
writer.writeLine('const searchParams = new URLSearchParams()')
|
|
203
|
+
const query = fullRequest ? 'request.query' : 'request'
|
|
204
|
+
writer.write(`if (${query}) `).inlineBlock(() => {
|
|
205
|
+
writer.write('queryParameters.forEach((qp) => ').inlineBlock(() => {
|
|
206
|
+
writer.writeLine(`const queryValue = ${query}?.[qp]`)
|
|
207
|
+
writer.write('if (queryValue) ').inlineBlock(() => {
|
|
208
|
+
writer.write('if (Array.isArray(queryValue)) ').inlineBlock(() => {
|
|
209
|
+
writer.write('queryValue.forEach((p) => searchParams.append(qp, p))')
|
|
210
|
+
})
|
|
211
|
+
writer.write(' else ').inlineBlock(() => {
|
|
212
|
+
writer.writeLine('searchParams.append(qp, queryValue.toString())')
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
writer.writeLine(`delete ${query}?.[qp]`)
|
|
216
|
+
})
|
|
217
|
+
writer.write(')')
|
|
218
|
+
})
|
|
219
|
+
writer.blankLine()
|
|
220
|
+
}
|
|
221
|
+
const reqBody = fullRequest ? 'request.body' : 'request'
|
|
222
|
+
if (!isGetMethod) {
|
|
223
|
+
writer.writeLine(`const body = ${fullRequest ? `'body' in request ? (${reqBody}) : undefined` : reqBody}`)
|
|
224
|
+
writer.writeLine('const isFormData = body instanceof FormData')
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
writer.write(`const headers${isTsLang ? ': HeadersInit' : ''} =`).block(() => {
|
|
228
|
+
if (isGetMethod) {
|
|
229
|
+
writer.writeLine('...defaultHeaders')
|
|
230
|
+
} else {
|
|
231
|
+
writer.writeLine('...defaultHeaders,')
|
|
232
|
+
writer.writeLine('...(isFormData || body === undefined) ? {} : defaultJsonType')
|
|
233
|
+
}
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
const headers = fullRequest ? 'request.headers' : 'request'
|
|
237
|
+
headerParams.forEach(param => {
|
|
238
|
+
writer.write(`if (${headers} && ${headers}['${param}'] !== undefined)`).block(() => {
|
|
239
|
+
writer.writeLine(`headers['${param}'] = ${headers}['${param}']`)
|
|
240
|
+
writer.writeLine(`delete ${headers}['${param}']`)
|
|
241
|
+
})
|
|
242
|
+
})
|
|
243
|
+
writer.blankLine()
|
|
244
|
+
|
|
245
|
+
/* eslint-disable-next-line no-template-curly-in-string */
|
|
246
|
+
const searchString = queryParams.length > 0 ? '?${searchParams.toString()}' : ''
|
|
247
|
+
if (!isGetMethod) {
|
|
248
|
+
writer
|
|
249
|
+
.write(`const response = await fetch(\`\${url}${stringLiteralPath}${searchString}\`, `)
|
|
250
|
+
.inlineBlock(() => {
|
|
251
|
+
writer.write('method: ').quote().write(method.toUpperCase()).quote().write(',')
|
|
252
|
+
writer.writeLine('body: isFormData ? body : JSON.stringify(body),')
|
|
253
|
+
if (withCredentials) {
|
|
254
|
+
writer.writeLine("credentials: 'include',")
|
|
255
|
+
}
|
|
256
|
+
writer.writeLine('headers,')
|
|
257
|
+
writer.writeLine('...defaultFetchParams')
|
|
258
|
+
})
|
|
259
|
+
.write(')')
|
|
260
|
+
} else {
|
|
261
|
+
writer
|
|
262
|
+
.write(`const response = await fetch(\`\${url}${stringLiteralPath}${searchString}\`, `)
|
|
263
|
+
.inlineBlock(() => {
|
|
264
|
+
if (withCredentials) {
|
|
265
|
+
writer.writeLine("credentials: 'include',")
|
|
266
|
+
}
|
|
267
|
+
writer.writeLine('headers,')
|
|
268
|
+
writer.writeLine('...defaultFetchParams')
|
|
269
|
+
})
|
|
270
|
+
.write(')')
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
writer.blankLine()
|
|
274
|
+
const mappedResponses = getResponseTypes(operation.operation.responses)
|
|
275
|
+
if (currentFullResponse) {
|
|
276
|
+
const allResponseCodes = getAllResponseCodes(operation.operation.responses)
|
|
277
|
+
if (allResponseCodes.includes(204)) {
|
|
278
|
+
mappedResponses.text = mappedResponses.text.filter(code => code !== 204)
|
|
279
|
+
writer.write('if (response.status === 204)').block(() => {
|
|
280
|
+
writer.writeLine(
|
|
281
|
+
'return { statusCode: response.status, headers: headersToJSON(response.headers), body: undefined }'
|
|
282
|
+
)
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
Object.keys(mappedResponses).forEach(responseType => {
|
|
286
|
+
if (mappedResponses[responseType].length > 0) {
|
|
287
|
+
writer.writeLine(`const ${responseType}Responses = [${mappedResponses[responseType].join(', ')}]`)
|
|
288
|
+
writer.write(`if (${responseType}Responses.includes(response.status)) `).block(() => {
|
|
289
|
+
writer.write('return ').block(() => {
|
|
290
|
+
writer.write('statusCode: response.status')
|
|
291
|
+
if (isTsLang) {
|
|
292
|
+
writer.write(` as ${mappedResponses[responseType].join(' | ')},`)
|
|
293
|
+
} else {
|
|
294
|
+
writer.write(',')
|
|
295
|
+
}
|
|
296
|
+
writer.writeLine('headers: headersToJSON(response.headers),')
|
|
297
|
+
writer.writeLine(`body: await response.${responseType}()`)
|
|
298
|
+
})
|
|
299
|
+
})
|
|
300
|
+
}
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
// write default response as fallback
|
|
304
|
+
writer.writeLine(
|
|
305
|
+
"const responseType = response.headers.get('content-type')?.startsWith('application/json') ? 'json' : 'text'"
|
|
306
|
+
)
|
|
307
|
+
writer.write('return ').block(() => {
|
|
308
|
+
writer.write('statusCode: response.status')
|
|
309
|
+
if (isTsLang) {
|
|
310
|
+
writer.write(` as ${allResponseCodes.join(' | ')},`)
|
|
311
|
+
} else {
|
|
312
|
+
writer.write(',')
|
|
313
|
+
}
|
|
314
|
+
writer.writeLine('headers: headersToJSON(response.headers),')
|
|
315
|
+
writer.write('body: await response[responseType]()')
|
|
316
|
+
})
|
|
317
|
+
} else {
|
|
318
|
+
writer.write('if (!response.ok)').block(() => {
|
|
319
|
+
writer.writeLine('throw new Error(await response.text())')
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
writer.blankLine()
|
|
323
|
+
|
|
324
|
+
const has204Response = Object.keys(operation.operation.responses).includes('204')
|
|
325
|
+
if (has204Response) {
|
|
326
|
+
writer.write('if (response.status === 204) ').block(() => {
|
|
327
|
+
writer.writeLine('return undefined')
|
|
328
|
+
})
|
|
329
|
+
}
|
|
330
|
+
if (is200JsonResponse(operation.operation.responses)) {
|
|
331
|
+
writer.writeLine('return await response.json()')
|
|
332
|
+
} else {
|
|
333
|
+
writer.writeLine('return await response.text()')
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
})
|
|
337
|
+
writer.blankLine()
|
|
338
|
+
if (isTsLang) {
|
|
339
|
+
writer
|
|
340
|
+
.write(
|
|
341
|
+
`export const ${operationId}: ${camelCaseName}['${operationId}'] = async (request: Types.${operationRequestName}): Promise<Types.${operationResponseName}> =>`
|
|
342
|
+
)
|
|
343
|
+
.block(() => {
|
|
344
|
+
writer.write(`return await ${underscoredOperationId}(baseUrl, request)`)
|
|
345
|
+
})
|
|
346
|
+
} else {
|
|
347
|
+
// The JS version uses the JSDoc type format to offer IntelliSense autocompletion to the developer.
|
|
348
|
+
//
|
|
349
|
+
// ```js
|
|
350
|
+
// /** @type {import('./api-types.d.ts').Api['getMovies']} */
|
|
351
|
+
// export const getMovies = async (request) => {
|
|
352
|
+
// ```
|
|
353
|
+
//
|
|
354
|
+
writer
|
|
355
|
+
.writeLine(`/** @type {import('./${name}-types.d.ts').${camelCaseName}['${operationId}']} */`)
|
|
356
|
+
.write(`export const ${operationId} = async (request) =>`)
|
|
357
|
+
.block(() => {
|
|
358
|
+
writer.write(`return await ${underscoredOperationId}(baseUrl, request)`)
|
|
359
|
+
})
|
|
360
|
+
}
|
|
361
|
+
currentFullResponse = originalFullResponse
|
|
362
|
+
}
|
|
363
|
+
// create factory
|
|
364
|
+
if (isTsLang) {
|
|
365
|
+
writer.write('type BuildOptions = ').block(() => {
|
|
366
|
+
writer.writeLine('headers?: object')
|
|
367
|
+
})
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const factoryBuildFunction = isTsLang
|
|
371
|
+
? 'export default function build (url: string, options?: BuildOptions)'
|
|
372
|
+
: 'export default function build (url, options)'
|
|
373
|
+
writer.write(factoryBuildFunction).block(() => {
|
|
374
|
+
writer.writeLine('url = sanitizeUrl(url)')
|
|
375
|
+
writer.write('if (options?.headers) ').block(() => {
|
|
376
|
+
writer.writeLine('defaultHeaders = options.headers')
|
|
377
|
+
})
|
|
378
|
+
writer.write('return').block(() => {
|
|
379
|
+
for (const [idx, op] of allOperations.entries()) {
|
|
380
|
+
const underscoredOperation = `_${op}`
|
|
381
|
+
const methodString = `${op}: ${underscoredOperation}.bind(url, ...arguments)`
|
|
382
|
+
if (idx === allOperations.length - 1) {
|
|
383
|
+
writer.writeLine(`${methodString}`)
|
|
384
|
+
} else {
|
|
385
|
+
writer.writeLine(`${methodString},`)
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
})
|
|
389
|
+
})
|
|
390
|
+
return writer.toString()
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function generateTypesFromOpenAPI ({ schema, name, fullRequest, fullResponse, propsOptional }) {
|
|
394
|
+
const camelCaseName = capitalize(camelcase(name))
|
|
395
|
+
const { paths } = schema
|
|
396
|
+
const generatedOperationIds = []
|
|
397
|
+
const operations = Object.entries(paths).flatMap(([path, methods]) => {
|
|
398
|
+
return Object.entries(methods).map(([method, operation]) => {
|
|
399
|
+
const opId = generateOperationId(path, method, operation, generatedOperationIds)
|
|
400
|
+
return {
|
|
401
|
+
path,
|
|
402
|
+
method,
|
|
403
|
+
operation: {
|
|
404
|
+
...operation,
|
|
405
|
+
operationId: opId
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
})
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
const writer = new CodeBlockWriter({
|
|
412
|
+
indentNumberOfSpaces: 2,
|
|
413
|
+
useTabs: false,
|
|
414
|
+
useSingleQuote: true
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
const interfaces = new CodeBlockWriter({
|
|
418
|
+
indentNumberOfSpaces: 2,
|
|
419
|
+
useTabs: false,
|
|
420
|
+
useSingleQuote: true
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
interfaces.write('export interface FullResponse<T, U extends number>').block(() => {
|
|
424
|
+
interfaces.writeLine("'statusCode': U;")
|
|
425
|
+
interfaces.writeLine("'headers': object;")
|
|
426
|
+
interfaces.writeLine("'body': T;")
|
|
427
|
+
})
|
|
428
|
+
interfaces.blankLine()
|
|
429
|
+
|
|
430
|
+
writer.blankLine()
|
|
431
|
+
writer.write(`export interface ${camelCaseName}`).block(() => {
|
|
432
|
+
writer.writeLine('setBaseUrl(newUrl: string): void;')
|
|
433
|
+
writer.writeLine('setDefaultHeaders(headers: object): void;')
|
|
434
|
+
writer.writeLine('setDefaultFetchParams(fetchParams: RequestInit): void;')
|
|
435
|
+
writeOperations(interfaces, writer, operations, {
|
|
436
|
+
fullRequest,
|
|
437
|
+
fullResponse,
|
|
438
|
+
optionalHeaders: [],
|
|
439
|
+
schema,
|
|
440
|
+
propsOptional
|
|
441
|
+
})
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
writer.writeLine(`type PlatformaticFrontendClient = Omit<${camelCaseName}, 'setBaseUrl'>`)
|
|
445
|
+
writer.write('type BuildOptions = ').block(() => {
|
|
446
|
+
writer.writeLine('headers?: object')
|
|
447
|
+
})
|
|
448
|
+
writer.writeLine('export default function build(url: string, options?: BuildOptions): PlatformaticFrontendClient')
|
|
449
|
+
return interfaces.toString() + writer.toString()
|
|
450
|
+
}
|
package/lib/get-type.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import jsonpointer from 'jsonpointer'
|
|
2
|
+
|
|
3
|
+
export function getType (typeDef, methodType, spec) {
|
|
4
|
+
if (typeDef.$ref) {
|
|
5
|
+
typeDef = jsonpointer.get(spec, typeDef.$ref.replace('#', ''))
|
|
6
|
+
}
|
|
7
|
+
if (typeDef.schema) {
|
|
8
|
+
return getType(typeDef.schema, methodType, spec)
|
|
9
|
+
}
|
|
10
|
+
if (typeDef.anyOf) {
|
|
11
|
+
// recursively call this function
|
|
12
|
+
const mapped = typeDef.anyOf.map(t => {
|
|
13
|
+
return getType(t, methodType, spec)
|
|
14
|
+
})
|
|
15
|
+
return mapped.join(' | ')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (typeDef.oneOf) {
|
|
19
|
+
// recursively call this function
|
|
20
|
+
const mapped = typeDef.oneOf.map(t => {
|
|
21
|
+
return getType(t, methodType, spec)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
if (typeDef.discriminator && typeDef.discriminator.propertyName) {
|
|
25
|
+
const propertyName = typeDef.discriminator.propertyName
|
|
26
|
+
// we do such conversion
|
|
27
|
+
// from "{ 'type': string; 'meowSound': string }",
|
|
28
|
+
// to "{ 'type': 'Cat'; 'meowSound': string }",
|
|
29
|
+
// where typeDef.discriminator.propertyName = 'type'
|
|
30
|
+
|
|
31
|
+
// we support only an array of $ref values
|
|
32
|
+
const mappedRefNames = typeDef.oneOf.map(t => {
|
|
33
|
+
return t.$ref.split('/').slice(-1).toString()
|
|
34
|
+
})
|
|
35
|
+
return mapped
|
|
36
|
+
.map((mappedObject, idx) => {
|
|
37
|
+
const regexp = new RegExp(`'${propertyName}'[?]?: (string)`)
|
|
38
|
+
|
|
39
|
+
const match = mappedObject.match(regexp)
|
|
40
|
+
if (match) {
|
|
41
|
+
const firstPart = mappedObject.substring(0, match.index)
|
|
42
|
+
const l = match[0].length
|
|
43
|
+
const secondPart = mappedObject.substring(match.index + l)
|
|
44
|
+
const output = firstPart + match[0].replace('string', `'${mappedRefNames[idx]}'`) + secondPart
|
|
45
|
+
return output
|
|
46
|
+
}
|
|
47
|
+
// otherwise we return the object as is
|
|
48
|
+
return mappedObject
|
|
49
|
+
})
|
|
50
|
+
.join(' | ')
|
|
51
|
+
}
|
|
52
|
+
return mapped.join(' | ')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (typeDef.allOf) {
|
|
56
|
+
// recursively call this function
|
|
57
|
+
return typeDef.allOf
|
|
58
|
+
.map(t => {
|
|
59
|
+
return getType(t, methodType, spec)
|
|
60
|
+
})
|
|
61
|
+
.join(' & ')
|
|
62
|
+
}
|
|
63
|
+
if (typeDef.type === 'array') {
|
|
64
|
+
const nullable = typeDef.nullable
|
|
65
|
+
return `Array<${getType(typeDef.items, methodType, spec)}>${nullable === true ? ' | null' : ''}`
|
|
66
|
+
}
|
|
67
|
+
if (typeDef.enum) {
|
|
68
|
+
const nullable = typeDef.nullable
|
|
69
|
+
return (
|
|
70
|
+
typeDef.enum
|
|
71
|
+
.map(en => {
|
|
72
|
+
if (typeDef.type === 'string') {
|
|
73
|
+
return `'${en.replace(/'/g, "\\'")}'`
|
|
74
|
+
} else {
|
|
75
|
+
return en
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
.join(' | ') + (nullable === true ? ' | null' : '')
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
if (typeDef.type === 'object') {
|
|
82
|
+
const additionalProps = typeDef?.additionalProperties
|
|
83
|
+
const additionalPropsObj = additionalProps?.properties
|
|
84
|
+
const additionalPropsType = additionalProps?.type
|
|
85
|
+
const additionalPropsRequired = additionalProps?.required
|
|
86
|
+
const nullable = typeDef.nullable
|
|
87
|
+
const objProperties = typeDef.properties || additionalPropsObj
|
|
88
|
+
if (!objProperties || Object.keys(objProperties).length === 0) {
|
|
89
|
+
// Object without properties
|
|
90
|
+
const resultType = additionalPropsType
|
|
91
|
+
? `Record<string, ${JSONSchemaToTsType({ type: additionalPropsType })}>`
|
|
92
|
+
: 'object'
|
|
93
|
+
return nullable === true ? `${resultType} | null` : resultType
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let output = additionalPropsObj && additionalPropsType === 'object' ? 'Record<string, { ' : '{ '
|
|
97
|
+
// TODO: add a test for objects without properties
|
|
98
|
+
/* c8 ignore next 1 */
|
|
99
|
+
const props = Object.keys(objProperties || {}).map(prop => {
|
|
100
|
+
let required = false
|
|
101
|
+
if (typeDef.required) {
|
|
102
|
+
required = !!typeDef.required.includes(prop)
|
|
103
|
+
}
|
|
104
|
+
if (additionalPropsRequired) {
|
|
105
|
+
required = required || !!additionalPropsRequired.includes(prop)
|
|
106
|
+
}
|
|
107
|
+
return `'${prop}'${required ? '' : '?'}: ${getType(objProperties[prop], methodType, spec)}`
|
|
108
|
+
})
|
|
109
|
+
if (additionalProps === true) {
|
|
110
|
+
props.push('[key: string]: unknown')
|
|
111
|
+
}
|
|
112
|
+
output += props.join('; ')
|
|
113
|
+
output += additionalPropsObj ? ' }>' : ' }'
|
|
114
|
+
if (nullable === true) {
|
|
115
|
+
output += ' | null'
|
|
116
|
+
}
|
|
117
|
+
return output
|
|
118
|
+
}
|
|
119
|
+
return JSONSchemaToTsType(typeDef, methodType)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function JSONSchemaToTsType ({ type, format, nullable }, methodType) {
|
|
123
|
+
const isDateType = format === 'date' || format === 'date-time'
|
|
124
|
+
let resultType = 'unknown'
|
|
125
|
+
|
|
126
|
+
switch (type) {
|
|
127
|
+
case 'string':
|
|
128
|
+
resultType = isDateType && methodType === 'req' ? 'string | Date' : 'string'
|
|
129
|
+
break
|
|
130
|
+
case 'integer':
|
|
131
|
+
resultType = 'number'
|
|
132
|
+
break
|
|
133
|
+
case 'number':
|
|
134
|
+
resultType = 'number'
|
|
135
|
+
break
|
|
136
|
+
case 'boolean':
|
|
137
|
+
resultType = 'boolean'
|
|
138
|
+
break
|
|
139
|
+
case 'null':
|
|
140
|
+
resultType = 'null'
|
|
141
|
+
break
|
|
142
|
+
// TODO what other types should we support here?
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return nullable === true ? `${resultType} | null` : resultType
|
|
146
|
+
}
|