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,136 @@
1
+ import CodeBlockWriter from 'code-block-writer'
2
+ import { UnknownTypeError } from './errors.js'
3
+ import { capitalize, toJavaScriptName } from './utils.js'
4
+
5
+ export function processGraphQL ({ schema, name, folder, url }) {
6
+ schema = schema.__schema
7
+ return {
8
+ types: generateTypesFromGraphQL({ schema, name }),
9
+ implementation: generateImplementationFromGraqhQL({ schema, name, url })
10
+ }
11
+ }
12
+
13
+ const skip = new Set(['Query', 'Mutation', 'Subscription', 'Boolean', 'String'])
14
+
15
+ function generateTypesFromGraphQL ({ schema, name }) {
16
+ const camelcasedName = toJavaScriptName(name)
17
+
18
+ const writer = new CodeBlockWriter({
19
+ indentNumberOfSpaces: 2,
20
+ useTabs: false,
21
+ useSingleQuote: true
22
+ })
23
+
24
+ const functionName = `generate${capitalize(camelcasedName)}Client`
25
+
26
+ writer.writeLine("import { type PlatformaticClientOptions } from '@platformatic/massimo'")
27
+ writer.blankLine()
28
+
29
+ writer.write('interface GraphQLQueryOptions').block(() => {
30
+ writer.writeLine('query: string;')
31
+ writer.writeLine('headers: Record<string, string>;')
32
+ writer.writeLine('variables: Record<string, unknown>;')
33
+ })
34
+
35
+ writer.write('interface GraphQLClient').block(() => {
36
+ writer.writeLine('graphql<T>(options: GraphQLQueryOptions): PromiseLike<T>;')
37
+ })
38
+ writer.blankLine()
39
+
40
+ for (const type of schema.types) {
41
+ if (type.kind === 'OBJECT' && type.name.indexOf('__') === -1 && !skip.has(type.name)) {
42
+ const capitalizedName = capitalize(type.name)
43
+ writer.write(`export interface ${capitalizedName}`).block(() => {
44
+ const addedProps = new Set()
45
+ for (const field of type.fields) {
46
+ writeProperty(writer, field.name, field.type, addedProps)
47
+ }
48
+ })
49
+ }
50
+ }
51
+
52
+ writer.write('interface GraphQLClient').block(() => {
53
+ writer.writeLine('graphql<T>(GraphQLQuery): Promise<T>;')
54
+ })
55
+
56
+ writer.blankLine()
57
+ writer.writeLine(`export function ${functionName}(opts: PlatformaticClientOptions): Promise<GraphQLClient>;`)
58
+ writer.writeLine(`export default ${functionName};`)
59
+
60
+ return writer.toString()
61
+ }
62
+
63
+ function generateImplementationFromGraqhQL ({ name, url }) {
64
+ const camelcasedName = toJavaScriptName(name)
65
+
66
+ const writer = new CodeBlockWriter({
67
+ indentNumberOfSpaces: 2,
68
+ useTabs: false,
69
+ useSingleQuote: true
70
+ })
71
+
72
+ writer.writeLine("import { buildGraphQLClient } from '@platformatic/massimo'")
73
+ writer.writeLine("import { join } from 'node:path'")
74
+ writer.blankLine()
75
+
76
+ url = new URL(url)
77
+
78
+ const functionName = `generate${capitalize(camelcasedName)}Client`
79
+ writer.write(`export async function ${functionName} (opts)`).block(() => {
80
+ writer.writeLine('const url = new URL(opts.url)')
81
+ writer.writeLine(`url.pathname = '${url.pathname}'`)
82
+ writer.write('return buildGraphQLClient(').inlineBlock(() => {
83
+ writer.writeLine("type: 'graphql',")
84
+ writer.writeLine(`name: '${camelcasedName}',`)
85
+ writer.writeLine(`path: join(import.meta.dirname, '${name}.schema.graphql'),`)
86
+ writer.writeLine('serviceId: opts.serviceId,')
87
+ writer.writeLine('url: url.toString()')
88
+ })
89
+ writer.write(')')
90
+ })
91
+ writer.blankLine()
92
+ writer.writeLine(`export default ${functionName}`)
93
+ return writer.toString()
94
+ }
95
+
96
+ function GraphQLScalarToTsType (type) {
97
+ switch (type) {
98
+ case 'String':
99
+ return 'string'
100
+ case 'ID':
101
+ return 'string'
102
+ case 'Int':
103
+ return 'number'
104
+ case 'Float':
105
+ return 'number'
106
+ case 'Date':
107
+ return 'string'
108
+ case 'DateTime':
109
+ return 'string'
110
+ // TODO test other scalar types
111
+ /* c8 ignore next 3 */
112
+ default:
113
+ throw new UnknownTypeError(type)
114
+ }
115
+ }
116
+
117
+ function writeProperty (writer, key, value, addedProps) {
118
+ addedProps.add(key)
119
+ writer.quote(key)
120
+ writer.write('?')
121
+ if (value.kind === 'SCALAR') {
122
+ writer.write(`: ${GraphQLScalarToTsType(value.name)};`)
123
+ writer.newLine()
124
+ } else if (value.kind === 'LIST') {
125
+ writer.write(`: Array<${capitalize(value.ofType.name)}>;`)
126
+ writer.newLine()
127
+ } else if (value.kind === 'OBJECT') {
128
+ writer.write(`: ${capitalize(value.name)};`)
129
+ writer.newLine()
130
+ // TODO are there other kinds that needs to be handled?
131
+ /* c8 ignore next 3 */
132
+ } else {
133
+ throw new UnknownTypeError(value.kind)
134
+ }
135
+ writer.newLine()
136
+ }
@@ -0,0 +1,281 @@
1
+ import { hasDuplicatedParameters } from '@platformatic/massimo'
2
+ import camelcase from 'camelcase'
3
+ import CodeBlockWriter from 'code-block-writer'
4
+ import jsonpointer from 'jsonpointer'
5
+ import { TypeNotSupportedError } from './errors.js'
6
+ import { getType } from './get-type.js'
7
+ import { responsesWriter } from './responses-writer.js'
8
+ import { capitalize, getBodyType } from './utils.js'
9
+
10
+ export function writeOperations (
11
+ interfacesWriter,
12
+ mainWriter,
13
+ operations,
14
+ { fullRequest, fullResponse, optionalHeaders, schema, propsOptional }
15
+ ) {
16
+ const originalFullResponse = fullResponse
17
+ const originalFullRequest = fullRequest
18
+ let currentFullResponse = originalFullResponse
19
+ let currentFullRequest = originalFullRequest
20
+ for (const operation of operations) {
21
+ const { operationId, description, summary, deprecated } = operation.operation
22
+ const camelCaseOperationId = camelcase(operationId)
23
+ const { parameters, responses, requestBody } = operation.operation
24
+ currentFullRequest = fullRequest || hasDuplicatedParameters(operation.operation)
25
+ if (!responses) {
26
+ throw new Error(`Cannot find any response definition in operation ${operationId}.`)
27
+ }
28
+ const successResponses = Object.entries(responses).filter(([s]) => s.startsWith('2'))
29
+ if (successResponses.length !== 1) {
30
+ currentFullResponse = true
31
+ }
32
+
33
+ const capitalizedCamelCaseOperationId = capitalize(camelCaseOperationId)
34
+ const operationRequestName = `${capitalizedCamelCaseOperationId}Request`
35
+
36
+ let isRequestArray = false
37
+ let isStructuredType = false
38
+ const bodyWriter = new CodeBlockWriter({
39
+ indentNumberOfSpaces: 2,
40
+ useTabs: false,
41
+ useSingleQuote: true
42
+ })
43
+
44
+ const addedProps = new Set()
45
+ if (parameters) {
46
+ if (currentFullRequest) {
47
+ const bodyParams = []
48
+ const pathParams = []
49
+ const queryParams = []
50
+ const headersParams = []
51
+ for (const parameter of parameters) {
52
+ if (optionalHeaders.includes(parameter.name)) {
53
+ parameter.required = false
54
+ }
55
+ switch (parameter.in) {
56
+ case 'query':
57
+ queryParams.push(parameter)
58
+ break
59
+ case 'path':
60
+ pathParams.push(parameter)
61
+ break
62
+ case 'body':
63
+ bodyParams.push(parameter)
64
+ break
65
+ case 'header':
66
+ headersParams.push(parameter)
67
+ break
68
+ }
69
+ }
70
+ writeProperties(bodyWriter, 'body', bodyParams, addedProps, 'req', schema)
71
+ writeProperties(bodyWriter, 'path', pathParams, addedProps, 'req', schema)
72
+ writeProperties(bodyWriter, 'query', queryParams, addedProps, 'req', schema)
73
+ writeProperties(bodyWriter, 'headers', headersParams, addedProps, 'req', schema)
74
+ } else {
75
+ for (const parameter of parameters) {
76
+ let { name, required } = parameter
77
+ if (optionalHeaders.includes(name)) {
78
+ required = false
79
+ }
80
+ // We do not check for addedProps here because it's the first
81
+ // group of properties
82
+ writeProperty(bodyWriter, name, parameter, addedProps, required, 'req', schema)
83
+ }
84
+ }
85
+ }
86
+ if (requestBody) {
87
+ const bodyType = getBodyType(requestBody)
88
+ if (parameters && parameters.length && (bodyType === 'array' || bodyType === 'plain')) {
89
+ currentFullRequest = true
90
+ }
91
+ const writeContentOutput = writeContent(
92
+ bodyWriter,
93
+ requestBody.content,
94
+ schema,
95
+ addedProps,
96
+ 'req',
97
+ currentFullRequest ? 'body' : null,
98
+ propsOptional
99
+ )
100
+ isRequestArray = writeContentOutput.isArray
101
+ isStructuredType = writeContentOutput.isStructuredType
102
+ }
103
+
104
+ if (isStructuredType || currentFullRequest || !isRequestArray) {
105
+ interfacesWriter.write(`export type ${operationRequestName} =`).block(() => {
106
+ interfacesWriter.write(bodyWriter.toString())
107
+ })
108
+ } else {
109
+ interfacesWriter.write(`export type ${operationRequestName} = `)
110
+ interfacesWriter.write(bodyWriter.toString())
111
+ }
112
+
113
+ interfacesWriter.blankLine()
114
+ const allResponsesName = responsesWriter(
115
+ capitalizedCamelCaseOperationId,
116
+ responses,
117
+ currentFullResponse,
118
+ interfacesWriter,
119
+ schema
120
+ )
121
+ mainWriter.writeLine('/**')
122
+ if (summary) {
123
+ for (const line of summary.split('\n')) {
124
+ mainWriter.writeLine(` * ${line}`)
125
+ }
126
+ // Separate summary and description by blank line
127
+ if (description) {
128
+ mainWriter.writeLine(' *')
129
+ }
130
+ }
131
+ if (description) {
132
+ for (const line of description.split('\n')) {
133
+ mainWriter.writeLine(` * ${line}`)
134
+ }
135
+ }
136
+ if (deprecated) {
137
+ mainWriter.writeLine(' * @deprecated')
138
+ }
139
+ mainWriter.writeLine(' * @param req - request parameters object')
140
+ mainWriter.writeLine(` * @returns the API response${fullResponse ? '' : ' body'}`)
141
+ mainWriter.writeLine(' */')
142
+ mainWriter.writeLine(`${camelCaseOperationId}(req: ${operationRequestName}): Promise<${allResponsesName}>;`)
143
+ currentFullResponse = originalFullResponse
144
+ currentFullRequest = originalFullRequest
145
+ }
146
+ }
147
+
148
+ export function writeProperties (writer, blockName, parameters, addedProps, methodType, spec) {
149
+ if (parameters.length > 0) {
150
+ let allOptionalParams = true
151
+ for (const { required } of parameters) {
152
+ if (required !== false) {
153
+ allOptionalParams = false
154
+ }
155
+ }
156
+ const nameToWrite = allOptionalParams ? `${blockName}?: ` : `${blockName}: `
157
+ writer.write(nameToWrite).block(() => {
158
+ for (const parameter of parameters) {
159
+ const { name, required } = parameter
160
+ // We do not check for addedProps here because it's the first
161
+ // group of properties
162
+ writeProperty(writer, name, parameter, addedProps, required, methodType, spec)
163
+ }
164
+ })
165
+ }
166
+ }
167
+
168
+ export function writeProperty (writer, key, value, addedProps, required = true, methodType, spec) {
169
+ addedProps.add(key)
170
+
171
+ if (value.description || value.deprecated) {
172
+ writer.writeLine('/**')
173
+ if (value.description) {
174
+ for (const line of value.description.split('\n')) {
175
+ writer.writeLine(` * ${line}`)
176
+ }
177
+ }
178
+ if (value.deprecated) {
179
+ writer.writeLine(' * @deprecated')
180
+ }
181
+ writer.writeLine(' */')
182
+ }
183
+
184
+ if (required) {
185
+ writer.quote(key)
186
+ } else {
187
+ writer.quote(key)
188
+ writer.write('?')
189
+ }
190
+
191
+ writer.write(`: ${getType(value, methodType, spec)};`)
192
+ writer.newLine()
193
+ }
194
+
195
+ export function writeContent (writer, content, spec, addedProps, methodType, wrapper, propsOptional) {
196
+ let isArray = false
197
+ let isStructuredType = false
198
+ if (content) {
199
+ for (const [contentType, body] of Object.entries(content)) {
200
+ const isFormDataContent = contentType.indexOf('multipart/form-data') === 0
201
+
202
+ // We ignore all non-JSON endpoints for now
203
+ // TODO: support other content types
204
+ /* c8 ignore next 3 */
205
+ if (contentType.indexOf('application/json') !== 0 && !isFormDataContent) {
206
+ continue
207
+ }
208
+
209
+ if (isFormDataContent && wrapper) {
210
+ writer.write(`${wrapper}: FormData;`)
211
+ break
212
+ }
213
+
214
+ // Response body has no schema that can be processed
215
+ // Should not be possible with well formed OpenAPI
216
+ /* c8 ignore next 3 */
217
+ if (!body.schema?.type && !body.schema?.$ref && !body.schema?.allOf && !body.schema?.anyOf) {
218
+ break
219
+ }
220
+ if (body.schema.type === 'object' || body.schema.$ref) {
221
+ isStructuredType = true
222
+ }
223
+ let schema
224
+ // This is likely buggy as there can be multiple responses for different
225
+ // status codes. This is currently not possible with Platformatic DB
226
+ // services so we skip for now.
227
+ // TODO: support different schemas for different status codes
228
+ if (body.schema.type === 'array') {
229
+ isArray = true
230
+ if (wrapper) {
231
+ writer.write(`${wrapper}: `)
232
+ }
233
+ writer.write(getType(body.schema, methodType, spec))
234
+ return { isArray, isStructuredType }
235
+ } else {
236
+ schema = body.schema
237
+ }
238
+
239
+ if (wrapper) {
240
+ if (isStructuredType) {
241
+ writer
242
+ .write(`${wrapper}: `)
243
+ .block(() => writeObjectProperties(writer, schema, spec, addedProps, methodType, propsOptional))
244
+ } else {
245
+ writer.write(`${wrapper}: ${getType(body.schema, methodType, spec)}`)
246
+ }
247
+ } else {
248
+ writeObjectProperties(writer, schema, spec, addedProps, methodType, propsOptional)
249
+ }
250
+ break
251
+ }
252
+ }
253
+ return { isArray, isStructuredType }
254
+ }
255
+
256
+ export function writeObjectProperties (writer, schema, spec, addedProps, methodType, propsOptional) {
257
+ function _writeObjectProps (obj) {
258
+ for (const [key, value] of Object.entries(obj)) {
259
+ if (addedProps.has(key)) {
260
+ continue
261
+ }
262
+ const required = (propsOptional ? !!schema.required : schema.required) && schema.required.includes(key)
263
+ writeProperty(writer, key, value, addedProps, required, methodType, spec)
264
+ }
265
+ }
266
+
267
+ if (schema.$ref) {
268
+ schema = jsonpointer.get(spec, schema.$ref.replace('#', ''))
269
+ }
270
+ if (schema.type === 'object') {
271
+ if (schema.properties) {
272
+ _writeObjectProps(schema.properties)
273
+ }
274
+
275
+ if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {
276
+ _writeObjectProps(schema.additionalProperties)
277
+ }
278
+ } else {
279
+ throw new TypeNotSupportedError(schema.type)
280
+ }
281
+ }
@@ -0,0 +1,153 @@
1
+ import { generateOperationId } from '@platformatic/massimo'
2
+ import CodeBlockWriter from 'code-block-writer'
3
+ import { writeOperations } from './openapi-common.js'
4
+ import { capitalize, toJavaScriptName } from './utils.js'
5
+
6
+ export function processOpenAPI ({
7
+ schema,
8
+ name,
9
+ fullResponse,
10
+ fullRequest,
11
+ optionalHeaders,
12
+ validateResponse,
13
+ typesComment,
14
+ propsOptional
15
+ }) {
16
+ return {
17
+ types: generateTypesFromOpenAPI({
18
+ schema,
19
+ name,
20
+ fullResponse,
21
+ fullRequest,
22
+ optionalHeaders,
23
+ typesComment,
24
+ propsOptional
25
+ }),
26
+ implementation: generateImplementationFromOpenAPI({ name, fullResponse, fullRequest, validateResponse })
27
+ }
28
+ }
29
+
30
+ function generateImplementationFromOpenAPI ({ name, fullResponse, fullRequest, validateResponse }) {
31
+ const camelcasedName = toJavaScriptName(name)
32
+
33
+ const writer = new CodeBlockWriter({
34
+ indentNumberOfSpaces: 2,
35
+ useTabs: false,
36
+ useSingleQuote: true
37
+ })
38
+
39
+ writer.writeLine("import { buildOpenAPIClient } from '@platformatic/massimo'")
40
+ writer.writeLine("import { join } from 'node:path'")
41
+ writer.blankLine()
42
+
43
+ const functionName = `generate${capitalize(camelcasedName)}Client`
44
+ writer.write(`export async function ${functionName} (opts)`).block(() => {
45
+ writer.write('return buildOpenAPIClient(').inlineBlock(() => {
46
+ writer.writeLine("type: 'openapi',")
47
+ writer.writeLine(`name: '${camelcasedName}',`)
48
+ writer.writeLine(`path: join(import.meta.dirname, '${name}.openapi.json'),`)
49
+ writer.writeLine('url: opts.url,')
50
+ writer.writeLine('serviceId: opts.serviceId,')
51
+ writer.writeLine('throwOnError: opts.throwOnError,')
52
+ writer.writeLine(`fullResponse: ${fullResponse},`)
53
+ writer.writeLine(`fullRequest: ${fullRequest},`)
54
+ writer.writeLine(`validateResponse: ${validateResponse},`)
55
+ writer.writeLine('getHeaders: opts.getHeaders')
56
+ })
57
+ writer.write(')')
58
+ })
59
+ writer.blankLine()
60
+ writer.writeLine(`export default ${functionName}`)
61
+ return writer.toString()
62
+ }
63
+
64
+ function generateTypesFromOpenAPI ({
65
+ schema,
66
+ name,
67
+ fullResponse,
68
+ fullRequest,
69
+ optionalHeaders,
70
+ typesComment,
71
+ propsOptional
72
+ }) {
73
+ const camelcasedName = toJavaScriptName(name)
74
+ const capitalizedName = capitalize(camelcasedName)
75
+ const { paths } = schema
76
+ const generatedOperationIds = []
77
+
78
+ const operations = Object.entries(paths).flatMap(([path, methods]) => {
79
+ let commonParameters = []
80
+ if (methods.parameters) {
81
+ // common parameters for all operations
82
+ commonParameters = methods.parameters
83
+ delete methods.parameters
84
+ }
85
+ return Object.entries(methods).map(([method, operation]) => {
86
+ if (operation.parameters) {
87
+ operation.parameters = [...operation.parameters, ...commonParameters]
88
+ } else {
89
+ operation.parameters = commonParameters
90
+ }
91
+ const opId = generateOperationId(path, method, operation, generatedOperationIds)
92
+ return {
93
+ path,
94
+ method,
95
+ operation: {
96
+ ...operation,
97
+ operationId: opId
98
+ }
99
+ }
100
+ })
101
+ })
102
+
103
+ const writer = new CodeBlockWriter({
104
+ indentNumberOfSpaces: 2,
105
+ useTabs: false,
106
+ useSingleQuote: true
107
+ })
108
+
109
+ const interfaces = new CodeBlockWriter({
110
+ indentNumberOfSpaces: 2,
111
+ useTabs: false,
112
+ useSingleQuote: true
113
+ })
114
+
115
+ if (typesComment) {
116
+ writer.writeLine(`// ${typesComment}`)
117
+ }
118
+
119
+ writer.writeLine(
120
+ "import { type GetHeadersOptions, type PlatformaticClientOptions, type StatusCode1xx, type StatusCode2xx, type StatusCode3xx, type StatusCode4xx, type StatusCode5xx } from '@platformatic/massimo'"
121
+ )
122
+ writer.writeLine("import { type FormData } from 'undici'")
123
+ writer.blankLine()
124
+
125
+ const functionName = `generate${capitalize(camelcasedName)}Client`
126
+
127
+ // Add always FullResponse interface because we don't know yet
128
+ // if we are going to use it
129
+ interfaces.write('export interface FullResponse<T, U extends number>').block(() => {
130
+ interfaces.writeLine("'statusCode': U;")
131
+ interfaces.writeLine("'headers': Record<string, string>;")
132
+ interfaces.writeLine("'body': T;")
133
+ })
134
+ interfaces.blankLine()
135
+
136
+ writer.write(`export type ${capitalizedName} =`).block(() => {
137
+ writeOperations(interfaces, writer, operations, {
138
+ fullRequest,
139
+ fullResponse,
140
+ optionalHeaders,
141
+ schema,
142
+ propsOptional
143
+ })
144
+ })
145
+
146
+ writer.write(interfaces.toString())
147
+
148
+ writer.blankLine()
149
+ writer.writeLine(`export function ${functionName}(opts: PlatformaticClientOptions): Promise<${capitalizedName}>;`)
150
+ writer.writeLine(`export default ${functionName};`)
151
+
152
+ return writer.toString()
153
+ }
@@ -0,0 +1,91 @@
1
+ import { STATUS_CODES } from 'node:http'
2
+ import { getType } from './get-type.js'
3
+ import { capitalize, classCase, getResponseContentType, getResponseTypes } from './utils.js'
4
+
5
+ function responsesWriter (operationId, responsesObject, isFullResponse, writer, spec) {
6
+ const mappedResponses = getResponseTypes(responsesObject)
7
+ const responseTypes = Object.entries(responsesObject).map(([statusCode, response]) => {
8
+ // Unrecognized status code
9
+ const statusCodeName = STATUS_CODES[statusCode]
10
+ let typeName
11
+ if (statusCodeName === undefined) {
12
+ typeName = `${operationId}${statusCode}Response`
13
+ } else {
14
+ typeName = `${operationId}Response${classCase(STATUS_CODES[statusCode])}`
15
+ }
16
+ let isResponseArray
17
+ const responseContentType = getResponseContentType(response)
18
+ if (responseContentType === 'application/json') {
19
+ writeResponse(typeName, response.content['application/json'].schema, response.summary, response.description)
20
+ } else if (responseContentType === null) {
21
+ isFullResponse = true
22
+ writer.writeLine(`export type ${typeName} = unknown`)
23
+ } else if (mappedResponses.blob.includes(parseInt(statusCode))) {
24
+ writer.writeLine(`export type ${typeName} = Blob`)
25
+ } else if (mappedResponses.text.includes(parseInt(statusCode))) {
26
+ writer.writeLine(`export type ${typeName} = string`)
27
+ } else {
28
+ isFullResponse = true
29
+ writer.writeLine(`export type ${typeName} = string`)
30
+ }
31
+
32
+ const lowerStatusCode = statusCode.toLowerCase()
33
+ const isStatusCodeRange =
34
+ lowerStatusCode === '1xx' ||
35
+ lowerStatusCode === '2xx' ||
36
+ lowerStatusCode === '3xx' ||
37
+ lowerStatusCode === '4xx' ||
38
+ lowerStatusCode === '5xx'
39
+ if (statusCode === '204') {
40
+ if (isFullResponse) {
41
+ typeName = undefined
42
+ } else {
43
+ return 'undefined'
44
+ }
45
+ }
46
+ if (isResponseArray) typeName = `Array<${typeName}>`
47
+ if (isFullResponse) {
48
+ typeName = `FullResponse<${typeName}, ${isStatusCodeRange ? `StatusCode${lowerStatusCode}` : statusCode}>`
49
+ }
50
+ return typeName
51
+ })
52
+ // write response unions
53
+ if (responseTypes.length) {
54
+ const allResponsesName = `${capitalize(operationId)}Responses`
55
+ writer.writeLine(`export type ${allResponsesName} =`)
56
+ writer.indent(() => {
57
+ if (responseTypes.length > 0) {
58
+ writer.write(responseTypes.join('\n| '))
59
+ } else {
60
+ writer.write('unknown')
61
+ }
62
+ })
63
+ writer.blankLine()
64
+ return allResponsesName
65
+ }
66
+ return 'FullResponse<unknown, 200>'
67
+
68
+ function writeResponse (typeName, responseSchema, summary, description) {
69
+ if (!responseSchema) {
70
+ return
71
+ }
72
+ if (description || summary) {
73
+ writer.writeLine('/**')
74
+ if (summary) {
75
+ for (const line of summary.split('\n')) {
76
+ writer.writeLine(` * ${line}`)
77
+ }
78
+ writer.writeLine(' *')
79
+ }
80
+ if (description) {
81
+ for (const line of description.split('\n')) {
82
+ writer.writeLine(` * ${line}`)
83
+ }
84
+ }
85
+ writer.writeLine(' */')
86
+ }
87
+ writer.writeLine(`export type ${typeName} = ${getType(responseSchema, 'res', spec)}`)
88
+ }
89
+ }
90
+
91
+ export { responsesWriter }