@platformatic/service 1.13.7 → 1.14.1
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/help/help.txt +4 -1
- package/help/versions bump.txt +25 -0
- package/help/versions update.txt +23 -0
- package/index.js +15 -5
- package/lib/bump-version.js +178 -0
- package/lib/errors.js +16 -0
- package/lib/get-openapi-schema.js +47 -0
- package/lib/plugins/openapi.js +16 -4
- package/lib/plugins/versions.js +211 -0
- package/lib/root-endpoint/index.js +18 -0
- package/lib/root-endpoint/public/index.html +35 -7
- package/lib/schema.js +58 -11
- package/lib/start.js +3 -1
- package/lib/update-version.js +862 -0
- package/lib/utils.js +126 -3
- package/package.json +17 -8
- package/service.mjs +5 -1
|
@@ -0,0 +1,862 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { join, relative } = require('node:path')
|
|
4
|
+
const { readFile, writeFile, mkdir, readdir, rm } = require('node:fs/promises')
|
|
5
|
+
const { inspect } = require('node:util')
|
|
6
|
+
const pino = require('pino')
|
|
7
|
+
const pretty = require('pino-pretty')
|
|
8
|
+
const cliProgress = require('cli-progress')
|
|
9
|
+
const { request } = require('undici')
|
|
10
|
+
const { green } = require('colorette')
|
|
11
|
+
const compareOpenApiSchemas = require('openapi-schema-diff')
|
|
12
|
+
const { default: CodeBlockWriter } = require('code-block-writer')
|
|
13
|
+
const { loadConfig } = require('@platformatic/config')
|
|
14
|
+
const { analyze, write: writeConfig } = require('@platformatic/metaconfig')
|
|
15
|
+
const { getOpenapiSchema } = require('./get-openapi-schema.js')
|
|
16
|
+
const { platformaticService } = require('../index.js')
|
|
17
|
+
const {
|
|
18
|
+
isFileAccessible,
|
|
19
|
+
changeOpenapiSchemaPrefix,
|
|
20
|
+
convertOpenApiToFastifyPath,
|
|
21
|
+
convertOpenApiToFastifyRouteSchema
|
|
22
|
+
} = require('./utils.js')
|
|
23
|
+
|
|
24
|
+
const OPENAI_SERVICE_HOST = 'https://openai.platformatic.cloud'
|
|
25
|
+
const OPENAI_WARNING = '// !!! This function was generated by OpenAI. Check before use !!!\n'
|
|
26
|
+
|
|
27
|
+
const HTTP_METHODS_WITH_BODY = ['POST', 'PUT', 'PATCH', 'DELETE']
|
|
28
|
+
|
|
29
|
+
async function generateRequestMapper (openaiProxyHost, userApiKey, content) {
|
|
30
|
+
openaiProxyHost = openaiProxyHost ?? OPENAI_SERVICE_HOST
|
|
31
|
+
|
|
32
|
+
const url = openaiProxyHost + '/openapi/mappers/request'
|
|
33
|
+
const { statusCode, body } = await request(url, {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
headers: {
|
|
36
|
+
'Content-Type': 'application/json',
|
|
37
|
+
'x-platformatic-user-api-key': userApiKey
|
|
38
|
+
},
|
|
39
|
+
body: JSON.stringify(content)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
if (statusCode !== 200) {
|
|
43
|
+
const error = await body.text()
|
|
44
|
+
throw new Error('Failed to generate request mapper: ' + error)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const data = await body.json()
|
|
48
|
+
return OPENAI_WARNING + data.code
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function generateResponseMapper (openaiProxyHost, userApiKey, content) {
|
|
52
|
+
openaiProxyHost = openaiProxyHost ?? OPENAI_SERVICE_HOST
|
|
53
|
+
|
|
54
|
+
const url = openaiProxyHost + '/openapi/mappers/response'
|
|
55
|
+
const { statusCode, body } = await request(url, {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: {
|
|
58
|
+
'Content-Type': 'application/json',
|
|
59
|
+
'x-platformatic-user-api-key': userApiKey
|
|
60
|
+
},
|
|
61
|
+
body: JSON.stringify(content)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
if (statusCode !== 200) {
|
|
65
|
+
const error = await body.text()
|
|
66
|
+
throw new Error('Failed to generate response mapper: ' + error)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const data = await body.json()
|
|
70
|
+
return OPENAI_WARNING + data.code
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function generateMapperForDeletedRoute (
|
|
74
|
+
logger,
|
|
75
|
+
method,
|
|
76
|
+
openapiPath,
|
|
77
|
+
prevVersionConfig,
|
|
78
|
+
nextVersionConfig,
|
|
79
|
+
routeDiff
|
|
80
|
+
) {
|
|
81
|
+
const writer = new CodeBlockWriter({
|
|
82
|
+
newLine: '\n',
|
|
83
|
+
indentNumberOfSpaces: 2,
|
|
84
|
+
useTabs: false,
|
|
85
|
+
useSingleQuote: true
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const nextVersion = nextVersionConfig.version
|
|
89
|
+
|
|
90
|
+
const fastifySchema = convertOpenApiToFastifyRouteSchema(routeDiff.sourceSchema)
|
|
91
|
+
const serializedFastifySchema = inspect(
|
|
92
|
+
fastifySchema ?? {},
|
|
93
|
+
{
|
|
94
|
+
depth: null,
|
|
95
|
+
compact: false
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
writer.writeLine(
|
|
100
|
+
`// Route ${method.toUpperCase()} "${openapiPath}" was deleted in the "${nextVersion}" API`
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
const fastifyPath = convertOpenApiToFastifyPath(openapiPath)
|
|
104
|
+
|
|
105
|
+
const errorMessage =
|
|
106
|
+
`Route ${method.toUpperCase()} "${openapiPath}" was deleted in the "${nextVersion}" API`
|
|
107
|
+
|
|
108
|
+
writer.write('fastify.route({').indent(() => {
|
|
109
|
+
writer.writeLine(`method: '${method.toUpperCase()}',`)
|
|
110
|
+
writer.writeLine(`url: '${fastifyPath}',`)
|
|
111
|
+
writer.writeLine(`schema: ${serializedFastifySchema},`)
|
|
112
|
+
writer.write('handler: async (req, reply) => ').block(() => {
|
|
113
|
+
writer.writeLine('reply').indent(() => {
|
|
114
|
+
writer.writeLine('.code(404)')
|
|
115
|
+
writer.writeLine('.send({').indent(() => {
|
|
116
|
+
writer.writeLine('code: \'PLT_ERR_DELETED_ROUTE\',')
|
|
117
|
+
writer.writeLine(`message: '${errorMessage}'`)
|
|
118
|
+
}).write('})')
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
}).write('})')
|
|
122
|
+
|
|
123
|
+
return writer.toString()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function generateDefaultRequestMapper () {
|
|
127
|
+
const writer = new CodeBlockWriter({
|
|
128
|
+
newLine: '\n',
|
|
129
|
+
indentNumberOfSpaces: 2,
|
|
130
|
+
useTabs: false,
|
|
131
|
+
useSingleQuote: true
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
writer.writeLine('function mapInputData (inputData) {').indent(() => {
|
|
135
|
+
writer.writeLine('// Map your input params here')
|
|
136
|
+
writer.blankLine()
|
|
137
|
+
writer.writeLine('delete inputData.headers.connection')
|
|
138
|
+
writer.writeLine('delete inputData.headers[\'content-length\']')
|
|
139
|
+
writer.writeLine('delete inputData.headers[\'transfer-encoding\']')
|
|
140
|
+
writer.blankLine()
|
|
141
|
+
writer.write('return {').indent(() => {
|
|
142
|
+
writer.writeLine('pathParams: inputData.pathParams,')
|
|
143
|
+
writer.writeLine('queryParams: inputData.queryParams,')
|
|
144
|
+
writer.writeLine('headers: inputData.headers,')
|
|
145
|
+
writer.writeLine('requestBody: inputData.requestBody')
|
|
146
|
+
}).write('}')
|
|
147
|
+
}).write('}\n')
|
|
148
|
+
|
|
149
|
+
return writer.toString()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function generateDefaultResponseMapper () {
|
|
153
|
+
const writer = new CodeBlockWriter({
|
|
154
|
+
newLine: '\n',
|
|
155
|
+
indentNumberOfSpaces: 2,
|
|
156
|
+
useTabs: false,
|
|
157
|
+
useSingleQuote: true
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
writer.writeLine('function mapOutputData (outputData) {').indent(() => {
|
|
161
|
+
writer.writeLine('// Map your output params here')
|
|
162
|
+
writer.write('return {').indent(() => {
|
|
163
|
+
writer.writeLine('headers: outputData.headers,')
|
|
164
|
+
writer.writeLine('responseBody: outputData.responseBody')
|
|
165
|
+
}).write('}')
|
|
166
|
+
}).write('}\n')
|
|
167
|
+
|
|
168
|
+
return writer.toString()
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function generateHandlerForChangedRoute (
|
|
172
|
+
logger,
|
|
173
|
+
method,
|
|
174
|
+
openapiPath,
|
|
175
|
+
prevVersionPrefix,
|
|
176
|
+
nextVersionPrefix,
|
|
177
|
+
routeDiff,
|
|
178
|
+
userApiKey,
|
|
179
|
+
openai,
|
|
180
|
+
openaiProxyHost
|
|
181
|
+
) {
|
|
182
|
+
const writer = new CodeBlockWriter({
|
|
183
|
+
newLine: '\n',
|
|
184
|
+
indentNumberOfSpaces: 2,
|
|
185
|
+
useTabs: false,
|
|
186
|
+
useSingleQuote: true
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
const requestChanges = routeDiff.changes.filter(
|
|
190
|
+
c => c.type === 'parameter' || c.type === 'requestBody'
|
|
191
|
+
)
|
|
192
|
+
const responseChanges = routeDiff.changes.filter(
|
|
193
|
+
c => c.type === 'responseHeader' || c.type === 'responseBody'
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
const includeRequestBody = HTTP_METHODS_WITH_BODY.includes(method.toUpperCase())
|
|
197
|
+
|
|
198
|
+
let genRequestMapperReq = null
|
|
199
|
+
if (requestChanges.length > 0) {
|
|
200
|
+
if (openai) {
|
|
201
|
+
genRequestMapperReq = generateRequestMapper(
|
|
202
|
+
openaiProxyHost,
|
|
203
|
+
userApiKey,
|
|
204
|
+
{
|
|
205
|
+
includeBody: includeRequestBody,
|
|
206
|
+
sourceSchema: {
|
|
207
|
+
parameters: routeDiff.sourceSchema.parameters ?? [],
|
|
208
|
+
requestBody: routeDiff.sourceSchema.requestBody ?? {}
|
|
209
|
+
},
|
|
210
|
+
targetSchema: {
|
|
211
|
+
parameters: routeDiff.targetSchema.parameters ?? [],
|
|
212
|
+
requestBody: routeDiff.targetSchema.requestBody ?? {}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
).catch(async (err) => {
|
|
216
|
+
logger.warn({ err }, 'Failed to generate request params mapper. Using default mapper.')
|
|
217
|
+
return generateDefaultRequestMapper()
|
|
218
|
+
})
|
|
219
|
+
} else {
|
|
220
|
+
genRequestMapperReq = generateDefaultRequestMapper()
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const mappedStatusCodes = []
|
|
225
|
+
const getResponseMappersReqs = []
|
|
226
|
+
|
|
227
|
+
const sourceSchemaResponses = routeDiff.sourceSchema.responses ?? {}
|
|
228
|
+
const targetSchemaResponses = routeDiff.targetSchema.responses ?? {}
|
|
229
|
+
|
|
230
|
+
for (const statusCode in targetSchemaResponses) {
|
|
231
|
+
const targetSchemaResponse = targetSchemaResponses[statusCode]
|
|
232
|
+
const sourceSchemaResponse = sourceSchemaResponses[statusCode]
|
|
233
|
+
|
|
234
|
+
const genMapperReqBody = { sourceSchema: {}, targetSchema: {} }
|
|
235
|
+
|
|
236
|
+
const headerChanges = responseChanges.filter(change =>
|
|
237
|
+
change.type === 'responseHeader' &&
|
|
238
|
+
change.statusCode === statusCode
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
const bodyChange = responseChanges.find(change =>
|
|
242
|
+
change.action === 'changed' &&
|
|
243
|
+
change.type === 'responseBody' &&
|
|
244
|
+
change.statusCode === statusCode &&
|
|
245
|
+
change.mediaType === 'application/json'
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
if (bodyChange || headerChanges.length > 0) {
|
|
249
|
+
if (headerChanges.length > 0) {
|
|
250
|
+
genMapperReqBody.targetSchema.headers = sourceSchemaResponse.headers
|
|
251
|
+
genMapperReqBody.sourceSchema.headers = targetSchemaResponse.headers
|
|
252
|
+
}
|
|
253
|
+
if (bodyChange) {
|
|
254
|
+
genMapperReqBody.targetSchema.responseBody = bodyChange.sourceSchema.schema
|
|
255
|
+
genMapperReqBody.sourceSchema.responseBody = bodyChange.targetSchema.schema
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
let genResponseMapperReq = null
|
|
259
|
+
if (openai) {
|
|
260
|
+
genResponseMapperReq = generateResponseMapper(
|
|
261
|
+
openaiProxyHost,
|
|
262
|
+
userApiKey,
|
|
263
|
+
genMapperReqBody
|
|
264
|
+
).catch(async (err) => {
|
|
265
|
+
logger.warn({ err }, 'Failed to generate response params mapper. Using default mapper.')
|
|
266
|
+
return generateDefaultResponseMapper()
|
|
267
|
+
})
|
|
268
|
+
} else {
|
|
269
|
+
genResponseMapperReq = generateDefaultResponseMapper()
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
mappedStatusCodes.push(statusCode)
|
|
273
|
+
getResponseMappersReqs.push(genResponseMapperReq)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const [requestMapper, ...responseMappers] = await Promise.all([
|
|
278
|
+
genRequestMapperReq,
|
|
279
|
+
...getResponseMappersReqs
|
|
280
|
+
])
|
|
281
|
+
|
|
282
|
+
writer.write('async (req, reply) => ').block(() => {
|
|
283
|
+
if (requestMapper) {
|
|
284
|
+
writer.writeLine(requestMapper)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
for (let i = 0; i < mappedStatusCodes.length; i++) {
|
|
288
|
+
const statusCode = mappedStatusCodes[i]
|
|
289
|
+
const responseMapper = responseMappers[i]
|
|
290
|
+
|
|
291
|
+
const renamedResponseMapper = responseMapper.replace(
|
|
292
|
+
'mapOutputData',
|
|
293
|
+
`mapOutputData${statusCode}`
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
writer.writeLine(renamedResponseMapper)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (requestMapper) {
|
|
300
|
+
writer.write('const inputParams = mapInputData({').indent(() => {
|
|
301
|
+
writer.writeLine('pathParams: req.params,')
|
|
302
|
+
writer.writeLine('queryParams: req.query,')
|
|
303
|
+
writer.writeLine('headers: req.headers,')
|
|
304
|
+
if (includeRequestBody) {
|
|
305
|
+
writer.writeLine('requestBody: req.body')
|
|
306
|
+
}
|
|
307
|
+
}).write('})')
|
|
308
|
+
} else {
|
|
309
|
+
writer.write('const inputParams = {').indent(() => {
|
|
310
|
+
writer.writeLine('pathParams: req.params,')
|
|
311
|
+
writer.writeLine('queryParams: req.query,')
|
|
312
|
+
writer.writeLine('headers: req.headers,')
|
|
313
|
+
if (includeRequestBody) {
|
|
314
|
+
writer.writeLine('requestBody: req.body')
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
).write('}')
|
|
318
|
+
}
|
|
319
|
+
writer.blankLine()
|
|
320
|
+
|
|
321
|
+
let nextRouteUrl = null
|
|
322
|
+
const pathParams = openapiPath.match(/{(\w+)}/g) ?? []
|
|
323
|
+
if (pathParams.length > 0) {
|
|
324
|
+
let mappedPath = openapiPath
|
|
325
|
+
for (const pathParam of pathParams) {
|
|
326
|
+
let paramName = pathParam.slice(1, -1)
|
|
327
|
+
paramName = paramName === 'wildcard' ? '*' : paramName
|
|
328
|
+
mappedPath = mappedPath.replace(pathParam, '${params[\'' + paramName + '\']}')
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
writer.writeLine('const params = inputParams.pathParams')
|
|
332
|
+
nextRouteUrl = `\`${nextVersionPrefix + mappedPath}\``
|
|
333
|
+
} else {
|
|
334
|
+
nextRouteUrl = `'${nextVersionPrefix + openapiPath}'`
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
writer.write('const res = await fastify.inject({').indent(() => {
|
|
338
|
+
writer.writeLine('method: \'' + method.toUpperCase() + '\',')
|
|
339
|
+
writer.writeLine(`url: ${nextRouteUrl},`)
|
|
340
|
+
writer.writeLine('query: inputParams.queryParams,')
|
|
341
|
+
writer.writeLine('headers: inputParams.headers,')
|
|
342
|
+
if (includeRequestBody) {
|
|
343
|
+
writer.writeLine('payload: inputParams.requestBody')
|
|
344
|
+
}
|
|
345
|
+
}).write('})')
|
|
346
|
+
writer.blankLine()
|
|
347
|
+
|
|
348
|
+
writer.write('let outputParams = {').indent(() => {
|
|
349
|
+
writer.writeLine('headers: res.headers,')
|
|
350
|
+
writer.writeLine('responseBody: res.body')
|
|
351
|
+
}).write('}')
|
|
352
|
+
writer.blankLine()
|
|
353
|
+
|
|
354
|
+
for (let i = 0; i < mappedStatusCodes.length; i++) {
|
|
355
|
+
const statusCode = mappedStatusCodes[i]
|
|
356
|
+
const mapperFunctionName = `mapOutputData${statusCode}`
|
|
357
|
+
|
|
358
|
+
writer.write(`if (res.statusCode === ${statusCode}) `).block(() => {
|
|
359
|
+
writer.writeLine('const responseBody = outputParams.responseBody')
|
|
360
|
+
writer.write('if (typeof responseBody === \'string\')').block(() => {
|
|
361
|
+
writer.writeLine('outputParams.responseBody = JSON.parse(responseBody)')
|
|
362
|
+
})
|
|
363
|
+
writer.write(`outputParams = ${mapperFunctionName}(outputParams)`)
|
|
364
|
+
})
|
|
365
|
+
writer.blankLine()
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
writer.write('reply').indent(() => {
|
|
369
|
+
writer.writeLine('.code(res.statusCode)')
|
|
370
|
+
writer.writeLine('.headers(outputParams.headers)')
|
|
371
|
+
})
|
|
372
|
+
writer.blankLine()
|
|
373
|
+
writer.writeLine('return outputParams.responseBody')
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
return writer.toString()
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function generateMapperForChangedRoute (
|
|
380
|
+
logger,
|
|
381
|
+
method,
|
|
382
|
+
openapiPath,
|
|
383
|
+
prevVersionConfig,
|
|
384
|
+
nextVersionConfig,
|
|
385
|
+
routeDiff,
|
|
386
|
+
userApiKey,
|
|
387
|
+
openai,
|
|
388
|
+
openaiProxyHost
|
|
389
|
+
) {
|
|
390
|
+
const writer = new CodeBlockWriter({
|
|
391
|
+
newLine: '\n',
|
|
392
|
+
indentNumberOfSpaces: 2,
|
|
393
|
+
useTabs: false,
|
|
394
|
+
useSingleQuote: true
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
const nextVersion = nextVersionConfig.version
|
|
398
|
+
|
|
399
|
+
const prevVersionPrefix = prevVersionConfig.openapi.prefix ?? ''
|
|
400
|
+
const nextVersionPrefix = nextVersionConfig.openapi.prefix ?? ''
|
|
401
|
+
|
|
402
|
+
writer.writeLine(
|
|
403
|
+
`/* Route ${method.toUpperCase()} "${openapiPath}" was changed in the "${nextVersion}" API`
|
|
404
|
+
)
|
|
405
|
+
writer.indent(() => {
|
|
406
|
+
for (const componentChange of routeDiff.changes) {
|
|
407
|
+
writer.writeLine(`- ${componentChange.comment}`)
|
|
408
|
+
if (componentChange.action !== 'changed') continue
|
|
409
|
+
|
|
410
|
+
writer.indent(() => {
|
|
411
|
+
for (const keywordChange of componentChange.changes ?? []) {
|
|
412
|
+
writer.writeLine(`- ${keywordChange.comment}`)
|
|
413
|
+
if (keywordChange.keyword !== 'schema') continue
|
|
414
|
+
|
|
415
|
+
writer.indent(() => {
|
|
416
|
+
for (const schemaChange of keywordChange.changes ?? []) {
|
|
417
|
+
const prevSchemaComments = JSON.stringify(schemaChange.source || '', null, 2)
|
|
418
|
+
const nextSchemaComments = JSON.stringify(schemaChange.target || '', null, 2)
|
|
419
|
+
|
|
420
|
+
writer.writeLine(`- schema diff at "${schemaChange.jsonPath}":`)
|
|
421
|
+
writer.indent(() => {
|
|
422
|
+
prevSchemaComments.split('\n').forEach(l => writer.writeLine('- ' + l))
|
|
423
|
+
nextSchemaComments.split('\n').forEach(l => writer.writeLine('+ ' + l))
|
|
424
|
+
writer.blankLine()
|
|
425
|
+
})
|
|
426
|
+
}
|
|
427
|
+
})
|
|
428
|
+
}
|
|
429
|
+
})
|
|
430
|
+
}
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
writer.writeLine('*/')
|
|
434
|
+
|
|
435
|
+
const fastifySchema = convertOpenApiToFastifyRouteSchema(routeDiff.sourceSchema)
|
|
436
|
+
const serializedFastifySchema = inspect(
|
|
437
|
+
fastifySchema ?? {},
|
|
438
|
+
{
|
|
439
|
+
depth: null,
|
|
440
|
+
compact: false
|
|
441
|
+
}
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
const routeHandler = await generateHandlerForChangedRoute(
|
|
445
|
+
logger,
|
|
446
|
+
method,
|
|
447
|
+
openapiPath,
|
|
448
|
+
prevVersionPrefix,
|
|
449
|
+
nextVersionPrefix,
|
|
450
|
+
routeDiff,
|
|
451
|
+
userApiKey,
|
|
452
|
+
openai,
|
|
453
|
+
openaiProxyHost
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
const fastifyPath = convertOpenApiToFastifyPath(openapiPath)
|
|
457
|
+
|
|
458
|
+
writer.write('fastify.route({').indent(() => {
|
|
459
|
+
writer.writeLine(`method: '${method.toUpperCase()}',`)
|
|
460
|
+
writer.writeLine(`url: '${fastifyPath}',`)
|
|
461
|
+
writer.writeLine(`schema: ${serializedFastifySchema},`)
|
|
462
|
+
writer.write(`handler: ${routeHandler}`)
|
|
463
|
+
}).write('})')
|
|
464
|
+
|
|
465
|
+
return writer.toString()
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
async function generateMapperPluginForDeletedRoute (
|
|
469
|
+
logger,
|
|
470
|
+
deletedRouteDiff,
|
|
471
|
+
prevVersionConfig,
|
|
472
|
+
nextVersionConfig
|
|
473
|
+
) {
|
|
474
|
+
const writer = new CodeBlockWriter({
|
|
475
|
+
newLine: '\n',
|
|
476
|
+
indentNumberOfSpaces: 2,
|
|
477
|
+
useTabs: false,
|
|
478
|
+
useSingleQuote: true
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
writer.writeLine('\'use strict\'')
|
|
482
|
+
writer.blankLine()
|
|
483
|
+
|
|
484
|
+
const method = deletedRouteDiff.method
|
|
485
|
+
const openapiPath = deletedRouteDiff.path
|
|
486
|
+
|
|
487
|
+
const routeMapper = await generateMapperForDeletedRoute(
|
|
488
|
+
logger,
|
|
489
|
+
method,
|
|
490
|
+
openapiPath,
|
|
491
|
+
prevVersionConfig,
|
|
492
|
+
nextVersionConfig,
|
|
493
|
+
deletedRouteDiff
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
writer.write('module.exports = async function (fastify, opts) {').indent(() => {
|
|
497
|
+
writer.write(routeMapper)
|
|
498
|
+
}).write('}\n')
|
|
499
|
+
return writer.toString()
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
async function generateMapperPluginForChangesRoute (
|
|
503
|
+
logger,
|
|
504
|
+
changedRouteDiff,
|
|
505
|
+
prevVersionConfig,
|
|
506
|
+
nextVersionConfig,
|
|
507
|
+
userApiKey,
|
|
508
|
+
openai,
|
|
509
|
+
openaiProxyHost
|
|
510
|
+
) {
|
|
511
|
+
const writer = new CodeBlockWriter({
|
|
512
|
+
newLine: '\n',
|
|
513
|
+
indentNumberOfSpaces: 2,
|
|
514
|
+
useTabs: false,
|
|
515
|
+
useSingleQuote: true
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
writer.writeLine('\'use strict\'')
|
|
519
|
+
writer.blankLine()
|
|
520
|
+
|
|
521
|
+
const method = changedRouteDiff.method
|
|
522
|
+
const openapiPath = changedRouteDiff.path
|
|
523
|
+
|
|
524
|
+
const routeMapper = await generateMapperForChangedRoute(
|
|
525
|
+
logger,
|
|
526
|
+
method,
|
|
527
|
+
openapiPath,
|
|
528
|
+
prevVersionConfig,
|
|
529
|
+
nextVersionConfig,
|
|
530
|
+
changedRouteDiff,
|
|
531
|
+
userApiKey,
|
|
532
|
+
openai,
|
|
533
|
+
openaiProxyHost
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
writer.write('module.exports = async function (fastify, opts) {').indent(() => {
|
|
537
|
+
writer.write(routeMapper)
|
|
538
|
+
}).write('}\n')
|
|
539
|
+
return writer.toString()
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function createProgressBar (count) {
|
|
543
|
+
const multibar = new cliProgress.MultiBar({
|
|
544
|
+
format: 'Routes generation: |' + green('{bar}') + '| {value}/{total} Generated routes | Time: {duration}s',
|
|
545
|
+
clearOnComplete: true,
|
|
546
|
+
stopOnComplete: true,
|
|
547
|
+
gracefulExit: true
|
|
548
|
+
}, cliProgress.Presets.shades_classic)
|
|
549
|
+
|
|
550
|
+
const progressBar = multibar.create(count, 0)
|
|
551
|
+
return {
|
|
552
|
+
increment: () => progressBar.increment(),
|
|
553
|
+
stop: () => {
|
|
554
|
+
progressBar.stop()
|
|
555
|
+
multibar.stop()
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function generatePluginName (method, openapiPath) {
|
|
561
|
+
const methodPrefix = method.toLowerCase()
|
|
562
|
+
const pathPrefix = openapiPath
|
|
563
|
+
.split('-')
|
|
564
|
+
.map(p => p.replace(/\//g, '-'))
|
|
565
|
+
.join('--')
|
|
566
|
+
|
|
567
|
+
return methodPrefix + pathPrefix + '.js'
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function generateOpenapiPath (pluginName) {
|
|
571
|
+
const index = pluginName.indexOf('-')
|
|
572
|
+
const methodPrefix = pluginName.slice(0, index)
|
|
573
|
+
const pathPrefix = pluginName.slice(index + 1, -3)
|
|
574
|
+
|
|
575
|
+
const method = methodPrefix.toLowerCase()
|
|
576
|
+
|
|
577
|
+
// Should map "-" to "/" only if there is no another "-" next to it or before it
|
|
578
|
+
const path = '/' + pathPrefix
|
|
579
|
+
.split('--')
|
|
580
|
+
.map(p => p.replace(/-/g, '/'))
|
|
581
|
+
.join('-')
|
|
582
|
+
|
|
583
|
+
return { method, path }
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
async function createMappersPlugins ({
|
|
587
|
+
logger,
|
|
588
|
+
configManager,
|
|
589
|
+
prevVersion,
|
|
590
|
+
nextVersion,
|
|
591
|
+
prevOpenapiSchema,
|
|
592
|
+
nextOpenapiSchema,
|
|
593
|
+
userApiKey = null,
|
|
594
|
+
openai = false,
|
|
595
|
+
openaiProxyHost = OPENAI_SERVICE_HOST
|
|
596
|
+
}) {
|
|
597
|
+
const config = configManager.current
|
|
598
|
+
|
|
599
|
+
const versions = config.versions ?? {}
|
|
600
|
+
const versionsConfigs = versions.configs ?? []
|
|
601
|
+
|
|
602
|
+
const prevVersionConfig = versionsConfigs.find(c => c.version === prevVersion)
|
|
603
|
+
const nextVersionConfig = versionsConfigs.find(c => c.version === nextVersion)
|
|
604
|
+
|
|
605
|
+
const prevNormalizedOpenapiSchema = changeOpenapiSchemaPrefix(
|
|
606
|
+
prevOpenapiSchema,
|
|
607
|
+
prevVersionConfig.openapi.prefix,
|
|
608
|
+
''
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
const nextNormalizedOpenapiSchema = changeOpenapiSchemaPrefix(
|
|
612
|
+
nextOpenapiSchema,
|
|
613
|
+
nextVersionConfig.openapi.prefix,
|
|
614
|
+
''
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
const schemasDiff = compareOpenApiSchemas(
|
|
618
|
+
prevNormalizedOpenapiSchema,
|
|
619
|
+
nextNormalizedOpenapiSchema
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
const modifiedRoutesCount =
|
|
623
|
+
schemasDiff.changedRoutes.length +
|
|
624
|
+
schemasDiff.deletedRoutes.length
|
|
625
|
+
|
|
626
|
+
if (modifiedRoutesCount === 0) {
|
|
627
|
+
logger.info(
|
|
628
|
+
`No changes found between "${prevVersion}" and "${nextVersion}" openapi schemas.` +
|
|
629
|
+
' Skipping mappers generation'
|
|
630
|
+
)
|
|
631
|
+
return null
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
logger.info(`Generating openapi mappers for "${prevVersion}" -> "${nextVersion}"`)
|
|
635
|
+
|
|
636
|
+
const progressBar = createProgressBar(modifiedRoutesCount)
|
|
637
|
+
const onRouteGenerated = async () => {
|
|
638
|
+
progressBar.increment()
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const mappersDir = join(versions.dir, prevVersionConfig.version, 'mappers')
|
|
642
|
+
const mappersDirExists = await isFileAccessible(mappersDir)
|
|
643
|
+
if (!mappersDirExists) {
|
|
644
|
+
logger.info(`Creating mappers directory for "${prevVersion}" -> "${nextVersion}"`)
|
|
645
|
+
await mkdir(mappersDir, { recursive: true })
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const genPluginsReqs = []
|
|
649
|
+
for (const deletedRouteDiff of schemasDiff.deletedRoutes) {
|
|
650
|
+
const method = deletedRouteDiff.method
|
|
651
|
+
const openapiPath = deletedRouteDiff.path
|
|
652
|
+
|
|
653
|
+
const mapperPluginFileName = generatePluginName(method, openapiPath)
|
|
654
|
+
const mapperPluginFilePath = join(mappersDir, mapperPluginFileName)
|
|
655
|
+
|
|
656
|
+
const generateMapperReq = generateMapperPluginForDeletedRoute(
|
|
657
|
+
logger,
|
|
658
|
+
deletedRouteDiff,
|
|
659
|
+
prevVersionConfig,
|
|
660
|
+
nextVersionConfig
|
|
661
|
+
)
|
|
662
|
+
.then(
|
|
663
|
+
async (mapperPlugin) => {
|
|
664
|
+
await writeFile(mapperPluginFilePath, mapperPlugin, 'utf8')
|
|
665
|
+
}
|
|
666
|
+
)
|
|
667
|
+
.then(
|
|
668
|
+
async () => {
|
|
669
|
+
await onRouteGenerated(method, openapiPath)
|
|
670
|
+
}
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
genPluginsReqs.push(generateMapperReq)
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
for (const changedRouteDiff of schemasDiff.changedRoutes) {
|
|
677
|
+
const method = changedRouteDiff.method
|
|
678
|
+
const openapiPath = changedRouteDiff.path
|
|
679
|
+
|
|
680
|
+
const mapperPluginFileName = generatePluginName(method, openapiPath)
|
|
681
|
+
const mapperPluginFilePath = join(mappersDir, mapperPluginFileName)
|
|
682
|
+
|
|
683
|
+
const generateMapperReq = generateMapperPluginForChangesRoute(
|
|
684
|
+
logger,
|
|
685
|
+
changedRouteDiff,
|
|
686
|
+
prevVersionConfig,
|
|
687
|
+
nextVersionConfig,
|
|
688
|
+
userApiKey,
|
|
689
|
+
openai,
|
|
690
|
+
openaiProxyHost
|
|
691
|
+
)
|
|
692
|
+
.then(
|
|
693
|
+
async (mapperPlugin) => {
|
|
694
|
+
await writeFile(mapperPluginFilePath, mapperPlugin, 'utf8')
|
|
695
|
+
}
|
|
696
|
+
)
|
|
697
|
+
.then(
|
|
698
|
+
async () => {
|
|
699
|
+
await onRouteGenerated(method, openapiPath)
|
|
700
|
+
}
|
|
701
|
+
)
|
|
702
|
+
|
|
703
|
+
genPluginsReqs.push(generateMapperReq)
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
await Promise.all(genPluginsReqs)
|
|
707
|
+
progressBar.stop()
|
|
708
|
+
|
|
709
|
+
const mappersPluginsNames = await readdir(mappersDir)
|
|
710
|
+
for (const mappersPluginName of mappersPluginsNames) {
|
|
711
|
+
const { method, path } = generateOpenapiPath(mappersPluginName)
|
|
712
|
+
|
|
713
|
+
const isDeletedRoute = schemasDiff.deletedRoutes.some(
|
|
714
|
+
routeDiff => {
|
|
715
|
+
return routeDiff.method === method && routeDiff.path === path
|
|
716
|
+
}
|
|
717
|
+
)
|
|
718
|
+
const isChangedRoute = schemasDiff.changedRoutes.some(
|
|
719
|
+
routeDiff => {
|
|
720
|
+
return routeDiff.method === method && routeDiff.path === path
|
|
721
|
+
}
|
|
722
|
+
)
|
|
723
|
+
if (!isDeletedRoute && !isChangedRoute) {
|
|
724
|
+
logger.info(`Removing obsolete mappers plugin "${mappersPluginName}"`)
|
|
725
|
+
const mapperPluginPath = join(mappersDir, mappersPluginName)
|
|
726
|
+
await rm(mapperPluginPath, { force: true })
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
logger.info(`Updating "${prevVersion}" version config with mappers plugin path`)
|
|
731
|
+
await addMappersToConfig(mappersDir, prevVersionConfig.version, configManager)
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
async function addMappersToConfig (mappersDir, version, configManager) {
|
|
735
|
+
const config = configManager.current
|
|
736
|
+
const prevVersionConfig = config.versions.configs.find(c => c.version === version)
|
|
737
|
+
|
|
738
|
+
const pluginsPaths = prevVersionConfig.plugins?.paths ?? []
|
|
739
|
+
for (let foundPluginPath of pluginsPaths) {
|
|
740
|
+
foundPluginPath = typeof foundPluginPath === 'string'
|
|
741
|
+
? foundPluginPath
|
|
742
|
+
: foundPluginPath.path
|
|
743
|
+
|
|
744
|
+
if (mappersDir.startsWith(foundPluginPath)) return
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const metaConfig = await analyze({ file: configManager.fullPath })
|
|
748
|
+
const rawConfig = metaConfig.config
|
|
749
|
+
const rawPrevVersionConfig = rawConfig.versions.configs.find(c => c.version === version)
|
|
750
|
+
|
|
751
|
+
const relativePath = relative(configManager.dirname, mappersDir)
|
|
752
|
+
if (!prevVersionConfig.plugins) {
|
|
753
|
+
prevVersionConfig.plugins = { paths: [] }
|
|
754
|
+
rawPrevVersionConfig.plugins = { paths: [] }
|
|
755
|
+
}
|
|
756
|
+
prevVersionConfig.plugins.paths.push(relativePath)
|
|
757
|
+
rawPrevVersionConfig.plugins.paths.push(relativePath)
|
|
758
|
+
await Promise.all([configManager.update(), writeConfig(metaConfig)])
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
async function execute ({
|
|
762
|
+
logger,
|
|
763
|
+
configManager,
|
|
764
|
+
userApiKey,
|
|
765
|
+
openai,
|
|
766
|
+
openaiProxyHost
|
|
767
|
+
}) {
|
|
768
|
+
const config = configManager.current
|
|
769
|
+
|
|
770
|
+
const versions = config.versions ?? {}
|
|
771
|
+
const versionsConfigs = versions.configs ?? []
|
|
772
|
+
|
|
773
|
+
const nextVersionConfig = versionsConfigs.at(-1)
|
|
774
|
+
if (!nextVersionConfig) {
|
|
775
|
+
logger.info('No versions found. Skipping version update.')
|
|
776
|
+
return
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const nextVersion = nextVersionConfig.version
|
|
780
|
+
const nextOpenapiSchemaPath = nextVersionConfig.openapi.path
|
|
781
|
+
|
|
782
|
+
logger.info(`Loading openapi schema for "${nextVersion}"`)
|
|
783
|
+
const nextOpenapiSchema = await getOpenapiSchema({
|
|
784
|
+
logger,
|
|
785
|
+
configManager,
|
|
786
|
+
version: nextVersionConfig.version
|
|
787
|
+
})
|
|
788
|
+
logger.info(`Updating openapi schema for "${nextVersion}"`)
|
|
789
|
+
await writeFile(nextOpenapiSchemaPath, JSON.stringify(nextOpenapiSchema, null, 2), 'utf8')
|
|
790
|
+
|
|
791
|
+
const prevVersionConfig = versionsConfigs.at(-2)
|
|
792
|
+
if (!prevVersionConfig) {
|
|
793
|
+
logger.info('No previous versions found. Skipping mappers generation.')
|
|
794
|
+
return
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
const prevVersion = prevVersionConfig.version
|
|
798
|
+
const prevOpenapiSchemaPath = prevVersionConfig.openapi.path
|
|
799
|
+
|
|
800
|
+
logger.info(`Reading openapi schema for "${prevVersion}"`)
|
|
801
|
+
const prevOpenapiSchemaFile = await readFile(prevOpenapiSchemaPath, 'utf8')
|
|
802
|
+
const prevOpenapiSchema = JSON.parse(prevOpenapiSchemaFile)
|
|
803
|
+
|
|
804
|
+
await createMappersPlugins({
|
|
805
|
+
logger,
|
|
806
|
+
configManager,
|
|
807
|
+
prevVersion,
|
|
808
|
+
nextVersion,
|
|
809
|
+
prevOpenapiSchema,
|
|
810
|
+
nextOpenapiSchema,
|
|
811
|
+
userApiKey,
|
|
812
|
+
openai,
|
|
813
|
+
openaiProxyHost
|
|
814
|
+
})
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
async function updateVersion (_args) {
|
|
818
|
+
const logger = pino(pretty({
|
|
819
|
+
translateTime: 'SYS:HH:MM:ss',
|
|
820
|
+
ignore: 'hostname,pid'
|
|
821
|
+
}))
|
|
822
|
+
|
|
823
|
+
try {
|
|
824
|
+
const { configManager, args } = await loadConfig({
|
|
825
|
+
string: ['openai-proxy-host', 'user-api-key'],
|
|
826
|
+
boolean: ['openai']
|
|
827
|
+
}, _args, platformaticService)
|
|
828
|
+
await configManager.parseAndValidate()
|
|
829
|
+
|
|
830
|
+
const openai = args.openai ?? false
|
|
831
|
+
const openaiProxyHost = args['openai-proxy-host'] ?? OPENAI_SERVICE_HOST
|
|
832
|
+
|
|
833
|
+
let userApiKey = args['user-api-key'] ?? null
|
|
834
|
+
/* c8 ignore next 10 */
|
|
835
|
+
if (!userApiKey && openai) {
|
|
836
|
+
logger.info('Reading platformatic user api key')
|
|
837
|
+
const { getUserApiKey } = await import('@platformatic/authenticate')
|
|
838
|
+
try {
|
|
839
|
+
userApiKey = await getUserApiKey()
|
|
840
|
+
} catch (err) {
|
|
841
|
+
logger.error('Failed to read user api key. Please run "plt login" command.')
|
|
842
|
+
return
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
await execute({
|
|
847
|
+
logger,
|
|
848
|
+
configManager,
|
|
849
|
+
userApiKey,
|
|
850
|
+
openai,
|
|
851
|
+
openaiProxyHost
|
|
852
|
+
})
|
|
853
|
+
|
|
854
|
+
// TODO: find out why process stucks sometimes
|
|
855
|
+
process.exit(0)
|
|
856
|
+
} catch (err) {
|
|
857
|
+
logger.error(err.message)
|
|
858
|
+
process.exit(1)
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
module.exports = { execute, updateVersion, createMappersPlugins }
|