@platformatic/composer 2.40.0 → 2.42.0
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/index.js +9 -7
- package/lib/openapi-composer.js +28 -6
- package/lib/openapi-generator.js +45 -5
- package/lib/openapi-scalar.js +23 -0
- package/lib/proxy.js +21 -6
- package/package.json +11 -11
- package/schema.json +1 -1
- package/lib/openapi.js +0 -50
package/index.js
CHANGED
|
@@ -6,10 +6,9 @@ const { platformaticService, buildServer, buildStackable } = require('@platforma
|
|
|
6
6
|
|
|
7
7
|
const { schema } = require('./lib/schema')
|
|
8
8
|
const serviceProxy = require('./lib/proxy')
|
|
9
|
-
const openapi = require('./lib/openapi.js')
|
|
10
9
|
const graphql = require('./lib/graphql')
|
|
11
10
|
const composerHook = require('./lib/composer-hook')
|
|
12
|
-
const
|
|
11
|
+
const { openApiGenerator, openApiComposer } = require('./lib/openapi-generator')
|
|
13
12
|
const graphqlGenerator = require('./lib/graphql-generator')
|
|
14
13
|
const { isSameGraphqlSchema, fetchGraphqlSubgraphs } = require('./lib/graphql-fetch')
|
|
15
14
|
const notHostConstraints = require('./lib/proxy/not-host-constraints')
|
|
@@ -42,15 +41,18 @@ async function platformaticComposer (app, opts) {
|
|
|
42
41
|
}
|
|
43
42
|
}
|
|
44
43
|
|
|
44
|
+
await app.register(composerHook)
|
|
45
|
+
|
|
46
|
+
let generatedComposedOpenAPI = null
|
|
45
47
|
if (hasOpenapiServices) {
|
|
46
|
-
app
|
|
48
|
+
generatedComposedOpenAPI = await openApiGenerator(app, config.composer)
|
|
47
49
|
}
|
|
50
|
+
|
|
48
51
|
app.register(serviceProxy, { ...config.composer, context: opts.context })
|
|
49
|
-
app.register(
|
|
50
|
-
await app.register(platformaticService, { config, context: opts.context })
|
|
52
|
+
await app.register(platformaticService, { config: { ...config, openapi: false }, context: opts.context })
|
|
51
53
|
|
|
52
|
-
if (
|
|
53
|
-
await app.register(
|
|
54
|
+
if (generatedComposedOpenAPI) {
|
|
55
|
+
await app.register(openApiComposer, { opts: config.composer, generated: generatedComposedOpenAPI })
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
if (hasGraphqlServices) {
|
package/lib/openapi-composer.js
CHANGED
|
@@ -6,6 +6,7 @@ const errors = require('./errors')
|
|
|
6
6
|
function composeOpenApi (apis, options = {}) {
|
|
7
7
|
const mergedPaths = {}
|
|
8
8
|
const mergedSchemas = {}
|
|
9
|
+
const mergedSecuritySchemes = {}
|
|
9
10
|
|
|
10
11
|
for (const { id, prefix, schema } of apis) {
|
|
11
12
|
const { paths, components } = clone(schema)
|
|
@@ -15,6 +16,18 @@ function composeOpenApi (apis, options = {}) {
|
|
|
15
16
|
namespaceSchemaRefs(apiPrefix, pathSchema)
|
|
16
17
|
namespaceSchemaOperationIds(apiPrefix, pathSchema)
|
|
17
18
|
|
|
19
|
+
for (const methodSchema of Object.values(pathSchema)) {
|
|
20
|
+
if (methodSchema.security) {
|
|
21
|
+
methodSchema.security = methodSchema.security.map(security => {
|
|
22
|
+
const newSecurity = {}
|
|
23
|
+
for (const [securityKey, securityValue] of Object.entries(security)) {
|
|
24
|
+
newSecurity[apiPrefix + securityKey] = securityValue
|
|
25
|
+
}
|
|
26
|
+
return newSecurity
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
18
31
|
const mergedPath = prefix ? prefix + path : path
|
|
19
32
|
|
|
20
33
|
if (mergedPaths[mergedPath]) {
|
|
@@ -23,13 +36,21 @@ function composeOpenApi (apis, options = {}) {
|
|
|
23
36
|
mergedPaths[mergedPath] = pathSchema
|
|
24
37
|
}
|
|
25
38
|
|
|
26
|
-
if (components
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
schema.title
|
|
39
|
+
if (components) {
|
|
40
|
+
if (components.schemas) {
|
|
41
|
+
for (const [schemaKey, schema] of Object.entries(components.schemas)) {
|
|
42
|
+
if (schema.title == null) {
|
|
43
|
+
schema.title = schemaKey
|
|
44
|
+
}
|
|
45
|
+
namespaceSchemaRefs(apiPrefix, schema)
|
|
46
|
+
mergedSchemas[apiPrefix + schemaKey] = schema
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (components.securitySchemes) {
|
|
51
|
+
for (const [securitySchemeKey, securityScheme] of Object.entries(components.securitySchemes)) {
|
|
52
|
+
mergedSecuritySchemes[apiPrefix + securitySchemeKey] = securityScheme
|
|
30
53
|
}
|
|
31
|
-
namespaceSchemaRefs(apiPrefix, schema)
|
|
32
|
-
mergedSchemas[apiPrefix + schemaKey] = schema
|
|
33
54
|
}
|
|
34
55
|
}
|
|
35
56
|
}
|
|
@@ -41,6 +62,7 @@ function composeOpenApi (apis, options = {}) {
|
|
|
41
62
|
version: options.version || '1.0.0',
|
|
42
63
|
},
|
|
43
64
|
components: {
|
|
65
|
+
securitySchemes: mergedSecuritySchemes,
|
|
44
66
|
schemas: mergedSchemas,
|
|
45
67
|
},
|
|
46
68
|
paths: mergedPaths,
|
package/lib/openapi-generator.js
CHANGED
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
const { readFile } = require('node:fs/promises')
|
|
4
4
|
const { request, getGlobalDispatcher } = require('undici')
|
|
5
5
|
const fp = require('fastify-plugin')
|
|
6
|
-
const
|
|
6
|
+
const fastifySwagger = require('@fastify/swagger')
|
|
7
7
|
|
|
8
|
+
const errors = require('./errors')
|
|
8
9
|
const { modifyOpenApiSchema, originPathSymbol } = require('./openapi-modifier')
|
|
9
10
|
const composeOpenApi = require('./openapi-composer')
|
|
10
11
|
const loadOpenApiConfig = require('./openapi-load-config.js')
|
|
11
12
|
const { prefixWithSlash } = require('./utils.js')
|
|
13
|
+
const openApiScalar = require('./openapi-scalar')
|
|
12
14
|
|
|
13
15
|
async function fetchOpenApiSchema (openApiUrl) {
|
|
14
16
|
const { body } = await request(openApiUrl)
|
|
@@ -29,7 +31,7 @@ async function getOpenApiSchema (origin, openapi) {
|
|
|
29
31
|
return readOpenApiSchema(openapi.file)
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
async function
|
|
34
|
+
async function generateComposedOpenApi (app, opts) {
|
|
33
35
|
if (!opts.services.some(s => s.openapi)) { return }
|
|
34
36
|
|
|
35
37
|
const { services } = opts
|
|
@@ -72,9 +74,46 @@ async function composeOpenAPI (app, opts) {
|
|
|
72
74
|
openApiSchemas.push({ id, prefix, schema, originSchema, config: openapiConfig })
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
const composedOpenApiSchema = composeOpenApi(openApiSchemas, opts.openapi)
|
|
78
|
+
|
|
75
79
|
app.decorate('openApiSchemas', openApiSchemas)
|
|
80
|
+
app.decorate('composedOpenApiSchema', composedOpenApiSchema)
|
|
81
|
+
|
|
82
|
+
await app.register(fastifySwagger, {
|
|
83
|
+
exposeRoute: true,
|
|
84
|
+
openapi: {
|
|
85
|
+
info: {
|
|
86
|
+
title: opts.openapi?.title || 'Platformatic Composer',
|
|
87
|
+
version: opts.openapi?.version || '1.0.0'
|
|
88
|
+
},
|
|
89
|
+
servers: [{ url: globalThis.platformatic?.runtimeBasePath ?? '/' }],
|
|
90
|
+
components: app.composedOpenApiSchema.components
|
|
91
|
+
},
|
|
92
|
+
transform ({ schema, url }) {
|
|
93
|
+
for (const service of opts.services) {
|
|
94
|
+
if (!service.proxy) continue
|
|
95
|
+
|
|
96
|
+
const prefix = service.proxy.prefix ?? ''
|
|
97
|
+
const proxyPrefix = prefix.at(-1) === '/' ? prefix.slice(0, -1) : prefix
|
|
98
|
+
|
|
99
|
+
const proxyUrls = [proxyPrefix + '/', proxyPrefix + '/*']
|
|
100
|
+
if (proxyUrls.includes(url)) {
|
|
101
|
+
schema = schema ?? {}
|
|
102
|
+
schema.hide = true
|
|
103
|
+
break
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { schema, url }
|
|
107
|
+
}
|
|
108
|
+
})
|
|
76
109
|
|
|
77
|
-
|
|
110
|
+
await app.register(openApiScalar, opts)
|
|
111
|
+
|
|
112
|
+
return { apiByApiRoutes }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function openApiComposer (app, { opts, generated }) {
|
|
116
|
+
const { apiByApiRoutes } = generated
|
|
78
117
|
|
|
79
118
|
const dispatcher = getGlobalDispatcher()
|
|
80
119
|
|
|
@@ -84,7 +123,7 @@ async function composeOpenAPI (app, opts) {
|
|
|
84
123
|
})
|
|
85
124
|
|
|
86
125
|
await app.register(await import('@platformatic/fastify-openapi-glue'), {
|
|
87
|
-
specification: composedOpenApiSchema,
|
|
126
|
+
specification: app.composedOpenApiSchema,
|
|
88
127
|
addEmptySchema: opts.addEmptySchema,
|
|
89
128
|
operationResolver: (operationId, method, openApiPath) => {
|
|
90
129
|
const { origin, prefix, schema } = apiByApiRoutes[openApiPath]
|
|
@@ -167,4 +206,5 @@ function generateRenamedPath (renamedOpenApiPath, routeParams) {
|
|
|
167
206
|
return renamedOpenApiPath.replace(/{(.*?)}/g, () => routeParams.shift())
|
|
168
207
|
}
|
|
169
208
|
|
|
170
|
-
module.exports =
|
|
209
|
+
module.exports.openApiGenerator = generateComposedOpenApi
|
|
210
|
+
module.exports.openApiComposer = fp(openApiComposer)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fp = require('fastify-plugin')
|
|
4
|
+
|
|
5
|
+
async function openApiScalar (app, opts) {
|
|
6
|
+
const { default: scalarTheme } = await import('@platformatic/scalar-theme')
|
|
7
|
+
|
|
8
|
+
/** Serve spec file in yaml and json */
|
|
9
|
+
app.get('/documentation/json', { schema: { hide: true } }, async () => app.swagger())
|
|
10
|
+
app.get('/documentation/yaml', { schema: { hide: true } }, async () => app.swagger({ yaml: true }))
|
|
11
|
+
|
|
12
|
+
const routePrefix = opts.openapi?.swaggerPrefix || '/documentation'
|
|
13
|
+
|
|
14
|
+
await app.register(require('@scalar/fastify-api-reference'), {
|
|
15
|
+
logLevel: 'warn',
|
|
16
|
+
routePrefix,
|
|
17
|
+
configuration: {
|
|
18
|
+
customCss: scalarTheme.theme
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = fp(openApiScalar)
|
package/lib/proxy.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const { getGlobalDispatcher } = require('undici')
|
|
4
3
|
const httpProxy = require('@fastify/http-proxy')
|
|
5
4
|
const fp = require('fastify-plugin')
|
|
5
|
+
const { workerData } = require('node:worker_threads')
|
|
6
|
+
const { getGlobalDispatcher } = require('undici')
|
|
6
7
|
|
|
7
8
|
const kITC = Symbol.for('plt.runtime.itc')
|
|
8
9
|
const kProxyRoute = Symbol('plt.composer.proxy.route')
|
|
@@ -14,12 +15,18 @@ async function resolveServiceProxyParameters (service) {
|
|
|
14
15
|
const meta = (await globalThis[kITC]?.send('getServiceMeta', service.id))?.composer ?? { prefix: service.id }
|
|
15
16
|
|
|
16
17
|
// If no prefix could be found, assume the service id
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
let prefix = (service.proxy?.prefix ?? meta.prefix ?? service.id).replace(/(\/$)/g, '')
|
|
19
19
|
let rewritePrefix = ''
|
|
20
20
|
let internalRewriteLocationHeader = true
|
|
21
21
|
|
|
22
22
|
if (meta.wantsAbsoluteUrls) {
|
|
23
|
+
const basePath = workerData.config.basePath
|
|
24
|
+
|
|
25
|
+
// Strip the runtime basepath from the prefix when it comes from the service meta
|
|
26
|
+
if (basePath && !service.proxy?.prefix && prefix.startsWith(basePath)) {
|
|
27
|
+
prefix = prefix.substring(basePath.length)
|
|
28
|
+
}
|
|
29
|
+
|
|
23
30
|
// The rewritePrefix purposely ignores service.proxy?.prefix to let
|
|
24
31
|
// the service always being able to configure their value
|
|
25
32
|
rewritePrefix = meta.prefix ?? service.id
|
|
@@ -138,7 +145,14 @@ module.exports = fp(async function (app, opts) {
|
|
|
138
145
|
})
|
|
139
146
|
}
|
|
140
147
|
|
|
141
|
-
const toReplace = url
|
|
148
|
+
const toReplace = url
|
|
149
|
+
? new RegExp(
|
|
150
|
+
url
|
|
151
|
+
.replace(/127\.0\.0\.1/, 'localhost')
|
|
152
|
+
.replace(/\[::\]/, 'localhost')
|
|
153
|
+
.replace('http://', 'https?://')
|
|
154
|
+
)
|
|
155
|
+
: null
|
|
142
156
|
|
|
143
157
|
const proxyOptions = {
|
|
144
158
|
websocket: true,
|
|
@@ -153,7 +167,7 @@ module.exports = fp(async function (app, opts) {
|
|
|
153
167
|
},
|
|
154
168
|
internalRewriteLocationHeader: false,
|
|
155
169
|
replyOptions: {
|
|
156
|
-
rewriteHeaders:
|
|
170
|
+
rewriteHeaders: headers => {
|
|
157
171
|
let location = headers.location
|
|
158
172
|
if (location) {
|
|
159
173
|
if (toReplace) {
|
|
@@ -186,7 +200,7 @@ module.exports = fp(async function (app, opts) {
|
|
|
186
200
|
...telemetryHeaders,
|
|
187
201
|
'x-forwarded-for': request.ip,
|
|
188
202
|
'x-forwarded-host': request.host,
|
|
189
|
-
'x-forwarded-proto': request.protocol
|
|
203
|
+
'x-forwarded-proto': request.protocol
|
|
190
204
|
}
|
|
191
205
|
|
|
192
206
|
request.log.trace({ headers }, 'rewritten headers before proxying')
|
|
@@ -209,6 +223,7 @@ module.exports = fp(async function (app, opts) {
|
|
|
209
223
|
if (host) {
|
|
210
224
|
await app.register(httpProxy, {
|
|
211
225
|
...proxyOptions,
|
|
226
|
+
prefix: '/',
|
|
212
227
|
constraints: { host }
|
|
213
228
|
})
|
|
214
229
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformatic/composer",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.42.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
"typescript": "^5.5.4",
|
|
32
32
|
"why-is-node-running": "2",
|
|
33
33
|
"ws": "^8.16.0",
|
|
34
|
-
"@platformatic/client": "2.
|
|
35
|
-
"@platformatic/config": "2.
|
|
36
|
-
"@platformatic/db": "2.
|
|
34
|
+
"@platformatic/client": "2.42.0",
|
|
35
|
+
"@platformatic/config": "2.42.0",
|
|
36
|
+
"@platformatic/db": "2.42.0"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@fastify/error": "^4.0.0",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"@fastify/static": "^8.0.0",
|
|
43
43
|
"@fastify/swagger": "^9.0.0",
|
|
44
44
|
"@fastify/view": "^10.0.1",
|
|
45
|
-
"@platformatic/fastify-openapi-glue": "^5.
|
|
45
|
+
"@platformatic/fastify-openapi-glue": "^5.1.0",
|
|
46
46
|
"@platformatic/graphql-composer": "^0.10.0",
|
|
47
47
|
"@scalar/fastify-api-reference": "^1.19.5",
|
|
48
48
|
"ajv": "^8.12.0",
|
|
@@ -67,12 +67,12 @@
|
|
|
67
67
|
"rfdc": "^1.3.1",
|
|
68
68
|
"semgrator": "^0.3.0",
|
|
69
69
|
"undici": "^7.0.0",
|
|
70
|
-
"@platformatic/
|
|
71
|
-
"@platformatic/
|
|
72
|
-
"@platformatic/
|
|
73
|
-
"@platformatic/service": "2.
|
|
74
|
-
"@platformatic/
|
|
75
|
-
"@platformatic/
|
|
70
|
+
"@platformatic/generators": "2.42.0",
|
|
71
|
+
"@platformatic/scalar-theme": "2.42.0",
|
|
72
|
+
"@platformatic/telemetry": "2.42.0",
|
|
73
|
+
"@platformatic/service": "2.42.0",
|
|
74
|
+
"@platformatic/utils": "^2.42.0",
|
|
75
|
+
"@platformatic/config": "2.42.0"
|
|
76
76
|
},
|
|
77
77
|
"scripts": {
|
|
78
78
|
"test": "pnpm run lint && borp -T --timeout=300000 -c 1 && tsd",
|
package/schema.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://schemas.platformatic.dev/@platformatic/composer/2.
|
|
2
|
+
"$id": "https://schemas.platformatic.dev/@platformatic/composer/2.42.0.json",
|
|
3
3
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
4
4
|
"title": "Platformatic Composer",
|
|
5
5
|
"type": "object",
|
package/lib/openapi.js
DELETED
|
@@ -1,50 +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
|
-
servers: [{ url: globalThis.platformatic?.runtimeBasePath ?? '/' }],
|
|
14
|
-
},
|
|
15
|
-
transform: ({ schema, url }) => {
|
|
16
|
-
for (const service of opts.services) {
|
|
17
|
-
if (!service.proxy) continue
|
|
18
|
-
|
|
19
|
-
const prefix = service.proxy.prefix ?? ''
|
|
20
|
-
const proxyPrefix = prefix.at(-1) === '/' ? prefix.slice(0, -1) : prefix
|
|
21
|
-
|
|
22
|
-
const proxyUrls = [proxyPrefix + '/', proxyPrefix + '/*']
|
|
23
|
-
if (proxyUrls.includes(url)) {
|
|
24
|
-
schema = schema ?? {}
|
|
25
|
-
schema.hide = true
|
|
26
|
-
break
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
return { schema, url }
|
|
30
|
-
}
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
const { default: scalarTheme } = await import('@platformatic/scalar-theme')
|
|
34
|
-
|
|
35
|
-
/** Serve spec file in yaml and json */
|
|
36
|
-
app.get('/documentation/json', { schema: { hide: true } }, async () => app.swagger())
|
|
37
|
-
app.get('/documentation/yaml', { schema: { hide: true } }, async () => app.swagger({ yaml: true }))
|
|
38
|
-
|
|
39
|
-
const routePrefix = opts.openapi?.swaggerPrefix || '/documentation'
|
|
40
|
-
|
|
41
|
-
await app.register(require('@scalar/fastify-api-reference'), {
|
|
42
|
-
logLevel: 'warn',
|
|
43
|
-
routePrefix,
|
|
44
|
-
configuration: {
|
|
45
|
-
customCss: scalarTheme.theme
|
|
46
|
-
}
|
|
47
|
-
})
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
module.exports = fp(composeOpenAPI)
|