@platformatic/gateway 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 +201 -0
  2. package/NOTICE +13 -0
  3. package/config.d.ts +1000 -0
  4. package/eslint.config.js +12 -0
  5. package/index.d.ts +58 -0
  6. package/index.js +34 -0
  7. package/lib/application.js +186 -0
  8. package/lib/capability.js +89 -0
  9. package/lib/commands/index.js +15 -0
  10. package/lib/commands/openapi-fetch-schemas.js +48 -0
  11. package/lib/errors.js +18 -0
  12. package/lib/gateway-hook.js +66 -0
  13. package/lib/generator.js +127 -0
  14. package/lib/graphql-fetch.js +83 -0
  15. package/lib/graphql-generator.js +33 -0
  16. package/lib/graphql.js +29 -0
  17. package/lib/metrics.js +12 -0
  18. package/lib/not-host-constraints.js +31 -0
  19. package/lib/openapi-composer.js +101 -0
  20. package/lib/openapi-config-schema.js +89 -0
  21. package/lib/openapi-generator.js +215 -0
  22. package/lib/openapi-load-config.js +31 -0
  23. package/lib/openapi-modifier.js +131 -0
  24. package/lib/openapi-scalar.js +22 -0
  25. package/lib/proxy.js +266 -0
  26. package/lib/root.js +75 -0
  27. package/lib/schema.js +258 -0
  28. package/lib/upgrade.js +20 -0
  29. package/lib/utils.js +16 -0
  30. package/lib/versions/2.0.0.js +9 -0
  31. package/lib/versions/3.0.0.js +24 -0
  32. package/package.json +83 -0
  33. package/public/images/dark_mode.svg +3 -0
  34. package/public/images/ellipse.svg +21 -0
  35. package/public/images/external-link.svg +5 -0
  36. package/public/images/favicon.ico +0 -0
  37. package/public/images/graphiql.svg +10 -0
  38. package/public/images/graphql.svg +10 -0
  39. package/public/images/light_mode.svg +11 -0
  40. package/public/images/openapi.svg +13 -0
  41. package/public/images/platformatic-logo-dark.svg +30 -0
  42. package/public/images/platformatic-logo-light.svg +30 -0
  43. package/public/images/reverse-proxy.svg +8 -0
  44. package/public/images/triangle_dark.svg +3 -0
  45. package/public/images/triangle_light.svg +3 -0
  46. package/public/index.html +253 -0
  47. package/public/index.njk +101 -0
  48. package/public/main.css +244 -0
  49. package/schema.json +3779 -0
@@ -0,0 +1,83 @@
1
+ import { compose } from '@platformatic/graphql-composer'
2
+
3
+ const placeholderSdl = 'Query { _info: String }'
4
+ const placeholderResolvers = { Query: { _info: '@platformatic/gateway' } }
5
+
6
+ // TODO support subscriptions
7
+ // const defaultSubscriptionsOptions = {
8
+ // onError: function onGatewaySubscriptionsError (ctx, topic, err) {
9
+ // // TODO log.error({err})
10
+ // throw err
11
+ // },
12
+ // publish (ctx, topic, payload) {
13
+ // ctx.pubsub.publish({ topic, payload })
14
+ // },
15
+ // subscribe (ctx, topic) {
16
+ // return ctx.pubsub.subscribe(topic)
17
+ // },
18
+ // unsubscribe (ctx, topic) {
19
+ // ctx.pubsub.close()
20
+ // }
21
+ // }
22
+
23
+ function toGatewayOptions (options, app) {
24
+ return {
25
+ logger: app.log,
26
+ defaultArgsAdapter: options?.defaultArgsAdapter,
27
+ addEntitiesResolvers: options?.addEntitiesResolvers,
28
+ entities: options?.entities,
29
+ onSubgraphError: (err, subgraphName) => {
30
+ app.log.error({ err }, 'graphql composer error on subgraph ' + subgraphName)
31
+
32
+ if (options?.onSubgraphError) {
33
+ try {
34
+ options.onSubgraphError(err, subgraphName)
35
+ } catch (err) {
36
+ app.log.error({ err }, 'running onSubgraphError')
37
+ }
38
+ }
39
+ }
40
+ }
41
+ }
42
+
43
+ export function createSupergraph ({ sdl = null, resolvers = {} } = {}) {
44
+ // in case of temporary failures of subgraphs on watching, the application can restart if no subgraphs are (tempoary) available
45
+ if (!sdl) {
46
+ return {
47
+ sdl: placeholderSdl,
48
+ resolvers: placeholderResolvers
49
+ }
50
+ }
51
+ return { sdl, resolvers }
52
+ }
53
+
54
+ export function isSameGraphqlSchema (a, b) {
55
+ // TODO review
56
+ return a?.sdl === b?.sdl
57
+ }
58
+
59
+ export function applicationToSubgraphConfig (application) {
60
+ if (!application.graphql) {
61
+ return
62
+ }
63
+ return {
64
+ name: application.graphql.name || application.id || application.origin,
65
+ entities: application.graphql.entities,
66
+ server: {
67
+ host: application.graphql.host || application.origin,
68
+ composeEndpoint: application.graphql.composeEndpoint,
69
+ graphqlEndpoint: application.graphql.graphqlEndpoint
70
+ }
71
+ }
72
+ }
73
+
74
+ export async function fetchGraphqlSubgraphs (applications, options, app) {
75
+ const subgraphs = applications.map(applicationToSubgraphConfig).filter(s => !!s)
76
+ const gateway = await compose({ ...toGatewayOptions(options, app), subgraphs })
77
+
78
+ return createSupergraph({
79
+ logger: app.log,
80
+ sdl: gateway.toSdl(),
81
+ resolvers: gateway.resolvers
82
+ })
83
+ }
@@ -0,0 +1,33 @@
1
+ import fp from 'fastify-plugin'
2
+ import mercurius from 'mercurius'
3
+ import { fetchGraphqlSubgraphs } from './graphql-fetch.js'
4
+
5
+ async function graphqlGeneratorPlugin (app, opts) {
6
+ if (!opts.applications.some(s => s.graphql)) {
7
+ return
8
+ }
9
+
10
+ const applications = []
11
+
12
+ for (const application of opts.applications) {
13
+ if (!application.graphql) {
14
+ continue
15
+ }
16
+ applications.push(application)
17
+ }
18
+
19
+ const graphqlConfig = {
20
+ graphiql: opts.graphql?.graphiql
21
+ }
22
+ if (applications.length > 0) {
23
+ const graphqlSupergraph = await fetchGraphqlSubgraphs(applications, opts.graphql, app)
24
+ graphqlConfig.schema = graphqlSupergraph.sdl
25
+ graphqlConfig.resolvers = graphqlSupergraph.resolvers
26
+ graphqlConfig.subscription = false // TODO support subscriptions, will be !!opts.graphql.subscriptions
27
+ app.graphqlSupergraph = graphqlSupergraph
28
+ }
29
+
30
+ await app.register(mercurius, graphqlConfig)
31
+ }
32
+
33
+ export const graphqlGenerator = fp(graphqlGeneratorPlugin)
package/lib/graphql.js ADDED
@@ -0,0 +1,29 @@
1
+ import fp from 'fastify-plugin'
2
+ import { createSupergraph } from './graphql-fetch.js'
3
+
4
+ const graphqlSupergraphSymbol = Symbol('graphqlSupergraph')
5
+
6
+ export async function graphqlPlugin (app, opts) {
7
+ app.decorate('graphqlSupergraph', {
8
+ getter () {
9
+ return this[graphqlSupergraphSymbol]
10
+ },
11
+ setter (v) {
12
+ this[graphqlSupergraphSymbol] = v
13
+ }
14
+ })
15
+ app.decorate('graphqlGatewayOptions', {
16
+ getter () {
17
+ return opts
18
+ }
19
+ })
20
+ app.decorate('graphqlComposerOptions', {
21
+ getter () {
22
+ return opts
23
+ }
24
+ })
25
+
26
+ app.graphqlSupergraph = createSupergraph()
27
+ }
28
+
29
+ export const graphql = fp(graphqlPlugin)
package/lib/metrics.js ADDED
@@ -0,0 +1,12 @@
1
+ export function initMetrics (prometheus) {
2
+ if (!prometheus?.registry || !prometheus?.client) return null
3
+ const { client, registry } = prometheus
4
+
5
+ return {
6
+ activeWsConnections: new client.Gauge({
7
+ name: 'active_ws_gateway_connections',
8
+ help: 'Active Websocket gateway connections in "@platformatic/gateway"',
9
+ registers: [registry]
10
+ })
11
+ }
12
+ }
@@ -0,0 +1,31 @@
1
+ export const notHostConstraints = {
2
+ name: 'notHost',
3
+ storage () {
4
+ const store = []
5
+
6
+ return {
7
+ get (host) {
8
+ if (typeof host === 'string') {
9
+ for (const [hosts, value] of store) {
10
+ if (!hosts.includes(host)) {
11
+ return value
12
+ }
13
+ }
14
+ }
15
+
16
+ return null
17
+ },
18
+ set: (hosts, value) => {
19
+ store.push([hosts, value])
20
+ },
21
+ store
22
+ }
23
+ },
24
+ deriveConstraint (req) {
25
+ return req.headers.host || req.headers[':authority']
26
+ },
27
+ mustMatchWhenDerived: false,
28
+ validate () {
29
+ return true
30
+ }
31
+ }
@@ -0,0 +1,101 @@
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 Gateway',
93
+ version: options.version || '1.0.0'
94
+ },
95
+ components: {
96
+ securitySchemes: mergedSecuritySchemes,
97
+ schemas: mergedSchemas
98
+ },
99
+ paths: mergedPaths
100
+ }
101
+ }
@@ -0,0 +1,89 @@
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
+ }
@@ -0,0 +1,215 @@
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 openApiGatewayPlugin (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
+
85
+ const onResponse = req.routeOptions.config?.onGatewayResponse ?? req.routeOptions.config?.onComposerResponse
86
+ if (onResponse) {
87
+ onResponse(request, reply, res.stream)
88
+ } else {
89
+ reply.send(res.stream)
90
+ }
91
+ }
92
+ const rewriteRequestHeaders = (request, headers) => {
93
+ const targetUrl = `${origin}${request.url}`
94
+ const context = request.span?.context
95
+ const { span, telemetryHeaders } = app.openTelemetry?.startHTTPSpanClient(
96
+ targetUrl,
97
+ request.method,
98
+ context
99
+ ) || { span: null, telemetryHeaders: {} }
100
+ // We need to store the span in a different object
101
+ // to correctly close it in the onResponse hook
102
+ // Note that we have 2 spans:
103
+ // - request.span: the span of the request to the proxy
104
+ // - request.proxedCallSpan: the span of the request to the proxied application
105
+ request.proxedCallSpan = span
106
+
107
+ headers = {
108
+ ...headers,
109
+ ...telemetryHeaders,
110
+ 'x-forwarded-for': request.ip,
111
+ 'x-forwarded-host': request.host
112
+ }
113
+
114
+ return headers
115
+ }
116
+ replyOptions.onResponse = onResponse
117
+ replyOptions.rewriteRequestHeaders = rewriteRequestHeaders
118
+
119
+ reply.from(origin + newRoutePath, replyOptions)
120
+ }
121
+ }
122
+ }
123
+ })
124
+
125
+ app.addHook('preValidation', async req => {
126
+ if (typeof req.query.fields === 'string') {
127
+ req.query.fields = req.query.fields.split(',')
128
+ }
129
+ })
130
+ }
131
+
132
+ export async function openApiGenerator (app, opts) {
133
+ if (!opts.applications.some(s => s.openapi)) {
134
+ return
135
+ }
136
+
137
+ const { applications } = opts
138
+
139
+ const openApiSchemas = []
140
+ const apiByApiRoutes = {}
141
+
142
+ for (const { id, origin, openapi } of applications) {
143
+ if (!openapi) continue
144
+
145
+ let openapiConfig = null
146
+ if (openapi.config) {
147
+ try {
148
+ openapiConfig = await loadOpenApiConfig(openapi.config)
149
+ } catch (error) {
150
+ app.log.error(error)
151
+ throw new CouldNotReadOpenAPIConfigError(id)
152
+ }
153
+ }
154
+
155
+ let originSchema = null
156
+ try {
157
+ originSchema = await getOpenApiSchema(origin, openapi)
158
+ } catch (error) {
159
+ app.log.error(error, `failed to fetch schema for "${id} application"`)
160
+ continue
161
+ }
162
+
163
+ const schema = modifyOpenApiSchema(app, originSchema, openapiConfig)
164
+
165
+ const prefix = openapi.prefix ? prefixWithSlash(openapi.prefix) : ''
166
+ for (const path in schema.paths) {
167
+ apiByApiRoutes[prefix + path] = {
168
+ origin,
169
+ prefix,
170
+ schema: schema.paths[path]
171
+ }
172
+ }
173
+
174
+ openApiSchemas.push({ id, prefix, schema, originSchema, config: openapiConfig })
175
+ }
176
+
177
+ const composedOpenApiSchema = composeOpenApi(openApiSchemas, opts.openapi)
178
+
179
+ app.decorate('openApiSchemas', openApiSchemas)
180
+ app.decorate('composedOpenApiSchema', composedOpenApiSchema)
181
+
182
+ await app.register(fastifySwagger, {
183
+ exposeRoute: true,
184
+ openapi: {
185
+ info: {
186
+ title: opts.openapi?.title || 'Platformatic Gateway',
187
+ version: opts.openapi?.version || '1.0.0'
188
+ },
189
+ servers: [{ url: globalThis.platformatic?.runtimeBasePath ?? '/' }],
190
+ components: app.composedOpenApiSchema.components
191
+ },
192
+ transform ({ schema, url }) {
193
+ for (const application of opts.applications) {
194
+ if (!application.proxy) continue
195
+
196
+ const prefix = application.proxy.prefix ?? ''
197
+ const proxyPrefix = prefix.at(-1) === '/' ? prefix.slice(0, -1) : prefix
198
+
199
+ const proxyUrls = [proxyPrefix + '/', proxyPrefix + '/*']
200
+ if (proxyUrls.includes(url)) {
201
+ schema = schema ?? {}
202
+ schema.hide = true
203
+ break
204
+ }
205
+ }
206
+ return { schema, url }
207
+ }
208
+ })
209
+
210
+ await app.register(openApiScalar, opts)
211
+
212
+ return { apiByApiRoutes }
213
+ }
214
+
215
+ export const openApiGateway = fp(openApiGatewayPlugin)
@@ -0,0 +1,31 @@
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
+ }