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.
@@ -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
+ }
@@ -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
+ }