@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.
Files changed (49) hide show
  1. package/LICENSE +1 -1
  2. package/eslint.config.js +1 -8
  3. package/index.d.ts +1 -58
  4. package/index.js +9 -30
  5. package/package.json +8 -54
  6. package/schema.json +1136 -907
  7. package/scripts/schema.js +12 -0
  8. package/config.d.ts +0 -997
  9. package/lib/application.js +0 -186
  10. package/lib/commands/index.js +0 -15
  11. package/lib/commands/openapi-fetch-schemas.js +0 -47
  12. package/lib/composer-hook.js +0 -60
  13. package/lib/errors.js +0 -18
  14. package/lib/generator.js +0 -127
  15. package/lib/graphql-fetch.js +0 -83
  16. package/lib/graphql-generator.js +0 -33
  17. package/lib/graphql.js +0 -24
  18. package/lib/metrics.js +0 -12
  19. package/lib/not-host-constraints.js +0 -31
  20. package/lib/openapi-composer.js +0 -101
  21. package/lib/openapi-config-schema.js +0 -89
  22. package/lib/openapi-generator.js +0 -213
  23. package/lib/openapi-load-config.js +0 -31
  24. package/lib/openapi-modifier.js +0 -128
  25. package/lib/openapi-scalar.js +0 -22
  26. package/lib/proxy.js +0 -265
  27. package/lib/root.js +0 -75
  28. package/lib/schema.js +0 -258
  29. package/lib/stackable.js +0 -88
  30. package/lib/upgrade.js +0 -20
  31. package/lib/utils.js +0 -16
  32. package/lib/versions/2.0.0.js +0 -9
  33. package/lib/versions/3.0.0.js +0 -14
  34. package/public/images/dark_mode.svg +0 -3
  35. package/public/images/ellipse.svg +0 -21
  36. package/public/images/external-link.svg +0 -5
  37. package/public/images/favicon.ico +0 -0
  38. package/public/images/graphiql.svg +0 -10
  39. package/public/images/graphql.svg +0 -10
  40. package/public/images/light_mode.svg +0 -11
  41. package/public/images/openapi.svg +0 -13
  42. package/public/images/platformatic-logo-dark.svg +0 -30
  43. package/public/images/platformatic-logo-light.svg +0 -30
  44. package/public/images/reverse-proxy.svg +0 -8
  45. package/public/images/triangle_dark.svg +0 -3
  46. package/public/images/triangle_light.svg +0 -3
  47. package/public/index.html +0 -253
  48. package/public/index.njk +0 -101
  49. package/public/main.css +0 -244
@@ -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
- }
@@ -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
- }
@@ -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
- }
@@ -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)