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,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 }
|