@platformatic/composer 3.0.0-alpha.4 → 3.0.0-alpha.6
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/eslint.config.js +1 -8
- package/index.d.ts +1 -58
- package/index.js +9 -30
- package/package.json +8 -54
- package/schema.json +1136 -907
- package/scripts/schema.js +12 -0
- package/config.d.ts +0 -997
- package/lib/application.js +0 -186
- package/lib/commands/index.js +0 -15
- package/lib/commands/openapi-fetch-schemas.js +0 -47
- package/lib/composer-hook.js +0 -60
- package/lib/errors.js +0 -18
- package/lib/generator.js +0 -127
- package/lib/graphql-fetch.js +0 -83
- package/lib/graphql-generator.js +0 -33
- package/lib/graphql.js +0 -24
- package/lib/metrics.js +0 -12
- package/lib/not-host-constraints.js +0 -31
- package/lib/openapi-composer.js +0 -101
- package/lib/openapi-config-schema.js +0 -89
- package/lib/openapi-generator.js +0 -213
- package/lib/openapi-load-config.js +0 -31
- package/lib/openapi-modifier.js +0 -128
- package/lib/openapi-scalar.js +0 -22
- package/lib/proxy.js +0 -265
- package/lib/root.js +0 -75
- package/lib/schema.js +0 -258
- package/lib/stackable.js +0 -88
- package/lib/upgrade.js +0 -20
- package/lib/utils.js +0 -16
- package/lib/versions/2.0.0.js +0 -9
- package/lib/versions/3.0.0.js +0 -14
- package/public/images/dark_mode.svg +0 -3
- package/public/images/ellipse.svg +0 -21
- package/public/images/external-link.svg +0 -5
- package/public/images/favicon.ico +0 -0
- package/public/images/graphiql.svg +0 -10
- package/public/images/graphql.svg +0 -10
- package/public/images/light_mode.svg +0 -11
- package/public/images/openapi.svg +0 -13
- package/public/images/platformatic-logo-dark.svg +0 -30
- package/public/images/platformatic-logo-light.svg +0 -30
- package/public/images/reverse-proxy.svg +0 -8
- package/public/images/triangle_dark.svg +0 -3
- package/public/images/triangle_light.svg +0 -3
- package/public/index.html +0 -253
- package/public/index.njk +0 -101
- package/public/main.css +0 -244
package/lib/openapi-composer.js
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import rfdc from 'rfdc'
|
|
2
|
-
import { PathAlreadyExistsError } from './errors.js'
|
|
3
|
-
|
|
4
|
-
const clone = rfdc()
|
|
5
|
-
|
|
6
|
-
function generateOperationIdApiPrefix (operationId) {
|
|
7
|
-
return (
|
|
8
|
-
operationId
|
|
9
|
-
.trim()
|
|
10
|
-
.replace(/[^A-Z0-9]+/gi, '_')
|
|
11
|
-
.replace(/^_+|_+$/g, '') + '_'
|
|
12
|
-
)
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function namespaceSchemaRefs (apiPrefix, schema) {
|
|
16
|
-
if (schema.$ref && schema.$ref.startsWith('#/components/schemas')) {
|
|
17
|
-
schema.$ref = schema.$ref.replace('#/components/schemas/', '#/components/schemas/' + apiPrefix)
|
|
18
|
-
}
|
|
19
|
-
for (const childSchema of Object.values(schema)) {
|
|
20
|
-
if (typeof childSchema === 'object') {
|
|
21
|
-
namespaceSchemaRefs(apiPrefix, childSchema)
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function namespaceSchemaOperationIds (apiPrefix, schema) {
|
|
27
|
-
if (schema.operationId) {
|
|
28
|
-
schema.operationId = apiPrefix + schema.operationId
|
|
29
|
-
}
|
|
30
|
-
for (const childSchema of Object.values(schema)) {
|
|
31
|
-
if (typeof childSchema === 'object') {
|
|
32
|
-
namespaceSchemaOperationIds(apiPrefix, childSchema)
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function composeOpenApi (apis, options = {}) {
|
|
38
|
-
const mergedPaths = {}
|
|
39
|
-
const mergedSchemas = {}
|
|
40
|
-
const mergedSecuritySchemes = {}
|
|
41
|
-
|
|
42
|
-
for (const { id, prefix, schema } of apis) {
|
|
43
|
-
const { paths, components } = clone(schema)
|
|
44
|
-
|
|
45
|
-
const apiPrefix = generateOperationIdApiPrefix(id)
|
|
46
|
-
for (const [path, pathSchema] of Object.entries(paths)) {
|
|
47
|
-
namespaceSchemaRefs(apiPrefix, pathSchema)
|
|
48
|
-
namespaceSchemaOperationIds(apiPrefix, pathSchema)
|
|
49
|
-
|
|
50
|
-
for (const methodSchema of Object.values(pathSchema)) {
|
|
51
|
-
if (methodSchema.security) {
|
|
52
|
-
methodSchema.security = methodSchema.security.map(security => {
|
|
53
|
-
const newSecurity = {}
|
|
54
|
-
for (const [securityKey, securityValue] of Object.entries(security)) {
|
|
55
|
-
newSecurity[apiPrefix + securityKey] = securityValue
|
|
56
|
-
}
|
|
57
|
-
return newSecurity
|
|
58
|
-
})
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const mergedPath = prefix ? prefix + path : path
|
|
63
|
-
|
|
64
|
-
if (mergedPaths[mergedPath]) {
|
|
65
|
-
throw new PathAlreadyExistsError(mergedPath)
|
|
66
|
-
}
|
|
67
|
-
mergedPaths[mergedPath] = pathSchema
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (components) {
|
|
71
|
-
if (components.schemas) {
|
|
72
|
-
for (const [schemaKey, schema] of Object.entries(components.schemas)) {
|
|
73
|
-
if (schema.title == null) {
|
|
74
|
-
schema.title = schemaKey
|
|
75
|
-
}
|
|
76
|
-
namespaceSchemaRefs(apiPrefix, schema)
|
|
77
|
-
mergedSchemas[apiPrefix + schemaKey] = schema
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (components.securitySchemes) {
|
|
82
|
-
for (const [securitySchemeKey, securityScheme] of Object.entries(components.securitySchemes)) {
|
|
83
|
-
mergedSecuritySchemes[apiPrefix + securitySchemeKey] = securityScheme
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return {
|
|
90
|
-
openapi: '3.0.0',
|
|
91
|
-
info: {
|
|
92
|
-
title: options.title || 'Platformatic Composer',
|
|
93
|
-
version: options.version || '1.0.0'
|
|
94
|
-
},
|
|
95
|
-
components: {
|
|
96
|
-
securitySchemes: mergedSecuritySchemes,
|
|
97
|
-
schemas: mergedSchemas
|
|
98
|
-
},
|
|
99
|
-
paths: mergedPaths
|
|
100
|
-
}
|
|
101
|
-
}
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
const ignoreSchema = {
|
|
2
|
-
type: 'object',
|
|
3
|
-
properties: {
|
|
4
|
-
ignore: { type: 'boolean' }
|
|
5
|
-
},
|
|
6
|
-
additionalProperties: false
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const aliasSchema = {
|
|
10
|
-
type: 'object',
|
|
11
|
-
properties: {
|
|
12
|
-
alias: { type: 'string' }
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const jsonSchemaSchema = {
|
|
17
|
-
$id: 'json-schema',
|
|
18
|
-
type: 'object',
|
|
19
|
-
properties: {
|
|
20
|
-
type: { type: 'string' },
|
|
21
|
-
properties: {
|
|
22
|
-
type: 'object',
|
|
23
|
-
additionalProperties: {
|
|
24
|
-
oneOf: [
|
|
25
|
-
{ $ref: 'json-schema' },
|
|
26
|
-
{
|
|
27
|
-
type: 'object',
|
|
28
|
-
properties: {
|
|
29
|
-
rename: { type: 'string' }
|
|
30
|
-
},
|
|
31
|
-
additionalProperties: false
|
|
32
|
-
}
|
|
33
|
-
]
|
|
34
|
-
}
|
|
35
|
-
},
|
|
36
|
-
items: { $ref: 'json-schema' }
|
|
37
|
-
},
|
|
38
|
-
additionalProperties: false
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const routeSchema = {
|
|
42
|
-
anyOf: [
|
|
43
|
-
ignoreSchema,
|
|
44
|
-
{
|
|
45
|
-
type: 'object',
|
|
46
|
-
properties: {
|
|
47
|
-
responses: {
|
|
48
|
-
type: 'object',
|
|
49
|
-
properties: {
|
|
50
|
-
200: { $ref: 'json-schema' }
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
]
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export const openApiConfigSchema = {
|
|
59
|
-
type: 'object',
|
|
60
|
-
properties: {
|
|
61
|
-
paths: {
|
|
62
|
-
type: 'object',
|
|
63
|
-
additionalProperties: {
|
|
64
|
-
anyOf: [
|
|
65
|
-
ignoreSchema,
|
|
66
|
-
aliasSchema,
|
|
67
|
-
{
|
|
68
|
-
type: 'object',
|
|
69
|
-
properties: {
|
|
70
|
-
get: routeSchema,
|
|
71
|
-
post: routeSchema,
|
|
72
|
-
put: routeSchema,
|
|
73
|
-
patch: routeSchema,
|
|
74
|
-
delete: routeSchema,
|
|
75
|
-
options: routeSchema,
|
|
76
|
-
head: routeSchema,
|
|
77
|
-
trace: routeSchema
|
|
78
|
-
},
|
|
79
|
-
additionalProperties: false
|
|
80
|
-
}
|
|
81
|
-
]
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
},
|
|
85
|
-
additionalProperties: false,
|
|
86
|
-
definitions: {
|
|
87
|
-
'json-schema': jsonSchemaSchema
|
|
88
|
-
}
|
|
89
|
-
}
|
package/lib/openapi-generator.js
DELETED
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
import fastifyReplyFrom from '@fastify/reply-from'
|
|
2
|
-
import fastifySwagger from '@fastify/swagger'
|
|
3
|
-
import fp from 'fastify-plugin'
|
|
4
|
-
import { readFile } from 'node:fs/promises'
|
|
5
|
-
import { getGlobalDispatcher, request } from 'undici'
|
|
6
|
-
import { CouldNotReadOpenAPIConfigError } from './errors.js'
|
|
7
|
-
import { composeOpenApi } from './openapi-composer.js'
|
|
8
|
-
import { loadOpenApiConfig } from './openapi-load-config.js'
|
|
9
|
-
import { modifyOpenApiSchema, originPathSymbol } from './openapi-modifier.js'
|
|
10
|
-
import { openApiScalar } from './openapi-scalar.js'
|
|
11
|
-
import { prefixWithSlash } from './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
|
-
function createPathMapper (originOpenApiPath, renamedOpenApiPath, prefix) {
|
|
33
|
-
if (prefix + originOpenApiPath === renamedOpenApiPath) {
|
|
34
|
-
return path => path.slice(prefix.length)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const extractParamsRegexp = generateRouteRegex(renamedOpenApiPath)
|
|
38
|
-
return path => {
|
|
39
|
-
const routeParams = path.match(extractParamsRegexp).slice(1)
|
|
40
|
-
return generateRenamedPath(originOpenApiPath, routeParams)
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function generateRouteRegex (route) {
|
|
45
|
-
const regex = route.replace(/{(.*?)}/g, '(.*)')
|
|
46
|
-
return new RegExp(regex)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function generateRenamedPath (renamedOpenApiPath, routeParams) {
|
|
50
|
-
return renamedOpenApiPath.replace(/{(.*?)}/g, () => routeParams.shift())
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async function openApiComposerPlugin (app, { opts, generated }) {
|
|
54
|
-
const { apiByApiRoutes } = generated
|
|
55
|
-
|
|
56
|
-
const dispatcher = getGlobalDispatcher()
|
|
57
|
-
|
|
58
|
-
await app.register(fastifyReplyFrom, {
|
|
59
|
-
undici: dispatcher,
|
|
60
|
-
destroyAgent: false
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
await app.register(await import('@platformatic/fastify-openapi-glue'), {
|
|
64
|
-
specification: app.composedOpenApiSchema,
|
|
65
|
-
addEmptySchema: opts.addEmptySchema,
|
|
66
|
-
operationResolver: (operationId, method, openApiPath) => {
|
|
67
|
-
const { origin, prefix, schema } = apiByApiRoutes[openApiPath]
|
|
68
|
-
const originPath = schema[originPathSymbol]
|
|
69
|
-
|
|
70
|
-
const mapRoutePath = createPathMapper(originPath, openApiPath, prefix)
|
|
71
|
-
|
|
72
|
-
return {
|
|
73
|
-
config: { openApiPath },
|
|
74
|
-
handler: (req, reply) => {
|
|
75
|
-
const routePath = req.raw.url.split('?')[0]
|
|
76
|
-
const newRoutePath = mapRoutePath(routePath)
|
|
77
|
-
|
|
78
|
-
const replyOptions = {}
|
|
79
|
-
const onResponse = (request, reply, res) => {
|
|
80
|
-
app.openTelemetry?.endHTTPSpanClient(reply.request.proxedCallSpan, {
|
|
81
|
-
statusCode: reply.statusCode,
|
|
82
|
-
headers: res.headers
|
|
83
|
-
})
|
|
84
|
-
if (req.routeOptions.config?.onComposerResponse) {
|
|
85
|
-
req.routeOptions.config?.onComposerResponse(request, reply, res.stream)
|
|
86
|
-
} else {
|
|
87
|
-
reply.send(res.stream)
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
const rewriteRequestHeaders = (request, headers) => {
|
|
91
|
-
const targetUrl = `${origin}${request.url}`
|
|
92
|
-
const context = request.span?.context
|
|
93
|
-
const { span, telemetryHeaders } = app.openTelemetry?.startHTTPSpanClient(
|
|
94
|
-
targetUrl,
|
|
95
|
-
request.method,
|
|
96
|
-
context
|
|
97
|
-
) || { span: null, telemetryHeaders: {} }
|
|
98
|
-
// We need to store the span in a different object
|
|
99
|
-
// to correctly close it in the onResponse hook
|
|
100
|
-
// Note that we have 2 spans:
|
|
101
|
-
// - request.span: the span of the request to the proxy
|
|
102
|
-
// - request.proxedCallSpan: the span of the request to the proxied service
|
|
103
|
-
request.proxedCallSpan = span
|
|
104
|
-
|
|
105
|
-
headers = {
|
|
106
|
-
...headers,
|
|
107
|
-
...telemetryHeaders,
|
|
108
|
-
'x-forwarded-for': request.ip,
|
|
109
|
-
'x-forwarded-host': request.host
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return headers
|
|
113
|
-
}
|
|
114
|
-
replyOptions.onResponse = onResponse
|
|
115
|
-
replyOptions.rewriteRequestHeaders = rewriteRequestHeaders
|
|
116
|
-
|
|
117
|
-
reply.from(origin + newRoutePath, replyOptions)
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
app.addHook('preValidation', async req => {
|
|
124
|
-
if (typeof req.query.fields === 'string') {
|
|
125
|
-
req.query.fields = req.query.fields.split(',')
|
|
126
|
-
}
|
|
127
|
-
})
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export async function openApiGenerator (app, opts) {
|
|
131
|
-
if (!opts.services.some(s => s.openapi)) {
|
|
132
|
-
return
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const { services } = opts
|
|
136
|
-
|
|
137
|
-
const openApiSchemas = []
|
|
138
|
-
const apiByApiRoutes = {}
|
|
139
|
-
|
|
140
|
-
for (const { id, origin, openapi } of services) {
|
|
141
|
-
if (!openapi) continue
|
|
142
|
-
|
|
143
|
-
let openapiConfig = null
|
|
144
|
-
if (openapi.config) {
|
|
145
|
-
try {
|
|
146
|
-
openapiConfig = await loadOpenApiConfig(openapi.config)
|
|
147
|
-
} catch (error) {
|
|
148
|
-
app.log.error(error)
|
|
149
|
-
throw new CouldNotReadOpenAPIConfigError(id)
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
let originSchema = null
|
|
154
|
-
try {
|
|
155
|
-
originSchema = await getOpenApiSchema(origin, openapi)
|
|
156
|
-
} catch (error) {
|
|
157
|
-
app.log.error(error, `failed to fetch schema for "${id} service"`)
|
|
158
|
-
continue
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const schema = modifyOpenApiSchema(app, originSchema, openapiConfig)
|
|
162
|
-
|
|
163
|
-
const prefix = openapi.prefix ? prefixWithSlash(openapi.prefix) : ''
|
|
164
|
-
for (const path in schema.paths) {
|
|
165
|
-
apiByApiRoutes[prefix + path] = {
|
|
166
|
-
origin,
|
|
167
|
-
prefix,
|
|
168
|
-
schema: schema.paths[path]
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
openApiSchemas.push({ id, prefix, schema, originSchema, config: openapiConfig })
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const composedOpenApiSchema = composeOpenApi(openApiSchemas, opts.openapi)
|
|
176
|
-
|
|
177
|
-
app.decorate('openApiSchemas', openApiSchemas)
|
|
178
|
-
app.decorate('composedOpenApiSchema', composedOpenApiSchema)
|
|
179
|
-
|
|
180
|
-
await app.register(fastifySwagger, {
|
|
181
|
-
exposeRoute: true,
|
|
182
|
-
openapi: {
|
|
183
|
-
info: {
|
|
184
|
-
title: opts.openapi?.title || 'Platformatic Composer',
|
|
185
|
-
version: opts.openapi?.version || '1.0.0'
|
|
186
|
-
},
|
|
187
|
-
servers: [{ url: globalThis.platformatic?.runtimeBasePath ?? '/' }],
|
|
188
|
-
components: app.composedOpenApiSchema.components
|
|
189
|
-
},
|
|
190
|
-
transform ({ schema, url }) {
|
|
191
|
-
for (const service of opts.services) {
|
|
192
|
-
if (!service.proxy) continue
|
|
193
|
-
|
|
194
|
-
const prefix = service.proxy.prefix ?? ''
|
|
195
|
-
const proxyPrefix = prefix.at(-1) === '/' ? prefix.slice(0, -1) : prefix
|
|
196
|
-
|
|
197
|
-
const proxyUrls = [proxyPrefix + '/', proxyPrefix + '/*']
|
|
198
|
-
if (proxyUrls.includes(url)) {
|
|
199
|
-
schema = schema ?? {}
|
|
200
|
-
schema.hide = true
|
|
201
|
-
break
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
return { schema, url }
|
|
205
|
-
}
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
await app.register(openApiScalar, opts)
|
|
209
|
-
|
|
210
|
-
return { apiByApiRoutes }
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
export const openApiComposer = fp(openApiComposerPlugin)
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import Ajv from 'ajv'
|
|
2
|
-
import { readFile } from 'node:fs/promises'
|
|
3
|
-
import { ValidationErrors } from './errors.js'
|
|
4
|
-
import { openApiConfigSchema } from './openapi-config-schema.js'
|
|
5
|
-
|
|
6
|
-
const ajv = new Ajv()
|
|
7
|
-
const ajvValidate = ajv.compile(openApiConfigSchema)
|
|
8
|
-
|
|
9
|
-
export async function loadOpenApiConfig (pathToConfig) {
|
|
10
|
-
const openApiConfigFile = await readFile(pathToConfig, 'utf-8')
|
|
11
|
-
const openApiConfig = JSON.parse(openApiConfigFile)
|
|
12
|
-
|
|
13
|
-
if (!ajvValidate(openApiConfig)) {
|
|
14
|
-
const validationErrors = ajvValidate.errors.map(err => {
|
|
15
|
-
return {
|
|
16
|
-
/* c8 ignore next 1 */
|
|
17
|
-
path: err.instancePath === '' ? '/' : err.instancePath,
|
|
18
|
-
message: err.message + ' ' + JSON.stringify(err.params)
|
|
19
|
-
}
|
|
20
|
-
})
|
|
21
|
-
throw new ValidationErrors(
|
|
22
|
-
validationErrors
|
|
23
|
-
.map(err => {
|
|
24
|
-
return err.message
|
|
25
|
-
})
|
|
26
|
-
.join('\n')
|
|
27
|
-
)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return openApiConfig
|
|
31
|
-
}
|
package/lib/openapi-modifier.js
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import traverse from 'json-schema-traverse'
|
|
2
|
-
import rfdc from 'rfdc'
|
|
3
|
-
|
|
4
|
-
const clone = rfdc()
|
|
5
|
-
|
|
6
|
-
const MODIFICATION_KEYWORDS = ['rename']
|
|
7
|
-
|
|
8
|
-
export const originPathSymbol = Symbol('originPath')
|
|
9
|
-
|
|
10
|
-
function findDataBySchemaPointer (schemaPointer, schema, data, parentData, callback) {
|
|
11
|
-
const schemaPointerParts = schemaPointer.split('/').slice(1)
|
|
12
|
-
|
|
13
|
-
for (const schemaPointerPart of schemaPointerParts) {
|
|
14
|
-
parentData = data
|
|
15
|
-
schema = schema[schemaPointerPart]
|
|
16
|
-
|
|
17
|
-
if (schemaPointerPart === 'properties') continue
|
|
18
|
-
|
|
19
|
-
if (schemaPointerPart === 'items') {
|
|
20
|
-
for (const item of data) {
|
|
21
|
-
const newSchemaPointer = '/' + schemaPointerParts.slice(1).join('/')
|
|
22
|
-
findDataBySchemaPointer(newSchemaPointer, schema, item, parentData, callback)
|
|
23
|
-
}
|
|
24
|
-
return
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
data = data[schemaPointerPart]
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
callback(data, parentData)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function getModificationRules (modificationSchema) {
|
|
34
|
-
const modificationRules = {}
|
|
35
|
-
|
|
36
|
-
function getModificationRules (schema, jsonPointer) {
|
|
37
|
-
const schemaKeys = Object.keys(schema)
|
|
38
|
-
const modificationKeys = schemaKeys.filter(key => MODIFICATION_KEYWORDS.includes(key))
|
|
39
|
-
|
|
40
|
-
if (modificationKeys.length === 0) return
|
|
41
|
-
modificationRules[jsonPointer] = schema
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
traverse(modificationSchema, { cb: getModificationRules })
|
|
45
|
-
return modificationRules
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function modifySchema (originSchema, modificationRules) {
|
|
49
|
-
function modifyOriginSchema (schema, jsonPointer, rs, psp, pk, parentSchema, keyIndex) {
|
|
50
|
-
const modificationRule = modificationRules[jsonPointer]
|
|
51
|
-
if (!modificationRule) return
|
|
52
|
-
|
|
53
|
-
if (modificationRule.rename) {
|
|
54
|
-
parentSchema.properties[modificationRule.rename] = schema
|
|
55
|
-
delete parentSchema.properties[keyIndex]
|
|
56
|
-
|
|
57
|
-
if (parentSchema.required) {
|
|
58
|
-
const index = parentSchema.required.indexOf(keyIndex)
|
|
59
|
-
if (index !== -1) {
|
|
60
|
-
parentSchema.required[index] = modificationRule.rename
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
traverse(originSchema, { cb: modifyOriginSchema })
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function modifyPayload (payload, originSchema, modificationRules) {
|
|
69
|
-
for (const schemaJsonPointer in modificationRules) {
|
|
70
|
-
const rule = modificationRules[schemaJsonPointer]
|
|
71
|
-
|
|
72
|
-
findDataBySchemaPointer(schemaJsonPointer, originSchema, payload, null, (data, parentData) => {
|
|
73
|
-
if (rule.rename) {
|
|
74
|
-
parentData[rule.rename] = data
|
|
75
|
-
delete parentData[schemaJsonPointer.split('/').pop()]
|
|
76
|
-
}
|
|
77
|
-
})
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function modifyOpenApiSchema (app, schema, config) {
|
|
82
|
-
const newSchemaPaths = {}
|
|
83
|
-
const { paths } = clone(schema)
|
|
84
|
-
|
|
85
|
-
for (let path in paths) {
|
|
86
|
-
const pathConfig = config?.paths?.[path]
|
|
87
|
-
const pathSchema = paths[path]
|
|
88
|
-
|
|
89
|
-
if (pathConfig?.ignore) continue
|
|
90
|
-
|
|
91
|
-
pathSchema[originPathSymbol] = path
|
|
92
|
-
|
|
93
|
-
if (pathConfig?.alias) {
|
|
94
|
-
path = pathConfig.alias
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
for (const method in pathSchema) {
|
|
98
|
-
const routeConfig = pathConfig?.[method.toLowerCase()]
|
|
99
|
-
|
|
100
|
-
if (routeConfig?.ignore) {
|
|
101
|
-
delete pathSchema[method]
|
|
102
|
-
continue
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const modificationResponseSchema = routeConfig?.responses?.['200']
|
|
106
|
-
if (!modificationResponseSchema) continue
|
|
107
|
-
|
|
108
|
-
const modificationRules = getModificationRules(modificationResponseSchema)
|
|
109
|
-
|
|
110
|
-
app.platformatic.addComposerOnRouteHook(path, [method], routeOptions => {
|
|
111
|
-
const routeSchema = routeOptions.schema
|
|
112
|
-
const responseSchema = routeSchema.response?.['200']
|
|
113
|
-
modifySchema(responseSchema, modificationRules)
|
|
114
|
-
|
|
115
|
-
async function onComposerResponse (request, reply, body) {
|
|
116
|
-
const payload = await body.json()
|
|
117
|
-
modifyPayload(payload, responseSchema, modificationRules)
|
|
118
|
-
reply.send(payload)
|
|
119
|
-
}
|
|
120
|
-
routeOptions.config.onComposerResponse = onComposerResponse
|
|
121
|
-
})
|
|
122
|
-
}
|
|
123
|
-
if (Object.keys(pathSchema).length === 0) continue
|
|
124
|
-
newSchemaPaths[path] = pathSchema
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return { ...schema, paths: newSchemaPaths }
|
|
128
|
-
}
|
package/lib/openapi-scalar.js
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import fp from 'fastify-plugin'
|
|
2
|
-
|
|
3
|
-
async function openApiScalarPlugin (app, opts) {
|
|
4
|
-
const { default: scalarTheme } = await import('@platformatic/scalar-theme')
|
|
5
|
-
const { default: scalarApiReference } = await import('@scalar/fastify-api-reference')
|
|
6
|
-
|
|
7
|
-
/** Serve spec file in yaml and json */
|
|
8
|
-
app.get('/documentation/json', { schema: { hide: true } }, async () => app.swagger())
|
|
9
|
-
app.get('/documentation/yaml', { schema: { hide: true } }, async () => app.swagger({ yaml: true }))
|
|
10
|
-
|
|
11
|
-
const routePrefix = opts.openapi?.swaggerPrefix || '/documentation'
|
|
12
|
-
|
|
13
|
-
await app.register(scalarApiReference, {
|
|
14
|
-
logLevel: 'warn',
|
|
15
|
-
routePrefix,
|
|
16
|
-
configuration: {
|
|
17
|
-
customCss: scalarTheme.theme
|
|
18
|
-
}
|
|
19
|
-
})
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export const openApiScalar = fp(openApiScalarPlugin)
|