@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.
Files changed (46) hide show
  1. package/LICENSE +1 -1
  2. package/config.d.ts +482 -131
  3. package/eslint.config.js +4 -2
  4. package/index.d.ts +1 -17
  5. package/index.js +9 -210
  6. package/package.json +18 -59
  7. package/schema.json +2121 -843
  8. package/scripts/schema.js +12 -0
  9. package/.c8rc +0 -6
  10. package/composer.mjs +0 -54
  11. package/help/create.txt +0 -11
  12. package/help/help.txt +0 -7
  13. package/help/openapi schemas fetch.txt +0 -9
  14. package/help/start.txt +0 -54
  15. package/index.test-d.ts +0 -23
  16. package/lib/composer-hook.js +0 -60
  17. package/lib/create.mjs +0 -84
  18. package/lib/errors.js +0 -13
  19. package/lib/generator/README.md +0 -30
  20. package/lib/generator/composer-generator.d.ts +0 -11
  21. package/lib/generator/composer-generator.js +0 -128
  22. package/lib/graphql-fetch.js +0 -85
  23. package/lib/graphql-generator.js +0 -31
  24. package/lib/graphql.js +0 -20
  25. package/lib/openapi-composer.js +0 -81
  26. package/lib/openapi-config-schema.js +0 -93
  27. package/lib/openapi-fetch-schemas.mjs +0 -61
  28. package/lib/openapi-generator.js +0 -167
  29. package/lib/openapi-load-config.js +0 -31
  30. package/lib/openapi-modifier.js +0 -137
  31. package/lib/openapi.js +0 -49
  32. package/lib/proxy.js +0 -161
  33. package/lib/root-endpoint/index.js +0 -28
  34. package/lib/root-endpoint/public/images/dark_mode.svg +0 -3
  35. package/lib/root-endpoint/public/images/favicon.ico +0 -0
  36. package/lib/root-endpoint/public/images/light_mode.svg +0 -11
  37. package/lib/root-endpoint/public/images/platformatic-logo-dark.svg +0 -30
  38. package/lib/root-endpoint/public/images/platformatic-logo-light.svg +0 -30
  39. package/lib/root-endpoint/public/images/triangle_dark.svg +0 -3
  40. package/lib/root-endpoint/public/images/triangle_light.svg +0 -3
  41. package/lib/root-endpoint/public/index.html +0 -237
  42. package/lib/schema.js +0 -210
  43. package/lib/stackable.js +0 -59
  44. package/lib/upgrade.js +0 -22
  45. package/lib/utils.js +0 -27
  46. package/lib/versions/2.0.0.js +0 -11
@@ -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
- }
@@ -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
@@ -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)