@platformatic/composer 3.4.1 → 3.5.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/LICENSE +1 -1
- package/config.d.ts +482 -131
- package/eslint.config.js +4 -2
- package/index.d.ts +1 -17
- package/index.js +9 -210
- package/package.json +18 -59
- package/schema.json +2121 -843
- package/scripts/schema.js +12 -0
- package/.c8rc +0 -6
- package/composer.mjs +0 -54
- package/help/create.txt +0 -11
- package/help/help.txt +0 -7
- package/help/openapi schemas fetch.txt +0 -9
- package/help/start.txt +0 -54
- package/index.test-d.ts +0 -23
- package/lib/composer-hook.js +0 -60
- package/lib/create.mjs +0 -84
- package/lib/errors.js +0 -13
- package/lib/generator/README.md +0 -30
- package/lib/generator/composer-generator.d.ts +0 -11
- package/lib/generator/composer-generator.js +0 -128
- package/lib/graphql-fetch.js +0 -85
- package/lib/graphql-generator.js +0 -31
- package/lib/graphql.js +0 -20
- package/lib/openapi-composer.js +0 -81
- package/lib/openapi-config-schema.js +0 -93
- package/lib/openapi-fetch-schemas.mjs +0 -61
- package/lib/openapi-generator.js +0 -167
- package/lib/openapi-load-config.js +0 -31
- package/lib/openapi-modifier.js +0 -137
- package/lib/openapi.js +0 -49
- package/lib/proxy.js +0 -161
- package/lib/root-endpoint/index.js +0 -28
- package/lib/root-endpoint/public/images/dark_mode.svg +0 -3
- package/lib/root-endpoint/public/images/favicon.ico +0 -0
- package/lib/root-endpoint/public/images/light_mode.svg +0 -11
- package/lib/root-endpoint/public/images/platformatic-logo-dark.svg +0 -30
- package/lib/root-endpoint/public/images/platformatic-logo-light.svg +0 -30
- package/lib/root-endpoint/public/images/triangle_dark.svg +0 -3
- package/lib/root-endpoint/public/images/triangle_light.svg +0 -3
- package/lib/root-endpoint/public/index.html +0 -237
- package/lib/schema.js +0 -210
- package/lib/stackable.js +0 -59
- package/lib/upgrade.js +0 -22
- package/lib/utils.js +0 -27
- package/lib/versions/2.0.0.js +0 -11
package/lib/openapi-composer.js
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const clone = require('rfdc')()
|
|
4
|
-
const errors = require('./errors')
|
|
5
|
-
|
|
6
|
-
function composeOpenApi (apis, options = {}) {
|
|
7
|
-
const mergedPaths = {}
|
|
8
|
-
const mergedSchemas = {}
|
|
9
|
-
|
|
10
|
-
for (const { id, prefix, schema } of apis) {
|
|
11
|
-
const { paths, components } = clone(schema)
|
|
12
|
-
|
|
13
|
-
const apiPrefix = generateOperationIdApiPrefix(id)
|
|
14
|
-
for (const [path, pathSchema] of Object.entries(paths)) {
|
|
15
|
-
namespaceSchemaRefs(apiPrefix, pathSchema)
|
|
16
|
-
namespaceSchemaOperationIds(apiPrefix, pathSchema)
|
|
17
|
-
|
|
18
|
-
const mergedPath = prefix ? prefix + path : path
|
|
19
|
-
|
|
20
|
-
if (mergedPaths[mergedPath]) {
|
|
21
|
-
throw new errors.PathAlreadyExistsError(mergedPath)
|
|
22
|
-
}
|
|
23
|
-
mergedPaths[mergedPath] = pathSchema
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (components && components.schemas) {
|
|
27
|
-
for (const [schemaKey, schema] of Object.entries(components.schemas)) {
|
|
28
|
-
if (schema.title == null) {
|
|
29
|
-
schema.title = schemaKey
|
|
30
|
-
}
|
|
31
|
-
namespaceSchemaRefs(apiPrefix, schema)
|
|
32
|
-
mergedSchemas[apiPrefix + schemaKey] = schema
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return {
|
|
38
|
-
openapi: '3.0.0',
|
|
39
|
-
info: {
|
|
40
|
-
title: options.title || 'Platformatic Composer',
|
|
41
|
-
version: options.version || '1.0.0',
|
|
42
|
-
},
|
|
43
|
-
components: {
|
|
44
|
-
schemas: mergedSchemas,
|
|
45
|
-
},
|
|
46
|
-
paths: mergedPaths,
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function generateOperationIdApiPrefix (operationId) {
|
|
51
|
-
return operationId.trim()
|
|
52
|
-
.replace(/[^A-Z0-9]+/ig, '_')
|
|
53
|
-
.replace(/^_+|_+$/g, '') + '_'
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function namespaceSchemaRefs (apiPrefix, schema) {
|
|
57
|
-
if (schema.$ref && schema.$ref.startsWith('#/components/schemas')) {
|
|
58
|
-
schema.$ref = schema.$ref.replace(
|
|
59
|
-
'#/components/schemas/',
|
|
60
|
-
'#/components/schemas/' + apiPrefix
|
|
61
|
-
)
|
|
62
|
-
}
|
|
63
|
-
for (const childSchema of Object.values(schema)) {
|
|
64
|
-
if (typeof childSchema === 'object') {
|
|
65
|
-
namespaceSchemaRefs(apiPrefix, childSchema)
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function namespaceSchemaOperationIds (apiPrefix, schema) {
|
|
71
|
-
if (schema.operationId) {
|
|
72
|
-
schema.operationId = apiPrefix + schema.operationId
|
|
73
|
-
}
|
|
74
|
-
for (const childSchema of Object.values(schema)) {
|
|
75
|
-
if (typeof childSchema === 'object') {
|
|
76
|
-
namespaceSchemaOperationIds(apiPrefix, childSchema)
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
module.exports = composeOpenApi
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const ignoreSchema = {
|
|
4
|
-
type: 'object',
|
|
5
|
-
properties: {
|
|
6
|
-
ignore: { type: 'boolean' },
|
|
7
|
-
},
|
|
8
|
-
additionalProperties: false,
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const aliasSchema = {
|
|
12
|
-
type: 'object',
|
|
13
|
-
properties: {
|
|
14
|
-
alias: { type: 'string' },
|
|
15
|
-
},
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const jsonSchemaSchema = {
|
|
19
|
-
$id: 'json-schema',
|
|
20
|
-
type: 'object',
|
|
21
|
-
properties: {
|
|
22
|
-
type: { type: 'string' },
|
|
23
|
-
properties: {
|
|
24
|
-
type: 'object',
|
|
25
|
-
additionalProperties: {
|
|
26
|
-
oneOf: [
|
|
27
|
-
{ $ref: 'json-schema' },
|
|
28
|
-
{
|
|
29
|
-
type: 'object',
|
|
30
|
-
properties: {
|
|
31
|
-
rename: { type: 'string' },
|
|
32
|
-
},
|
|
33
|
-
additionalProperties: false,
|
|
34
|
-
},
|
|
35
|
-
],
|
|
36
|
-
},
|
|
37
|
-
},
|
|
38
|
-
items: { $ref: 'json-schema' },
|
|
39
|
-
},
|
|
40
|
-
additionalProperties: false,
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const routeSchema = {
|
|
44
|
-
anyOf: [
|
|
45
|
-
ignoreSchema,
|
|
46
|
-
{
|
|
47
|
-
type: 'object',
|
|
48
|
-
properties: {
|
|
49
|
-
responses: {
|
|
50
|
-
type: 'object',
|
|
51
|
-
properties: {
|
|
52
|
-
200: { $ref: 'json-schema' },
|
|
53
|
-
},
|
|
54
|
-
},
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
],
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const openApiConfigSchema = {
|
|
61
|
-
type: 'object',
|
|
62
|
-
properties: {
|
|
63
|
-
paths: {
|
|
64
|
-
type: 'object',
|
|
65
|
-
additionalProperties: {
|
|
66
|
-
anyOf: [
|
|
67
|
-
ignoreSchema,
|
|
68
|
-
aliasSchema,
|
|
69
|
-
{
|
|
70
|
-
type: 'object',
|
|
71
|
-
properties: {
|
|
72
|
-
get: routeSchema,
|
|
73
|
-
post: routeSchema,
|
|
74
|
-
put: routeSchema,
|
|
75
|
-
patch: routeSchema,
|
|
76
|
-
delete: routeSchema,
|
|
77
|
-
options: routeSchema,
|
|
78
|
-
head: routeSchema,
|
|
79
|
-
trace: routeSchema,
|
|
80
|
-
},
|
|
81
|
-
additionalProperties: false,
|
|
82
|
-
},
|
|
83
|
-
],
|
|
84
|
-
},
|
|
85
|
-
},
|
|
86
|
-
},
|
|
87
|
-
additionalProperties: false,
|
|
88
|
-
definitions: {
|
|
89
|
-
'json-schema': jsonSchemaSchema,
|
|
90
|
-
},
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
module.exports = openApiConfigSchema
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { writeFile } from 'node:fs/promises'
|
|
2
|
-
|
|
3
|
-
import pino from 'pino'
|
|
4
|
-
import pretty from 'pino-pretty'
|
|
5
|
-
import { request } from 'undici'
|
|
6
|
-
|
|
7
|
-
import { loadConfig } from '@platformatic/config'
|
|
8
|
-
import { platformaticComposer } from '../index.js'
|
|
9
|
-
import errors from '../lib/errors.js'
|
|
10
|
-
import { prefixWithSlash } from './utils.js'
|
|
11
|
-
|
|
12
|
-
async function fetchOpenApiSchema (service) {
|
|
13
|
-
const { origin, openapi } = service
|
|
14
|
-
|
|
15
|
-
const openApiUrl = origin + prefixWithSlash(openapi.url)
|
|
16
|
-
const { statusCode, body } = await request(openApiUrl)
|
|
17
|
-
|
|
18
|
-
if (statusCode !== 200 && statusCode !== 201) {
|
|
19
|
-
throw new errors.FailedToFetchOpenAPISchemaError(openApiUrl)
|
|
20
|
-
}
|
|
21
|
-
const schema = await body.json()
|
|
22
|
-
|
|
23
|
-
if (openapi.file !== undefined) {
|
|
24
|
-
await writeFile(openapi.file, JSON.stringify(schema, null, 2))
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return schema
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export default async function fetchOpenApiSchemas (_args) {
|
|
31
|
-
const logger = pino(pretty({
|
|
32
|
-
translateTime: 'SYS:HH:MM:ss',
|
|
33
|
-
ignore: 'hostname,pid',
|
|
34
|
-
}))
|
|
35
|
-
|
|
36
|
-
const { configManager } = await loadConfig({}, _args, platformaticComposer)
|
|
37
|
-
await configManager.parseAndValidate()
|
|
38
|
-
const config = configManager.current
|
|
39
|
-
const { services } = config.composer
|
|
40
|
-
|
|
41
|
-
const servicesWithValidOpenApi = services
|
|
42
|
-
.filter(({ openapi }) => openapi && openapi.url && openapi.file)
|
|
43
|
-
|
|
44
|
-
const fetchOpenApiRequests = servicesWithValidOpenApi
|
|
45
|
-
.map(service => fetchOpenApiSchema(service))
|
|
46
|
-
|
|
47
|
-
const fetchOpenApiResults = await Promise.allSettled(fetchOpenApiRequests)
|
|
48
|
-
fetchOpenApiResults.forEach((result, index) => {
|
|
49
|
-
const serviceId = servicesWithValidOpenApi[index].id
|
|
50
|
-
if (result.status === 'rejected') {
|
|
51
|
-
logger.error(`Failed to fetch OpenAPI schema for service with id ${serviceId}: ${result.reason}`)
|
|
52
|
-
} else {
|
|
53
|
-
logger.info(`Successfully fetched OpenAPI schema for service with id ${serviceId}`)
|
|
54
|
-
}
|
|
55
|
-
})
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export {
|
|
59
|
-
fetchOpenApiSchema,
|
|
60
|
-
fetchOpenApiSchemas,
|
|
61
|
-
}
|
package/lib/openapi-generator.js
DELETED
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const { readFile } = require('node:fs/promises')
|
|
4
|
-
const { request, getGlobalDispatcher } = require('undici')
|
|
5
|
-
const fp = require('fastify-plugin')
|
|
6
|
-
const errors = require('./errors')
|
|
7
|
-
|
|
8
|
-
const { modifyOpenApiSchema, originPathSymbol } = require('./openapi-modifier')
|
|
9
|
-
const composeOpenApi = require('./openapi-composer')
|
|
10
|
-
const loadOpenApiConfig = require('./openapi-load-config.js')
|
|
11
|
-
const { prefixWithSlash } = require('./utils.js')
|
|
12
|
-
|
|
13
|
-
async function fetchOpenApiSchema (openApiUrl) {
|
|
14
|
-
const { body } = await request(openApiUrl)
|
|
15
|
-
return body.json()
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async function readOpenApiSchema (pathToSchema) {
|
|
19
|
-
const schemaFile = await readFile(pathToSchema, 'utf-8')
|
|
20
|
-
return JSON.parse(schemaFile)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
async function getOpenApiSchema (origin, openapi) {
|
|
24
|
-
if (openapi.url) {
|
|
25
|
-
const openApiUrl = origin + prefixWithSlash(openapi.url)
|
|
26
|
-
return fetchOpenApiSchema(openApiUrl)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return readOpenApiSchema(openapi.file)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async function composeOpenAPI (app, opts) {
|
|
33
|
-
if (!opts.services.some(s => s.openapi)) { return }
|
|
34
|
-
|
|
35
|
-
const { services } = opts
|
|
36
|
-
|
|
37
|
-
const openApiSchemas = []
|
|
38
|
-
const apiByApiRoutes = {}
|
|
39
|
-
|
|
40
|
-
for (const { id, origin, openapi } of services) {
|
|
41
|
-
if (!openapi) continue
|
|
42
|
-
|
|
43
|
-
let openapiConfig = null
|
|
44
|
-
if (openapi.config) {
|
|
45
|
-
try {
|
|
46
|
-
openapiConfig = await loadOpenApiConfig(openapi.config)
|
|
47
|
-
} catch (error) {
|
|
48
|
-
app.log.error(error)
|
|
49
|
-
throw new errors.CouldNotReadOpenAPIConfigError(id)
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
let originSchema = null
|
|
54
|
-
try {
|
|
55
|
-
originSchema = await getOpenApiSchema(origin, openapi)
|
|
56
|
-
} catch (error) {
|
|
57
|
-
app.log.error(error, `failed to fetch schema for "${id} service"`)
|
|
58
|
-
continue
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const schema = modifyOpenApiSchema(app, originSchema, openapiConfig)
|
|
62
|
-
|
|
63
|
-
const prefix = openapi.prefix ? prefixWithSlash(openapi.prefix) : ''
|
|
64
|
-
for (const path in schema.paths) {
|
|
65
|
-
apiByApiRoutes[prefix + path] = {
|
|
66
|
-
origin,
|
|
67
|
-
prefix,
|
|
68
|
-
schema: schema.paths[path],
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
openApiSchemas.push({ id, prefix, schema, originSchema, config: openapiConfig })
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
app.decorate('openApiSchemas', openApiSchemas)
|
|
76
|
-
|
|
77
|
-
const composedOpenApiSchema = composeOpenApi(openApiSchemas, opts.openapi)
|
|
78
|
-
|
|
79
|
-
const dispatcher = getGlobalDispatcher()
|
|
80
|
-
|
|
81
|
-
await app.register(require('@fastify/reply-from'), {
|
|
82
|
-
undici: dispatcher,
|
|
83
|
-
destroyAgent: false,
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
await app.register(await import('fastify-openapi-glue'), {
|
|
87
|
-
specification: composedOpenApiSchema,
|
|
88
|
-
addEmptySchema: opts.addEmptySchema,
|
|
89
|
-
operationResolver: (operationId, method, openApiPath) => {
|
|
90
|
-
const { origin, prefix, schema } = apiByApiRoutes[openApiPath]
|
|
91
|
-
const originPath = schema[originPathSymbol]
|
|
92
|
-
|
|
93
|
-
const mapRoutePath = createPathMapper(originPath, openApiPath, prefix)
|
|
94
|
-
|
|
95
|
-
return {
|
|
96
|
-
config: { openApiPath },
|
|
97
|
-
handler: (req, reply) => {
|
|
98
|
-
const routePath = req.raw.url.split('?')[0]
|
|
99
|
-
const newRoutePath = mapRoutePath(routePath)
|
|
100
|
-
|
|
101
|
-
const replyOptions = {}
|
|
102
|
-
const onResponse = (request, reply, { stream }) => {
|
|
103
|
-
app.openTelemetry?.endHTTPSpanClient(reply.request.proxedCallSpan, { statusCode: reply.statusCode })
|
|
104
|
-
if (req.routeOptions.config?.onComposerResponse) {
|
|
105
|
-
req.routeOptions.config?.onComposerResponse(request, reply, stream)
|
|
106
|
-
} else {
|
|
107
|
-
reply.send(stream)
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
const rewriteRequestHeaders = (request, headers) => {
|
|
111
|
-
const targetUrl = `${origin}${request.url}`
|
|
112
|
-
const context = request.span?.context
|
|
113
|
-
const { span, telemetryHeaders } = app.openTelemetry?.startHTTPSpanClient(targetUrl, request.method, context) || { span: null, telemetryHeaders: {} }
|
|
114
|
-
// We need to store the span in a different object
|
|
115
|
-
// to correctly close it in the onResponse hook
|
|
116
|
-
// Note that we have 2 spans:
|
|
117
|
-
// - request.span: the span of the request to the proxy
|
|
118
|
-
// - request.proxedCallSpan: the span of the request to the proxied service
|
|
119
|
-
request.proxedCallSpan = span
|
|
120
|
-
|
|
121
|
-
headers = {
|
|
122
|
-
...headers,
|
|
123
|
-
...telemetryHeaders,
|
|
124
|
-
'x-forwarded-for': request.ip,
|
|
125
|
-
'x-forwarded-host': request.host,
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return headers
|
|
129
|
-
}
|
|
130
|
-
replyOptions.onResponse = onResponse
|
|
131
|
-
replyOptions.rewriteRequestHeaders = rewriteRequestHeaders
|
|
132
|
-
|
|
133
|
-
reply.from(origin + newRoutePath, replyOptions)
|
|
134
|
-
},
|
|
135
|
-
}
|
|
136
|
-
},
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
app.addHook('preValidation', async (req) => {
|
|
140
|
-
if (typeof req.query.fields === 'string') {
|
|
141
|
-
req.query.fields = req.query.fields.split(',')
|
|
142
|
-
}
|
|
143
|
-
})
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function createPathMapper (originOpenApiPath, renamedOpenApiPath, prefix) {
|
|
147
|
-
if (prefix + originOpenApiPath === renamedOpenApiPath) {
|
|
148
|
-
return (path) => path.slice(prefix.length)
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const extractParamsRegexp = generateRouteRegex(renamedOpenApiPath)
|
|
152
|
-
return (path) => {
|
|
153
|
-
const routeParams = path.match(extractParamsRegexp).slice(1)
|
|
154
|
-
return generateRenamedPath(originOpenApiPath, routeParams)
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function generateRouteRegex (route) {
|
|
159
|
-
const regex = route.replace(/{(.*?)}/g, '(.*)')
|
|
160
|
-
return new RegExp(regex)
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function generateRenamedPath (renamedOpenApiPath, routeParams) {
|
|
164
|
-
return renamedOpenApiPath.replace(/{(.*?)}/g, () => routeParams.shift())
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
module.exports = fp(composeOpenAPI)
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const { readFile } = require('node:fs/promises')
|
|
4
|
-
const Ajv = require('ajv')
|
|
5
|
-
const openApiConfigSchema = require('./openapi-config-schema')
|
|
6
|
-
const errors = require('./errors')
|
|
7
|
-
|
|
8
|
-
const ajv = new Ajv()
|
|
9
|
-
const ajvValidate = ajv.compile(openApiConfigSchema)
|
|
10
|
-
|
|
11
|
-
async function loadOpenApiConfig (pathToConfig) {
|
|
12
|
-
const openApiConfigFile = await readFile(pathToConfig, 'utf-8')
|
|
13
|
-
const openApiConfig = JSON.parse(openApiConfigFile)
|
|
14
|
-
|
|
15
|
-
if (!ajvValidate(openApiConfig)) {
|
|
16
|
-
const validationErrors = ajvValidate.errors.map((err) => {
|
|
17
|
-
return {
|
|
18
|
-
/* c8 ignore next 1 */
|
|
19
|
-
path: err.instancePath === '' ? '/' : err.instancePath,
|
|
20
|
-
message: err.message + ' ' + JSON.stringify(err.params),
|
|
21
|
-
}
|
|
22
|
-
})
|
|
23
|
-
throw new errors.ValidationErrors(validationErrors.map((err) => {
|
|
24
|
-
return err.message
|
|
25
|
-
}).join('\n'))
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return openApiConfig
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
module.exports = loadOpenApiConfig
|
package/lib/openapi-modifier.js
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const traverse = require('json-schema-traverse')
|
|
4
|
-
const clone = require('rfdc')()
|
|
5
|
-
|
|
6
|
-
const originPathSymbol = Symbol('originPath')
|
|
7
|
-
const MODIFICATION_KEYWORDS = ['rename']
|
|
8
|
-
|
|
9
|
-
function findDataBySchemaPointer (schemaPointer, schema, data, parentData, callback) {
|
|
10
|
-
const schemaPointerParts = schemaPointer.split('/').slice(1)
|
|
11
|
-
|
|
12
|
-
for (const schemaPointerPart of schemaPointerParts) {
|
|
13
|
-
parentData = data
|
|
14
|
-
schema = schema[schemaPointerPart]
|
|
15
|
-
|
|
16
|
-
if (schemaPointerPart === 'properties') continue
|
|
17
|
-
|
|
18
|
-
if (schemaPointerPart === 'items') {
|
|
19
|
-
for (const item of data) {
|
|
20
|
-
const newSchemaPointer = '/' + schemaPointerParts.slice(1).join('/')
|
|
21
|
-
findDataBySchemaPointer(newSchemaPointer, schema, item, parentData, callback)
|
|
22
|
-
}
|
|
23
|
-
return
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
data = data[schemaPointerPart]
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
callback(data, parentData)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function getModificationRules (modificationSchema) {
|
|
33
|
-
const modificationRules = {}
|
|
34
|
-
|
|
35
|
-
function getModificationRules (schema, jsonPointer) {
|
|
36
|
-
const schemaKeys = Object.keys(schema)
|
|
37
|
-
const modificationKeys = schemaKeys.filter(
|
|
38
|
-
key => MODIFICATION_KEYWORDS.includes(key)
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
if (modificationKeys.length === 0) return
|
|
42
|
-
modificationRules[jsonPointer] = schema
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
traverse(modificationSchema, { cb: getModificationRules })
|
|
46
|
-
return modificationRules
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function modifySchema (originSchema, modificationRules) {
|
|
50
|
-
function modifyOriginSchema (schema, jsonPointer, rs, psp, pk, parentSchema, keyIndex) {
|
|
51
|
-
const modificationRule = modificationRules[jsonPointer]
|
|
52
|
-
if (!modificationRule) return
|
|
53
|
-
|
|
54
|
-
if (modificationRule.rename) {
|
|
55
|
-
parentSchema.properties[modificationRule.rename] = schema
|
|
56
|
-
delete parentSchema.properties[keyIndex]
|
|
57
|
-
|
|
58
|
-
if (parentSchema.required) {
|
|
59
|
-
const index = parentSchema.required.indexOf(keyIndex)
|
|
60
|
-
if (index !== -1) {
|
|
61
|
-
parentSchema.required[index] = modificationRule.rename
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
traverse(originSchema, { cb: modifyOriginSchema })
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function modifyPayload (payload, originSchema, modificationRules) {
|
|
70
|
-
for (const schemaJsonPointer in modificationRules) {
|
|
71
|
-
const rule = modificationRules[schemaJsonPointer]
|
|
72
|
-
|
|
73
|
-
findDataBySchemaPointer(
|
|
74
|
-
schemaJsonPointer,
|
|
75
|
-
originSchema,
|
|
76
|
-
payload,
|
|
77
|
-
null,
|
|
78
|
-
(data, parentData) => {
|
|
79
|
-
if (rule.rename) {
|
|
80
|
-
parentData[rule.rename] = data
|
|
81
|
-
delete parentData[schemaJsonPointer.split('/').pop()]
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
)
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function modifyOpenApiSchema (app, schema, config) {
|
|
89
|
-
const newSchemaPaths = {}
|
|
90
|
-
const { paths } = clone(schema)
|
|
91
|
-
|
|
92
|
-
for (let path in paths) {
|
|
93
|
-
const pathConfig = config?.paths?.[path]
|
|
94
|
-
const pathSchema = paths[path]
|
|
95
|
-
|
|
96
|
-
if (pathConfig?.ignore) continue
|
|
97
|
-
|
|
98
|
-
pathSchema[originPathSymbol] = path
|
|
99
|
-
|
|
100
|
-
if (pathConfig?.alias) {
|
|
101
|
-
path = pathConfig.alias
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
for (const method in pathSchema) {
|
|
105
|
-
const routeConfig = pathConfig?.[method.toLowerCase()]
|
|
106
|
-
|
|
107
|
-
if (routeConfig?.ignore) {
|
|
108
|
-
delete pathSchema[method]
|
|
109
|
-
continue
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const modificationResponseSchema = routeConfig?.responses?.['200']
|
|
113
|
-
if (!modificationResponseSchema) continue
|
|
114
|
-
|
|
115
|
-
const modificationRules = getModificationRules(modificationResponseSchema)
|
|
116
|
-
|
|
117
|
-
app.platformatic.addComposerOnRouteHook(path, [method], routeOptions => {
|
|
118
|
-
const routeSchema = routeOptions.schema
|
|
119
|
-
const responseSchema = routeSchema.response?.['200']
|
|
120
|
-
modifySchema(responseSchema, modificationRules)
|
|
121
|
-
|
|
122
|
-
async function onComposerResponse (request, reply, body) {
|
|
123
|
-
const payload = await body.json()
|
|
124
|
-
modifyPayload(payload, responseSchema, modificationRules)
|
|
125
|
-
reply.send(payload)
|
|
126
|
-
}
|
|
127
|
-
routeOptions.config.onComposerResponse = onComposerResponse
|
|
128
|
-
})
|
|
129
|
-
}
|
|
130
|
-
if (Object.keys(pathSchema).length === 0) continue
|
|
131
|
-
newSchemaPaths[path] = pathSchema
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return { ...schema, paths: newSchemaPaths }
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
module.exports = { modifyOpenApiSchema, originPathSymbol }
|
package/lib/openapi.js
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const fp = require('fastify-plugin')
|
|
4
|
-
|
|
5
|
-
async function composeOpenAPI (app, opts) {
|
|
6
|
-
await app.register(require('@fastify/swagger'), {
|
|
7
|
-
exposeRoute: true,
|
|
8
|
-
openapi: {
|
|
9
|
-
info: {
|
|
10
|
-
title: opts.openapi?.title || 'Platformatic Composer',
|
|
11
|
-
version: opts.openapi?.version || '1.0.0'
|
|
12
|
-
}
|
|
13
|
-
},
|
|
14
|
-
transform: ({ schema, url }) => {
|
|
15
|
-
for (const service of opts.services) {
|
|
16
|
-
if (!service.proxy) continue
|
|
17
|
-
|
|
18
|
-
const prefix = service.proxy.prefix ?? ''
|
|
19
|
-
const proxyPrefix = prefix.at(-1) === '/' ? prefix.slice(0, -1) : prefix
|
|
20
|
-
|
|
21
|
-
const proxyUrls = [proxyPrefix + '/', proxyPrefix + '/*']
|
|
22
|
-
if (proxyUrls.includes(url)) {
|
|
23
|
-
schema = schema ?? {}
|
|
24
|
-
schema.hide = true
|
|
25
|
-
break
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return { schema, url }
|
|
29
|
-
}
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
const { default: scalarTheme } = await import('@platformatic/scalar-theme')
|
|
33
|
-
|
|
34
|
-
/** Serve spec file in yaml and json */
|
|
35
|
-
app.get('/documentation/json', { schema: { hide: true } }, async () => app.swagger())
|
|
36
|
-
app.get('/documentation/yaml', { schema: { hide: true } }, async () => app.swagger({ yaml: true }))
|
|
37
|
-
|
|
38
|
-
const routePrefix = opts.openapi?.swaggerPrefix || '/documentation'
|
|
39
|
-
|
|
40
|
-
await app.register(require('@scalar/fastify-api-reference'), {
|
|
41
|
-
logLevel: 'warn',
|
|
42
|
-
routePrefix,
|
|
43
|
-
configuration: {
|
|
44
|
-
customCss: scalarTheme.theme
|
|
45
|
-
}
|
|
46
|
-
})
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
module.exports = fp(composeOpenAPI)
|