@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 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 openapiGenerator = require('./lib/openapi-generator')
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.register(openapi, config.composer)
48
+ generatedComposedOpenAPI = await openApiGenerator(app, config.composer)
47
49
  }
50
+
48
51
  app.register(serviceProxy, { ...config.composer, context: opts.context })
49
- app.register(composerHook)
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 (hasOpenapiServices) {
53
- await app.register(openapiGenerator, config.composer)
54
+ if (generatedComposedOpenAPI) {
55
+ await app.register(openApiComposer, { opts: config.composer, generated: generatedComposedOpenAPI })
54
56
  }
55
57
 
56
58
  if (hasGraphqlServices) {
@@ -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 && components.schemas) {
27
- for (const [schemaKey, schema] of Object.entries(components.schemas)) {
28
- if (schema.title == null) {
29
- schema.title = schemaKey
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,
@@ -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 errors = require('./errors')
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 composeOpenAPI (app, opts) {
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
- const composedOpenApiSchema = composeOpenApi(openApiSchemas, opts.openapi)
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 = fp(composeOpenAPI)
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
- const prefix = (service.proxy?.prefix ?? meta.prefix ?? service.id).replace(/(\/$)/g, '')
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 ? new RegExp(url.replace(/127\.0\.0\.1/, 'localhost').replace(/\[::\]/, 'localhost').replace('http://', 'https?://')) : null
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: (headers) => {
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.40.0",
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.40.0",
35
- "@platformatic/config": "2.40.0",
36
- "@platformatic/db": "2.40.0"
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.0.0",
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/config": "2.40.0",
71
- "@platformatic/generators": "2.40.0",
72
- "@platformatic/scalar-theme": "2.40.0",
73
- "@platformatic/service": "2.40.0",
74
- "@platformatic/telemetry": "2.40.0",
75
- "@platformatic/utils": "^2.40.0"
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.40.0.json",
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)