@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.
- package/LICENSE +201 -0
- package/NOTICE +13 -0
- package/config.d.ts +1000 -0
- package/eslint.config.js +12 -0
- package/index.d.ts +58 -0
- package/index.js +34 -0
- package/lib/application.js +186 -0
- package/lib/capability.js +89 -0
- package/lib/commands/index.js +15 -0
- package/lib/commands/openapi-fetch-schemas.js +48 -0
- package/lib/errors.js +18 -0
- package/lib/gateway-hook.js +66 -0
- package/lib/generator.js +127 -0
- package/lib/graphql-fetch.js +83 -0
- package/lib/graphql-generator.js +33 -0
- package/lib/graphql.js +29 -0
- package/lib/metrics.js +12 -0
- package/lib/not-host-constraints.js +31 -0
- package/lib/openapi-composer.js +101 -0
- package/lib/openapi-config-schema.js +89 -0
- package/lib/openapi-generator.js +215 -0
- package/lib/openapi-load-config.js +31 -0
- package/lib/openapi-modifier.js +131 -0
- package/lib/openapi-scalar.js +22 -0
- package/lib/proxy.js +266 -0
- package/lib/root.js +75 -0
- package/lib/schema.js +258 -0
- package/lib/upgrade.js +20 -0
- package/lib/utils.js +16 -0
- package/lib/versions/2.0.0.js +9 -0
- package/lib/versions/3.0.0.js +24 -0
- package/package.json +83 -0
- package/public/images/dark_mode.svg +3 -0
- package/public/images/ellipse.svg +21 -0
- package/public/images/external-link.svg +5 -0
- package/public/images/favicon.ico +0 -0
- package/public/images/graphiql.svg +10 -0
- package/public/images/graphql.svg +10 -0
- package/public/images/light_mode.svg +11 -0
- package/public/images/openapi.svg +13 -0
- package/public/images/platformatic-logo-dark.svg +30 -0
- package/public/images/platformatic-logo-light.svg +30 -0
- package/public/images/reverse-proxy.svg +8 -0
- package/public/images/triangle_dark.svg +3 -0
- package/public/images/triangle_light.svg +3 -0
- package/public/index.html +253 -0
- package/public/index.njk +101 -0
- package/public/main.css +244 -0
- package/schema.json +3779 -0
package/eslint.config.js
ADDED
package/index.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { BaseCapability } from '@platformatic/basic'
|
|
2
|
+
import { BaseGenerator } from '@platformatic/generators'
|
|
3
|
+
import {
|
|
4
|
+
ApplicationCapability,
|
|
5
|
+
ServerInstance as ApplicationInstance,
|
|
6
|
+
Configuration,
|
|
7
|
+
ConfigurationOptions
|
|
8
|
+
} from '@platformatic/service'
|
|
9
|
+
import { JSONSchemaType } from 'ajv'
|
|
10
|
+
import { FastifyError, FastifyInstance } from 'fastify'
|
|
11
|
+
import { PlatformaticGatewayConfig } from './config'
|
|
12
|
+
|
|
13
|
+
export { PlatformaticService } from '@platformatic/service'
|
|
14
|
+
export { PlatformaticGatewayConfig } from './config'
|
|
15
|
+
|
|
16
|
+
export type GatewayCapability = ApplicationCapability<PlatformaticGatewayConfig>
|
|
17
|
+
|
|
18
|
+
export type ServerInstance = ApplicationInstance<PlatformaticGatewayConfig>
|
|
19
|
+
|
|
20
|
+
type GatewayConfiguration = Configuration<PlatformaticGatewayConfig>
|
|
21
|
+
|
|
22
|
+
export declare function loadConfiguration (
|
|
23
|
+
root: string | PlatformaticServiceConfig,
|
|
24
|
+
source?: string | PlatformaticServiceConfig,
|
|
25
|
+
context?: ConfigurationOptions
|
|
26
|
+
): Promise<GatewayConfiguration>
|
|
27
|
+
|
|
28
|
+
export function create (
|
|
29
|
+
root: string,
|
|
30
|
+
source?: string | PlatformaticGatewayConfig,
|
|
31
|
+
context?: ConfigurationOptions
|
|
32
|
+
): Promise<GatewayCapability>
|
|
33
|
+
|
|
34
|
+
export declare function platformaticGateway (app: FastifyInstance, capability: BaseCapability): Promise<void>
|
|
35
|
+
|
|
36
|
+
export class Generator extends BaseGenerator.BaseGenerator {}
|
|
37
|
+
|
|
38
|
+
export declare const packageJson: Record<string, unknown>
|
|
39
|
+
|
|
40
|
+
export declare const schema: JSONSchemaType<PlatformaticGatewayConfig>
|
|
41
|
+
|
|
42
|
+
export declare const schemaComponents: {
|
|
43
|
+
openApiApplication: JSONSchemaType<object>
|
|
44
|
+
entityResolver: JSONSchemaType<object>
|
|
45
|
+
entities: JSONSchemaType<object>
|
|
46
|
+
graphqlApplication: JSONSchemaType<object>
|
|
47
|
+
graphqlGatewayOptions: JSONSchemaType<object>
|
|
48
|
+
gateway: JSONSchemaType<object>
|
|
49
|
+
types: JSONSchemaType<object>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export declare const version: string
|
|
53
|
+
|
|
54
|
+
export function FastifyInstanceIsAlreadyListeningError (): FastifyError
|
|
55
|
+
export function FailedToFetchOpenAPISchemaError (): FastifyError
|
|
56
|
+
export function ValidationErrors (): FastifyError
|
|
57
|
+
export function PathAlreadyExistsError (): FastifyError
|
|
58
|
+
export function CouldNotReadOpenAPIConfigError (): FastifyError
|
package/index.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { resolve, validationOptions } from '@platformatic/basic'
|
|
2
|
+
import { kMetadata, loadConfiguration as utilsLoadConfiguration } from '@platformatic/foundation'
|
|
3
|
+
import { transform } from '@platformatic/service'
|
|
4
|
+
import { GatewayCapability } from './lib/capability.js'
|
|
5
|
+
import { schema } from './lib/schema.js'
|
|
6
|
+
import { upgrade } from './lib/upgrade.js'
|
|
7
|
+
|
|
8
|
+
export async function loadConfiguration (configOrRoot, sourceOrConfig, context) {
|
|
9
|
+
const { root, source } = await resolve(configOrRoot, sourceOrConfig, 'gateway')
|
|
10
|
+
|
|
11
|
+
return utilsLoadConfiguration(source, context?.schema ?? schema, {
|
|
12
|
+
validationOptions,
|
|
13
|
+
transform,
|
|
14
|
+
upgrade,
|
|
15
|
+
replaceEnv: true,
|
|
16
|
+
root,
|
|
17
|
+
...context
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function create (configOrRoot, sourceOrConfig, context) {
|
|
22
|
+
const config = await loadConfiguration(configOrRoot, sourceOrConfig, context)
|
|
23
|
+
return new GatewayCapability(config[kMetadata].root, config, context)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const skipTelemetryHooks = true
|
|
27
|
+
|
|
28
|
+
export { platformaticGateway } from './lib/application.js'
|
|
29
|
+
export { GatewayCapability } from './lib/capability.js'
|
|
30
|
+
export * from './lib/commands/index.js'
|
|
31
|
+
export * from './lib/errors.js'
|
|
32
|
+
export * as errors from './lib/errors.js'
|
|
33
|
+
export { Generator } from './lib/generator.js'
|
|
34
|
+
export { packageJson, schema, schemaComponents, version } from './lib/schema.js'
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { isKeyEnabled } from '@platformatic/foundation'
|
|
2
|
+
import { platformaticService } from '@platformatic/service'
|
|
3
|
+
import deepEqual from 'fast-deep-equal'
|
|
4
|
+
import { fetchOpenApiSchema } from './commands/openapi-fetch-schemas.js'
|
|
5
|
+
import { gatewayHook } from './gateway-hook.js'
|
|
6
|
+
import { fetchGraphqlSubgraphs, isSameGraphqlSchema } from './graphql-fetch.js'
|
|
7
|
+
import { graphqlGenerator } from './graphql-generator.js'
|
|
8
|
+
import { graphql } from './graphql.js'
|
|
9
|
+
import { openApiGateway, openApiGenerator } from './openapi-generator.js'
|
|
10
|
+
import { proxy } from './proxy.js'
|
|
11
|
+
import { isFetchable } from './utils.js'
|
|
12
|
+
|
|
13
|
+
const kITC = Symbol.for('plt.runtime.itc')
|
|
14
|
+
const EXPERIMENTAL_GRAPHQL_GATEWAY_FEATURE_MESSAGE = 'graphql composer is an experimental feature'
|
|
15
|
+
|
|
16
|
+
async function detectApplicationsUpdate ({ app, applications, fetchOpenApiSchema, fetchGraphqlSubgraphs }) {
|
|
17
|
+
let changed
|
|
18
|
+
|
|
19
|
+
const graphqlApplications = []
|
|
20
|
+
// assumes applications here are fetchable
|
|
21
|
+
for (const application of applications) {
|
|
22
|
+
const { id, origin, openapi, graphql } = application
|
|
23
|
+
|
|
24
|
+
if (openapi) {
|
|
25
|
+
const currentSchema = app.openApiSchemas.find(schema => schema.id === id)?.originSchema || null
|
|
26
|
+
|
|
27
|
+
let fetchedSchema = null
|
|
28
|
+
try {
|
|
29
|
+
fetchedSchema = await fetchOpenApiSchema({ origin, openapi })
|
|
30
|
+
} catch (err) {
|
|
31
|
+
app.log.error({ err }, 'failed to fetch schema (watch) for application ' + id)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!changed && !deepEqual(fetchedSchema, currentSchema)) {
|
|
35
|
+
changed = true
|
|
36
|
+
// it stops at first schema difference since all the schemas will be updated on reload
|
|
37
|
+
break
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (graphql) {
|
|
42
|
+
graphqlApplications.push(application)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!changed && graphqlApplications.length > 0) {
|
|
47
|
+
const graphqlSupergraph = await fetchGraphqlSubgraphs(graphqlApplications, app.graphqlGatewayOptions, app)
|
|
48
|
+
if (!isSameGraphqlSchema(graphqlSupergraph, app.graphqlSupergraph)) {
|
|
49
|
+
changed = true
|
|
50
|
+
app.graphqlSupergraph = graphqlSupergraph
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return changed
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* poll applications to detect changes, every `opts.gateway.refreshTimeout`
|
|
59
|
+
* polling is disabled on refreshTimeout = 0
|
|
60
|
+
* or there are no network openapi nor graphql remote applications (the applications are from file or they don't have a schema/graph to fetch)
|
|
61
|
+
*/
|
|
62
|
+
async function watchApplications (app, { config, capability }) {
|
|
63
|
+
const { applications, refreshTimeout } = config.gateway
|
|
64
|
+
if (refreshTimeout < 1) {
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const watching = applications.filter(isFetchable)
|
|
69
|
+
if (watching.length < 1) {
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!globalThis[Symbol.for('plt.runtime.id')]) {
|
|
74
|
+
app.log.warn('Watching applications is only supported when running within a Platformatic Runtime.')
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
capability.emit('watch:start')
|
|
79
|
+
app.log.info({ applications: watching }, 'start watching applications')
|
|
80
|
+
|
|
81
|
+
const timer = setInterval(async () => {
|
|
82
|
+
try {
|
|
83
|
+
if (await detectApplicationsUpdate({ app, applications: watching, fetchOpenApiSchema, fetchGraphqlSubgraphs })) {
|
|
84
|
+
clearInterval(timer)
|
|
85
|
+
app.log.info('detected applications changes, restarting ...')
|
|
86
|
+
|
|
87
|
+
globalThis[Symbol.for('plt.runtime.itc')].notify('changed')
|
|
88
|
+
}
|
|
89
|
+
} catch (error) {
|
|
90
|
+
app.log.error(
|
|
91
|
+
{
|
|
92
|
+
err: {
|
|
93
|
+
message: error.message,
|
|
94
|
+
stack: error.stack
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
'failed to get applications info'
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
}, refreshTimeout).unref()
|
|
101
|
+
|
|
102
|
+
app.addHook('onClose', async () => {
|
|
103
|
+
clearInterval(timer)
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function ensureApplications (gatewayId, config) {
|
|
108
|
+
if (config.gateway?.applications?.length) {
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
gatewayId ??= globalThis.platformatic?.applicationId
|
|
113
|
+
config.gateway ??= {}
|
|
114
|
+
config.gateway.applications ??= []
|
|
115
|
+
|
|
116
|
+
// When no applications are defined, all applications are exposed in the gateway
|
|
117
|
+
const applications = await globalThis[kITC]?.send('listApplications')
|
|
118
|
+
|
|
119
|
+
if (applications) {
|
|
120
|
+
config.gateway.applications = applications
|
|
121
|
+
.filter(id => id !== gatewayId) // Remove ourself
|
|
122
|
+
.map(id => ({ id, proxy: { prefix: `/${id}` } }))
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function platformaticGateway (app, capability) {
|
|
127
|
+
const config = await capability.getConfig()
|
|
128
|
+
let hasGraphqlApplications, hasOpenapiApplications
|
|
129
|
+
|
|
130
|
+
// When no applications are specified, get the list from the runtime.
|
|
131
|
+
await ensureApplications(capability.applicationId, config)
|
|
132
|
+
|
|
133
|
+
const { applications } = config.gateway
|
|
134
|
+
|
|
135
|
+
for (const application of applications) {
|
|
136
|
+
if (!application.origin) {
|
|
137
|
+
application.origin = `http://${application.id}.plt.local`
|
|
138
|
+
}
|
|
139
|
+
if (application.openapi && !hasOpenapiApplications) {
|
|
140
|
+
hasOpenapiApplications = true
|
|
141
|
+
}
|
|
142
|
+
if (application.graphql && !hasGraphqlApplications) {
|
|
143
|
+
hasGraphqlApplications = true
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
await app.register(gatewayHook)
|
|
148
|
+
|
|
149
|
+
let generatedComposedOpenAPI = null
|
|
150
|
+
if (hasOpenapiApplications) {
|
|
151
|
+
generatedComposedOpenAPI = await openApiGenerator(app, config.gateway)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (isKeyEnabled('healthCheck', config.server)) {
|
|
155
|
+
if (typeof config.server.healthCheck !== 'object') {
|
|
156
|
+
config.server.healthCheck = {}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
config.server.healthCheck.fn = capability.isHealthy.bind(capability)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
await app.register(proxy, { ...config.gateway, capability, context: capability.context })
|
|
163
|
+
|
|
164
|
+
await platformaticService(app, capability)
|
|
165
|
+
|
|
166
|
+
if (generatedComposedOpenAPI) {
|
|
167
|
+
await app.register(openApiGateway, { opts: config.gateway, generated: generatedComposedOpenAPI })
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (hasGraphqlApplications) {
|
|
171
|
+
app.log.warn(EXPERIMENTAL_GRAPHQL_GATEWAY_FEATURE_MESSAGE)
|
|
172
|
+
app.register(graphql, config.gateway)
|
|
173
|
+
await app.register(graphqlGenerator, config.gateway)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!app.hasRoute({ url: '/', method: 'GET' }) && !app.hasRoute({ url: '/*', method: 'GET' })) {
|
|
177
|
+
const rootHandler = await import('./root.js')
|
|
178
|
+
await app.register(rootHandler.default, config)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!capability.context?.isProduction) {
|
|
182
|
+
await watchApplications(app, { config, capability, context: capability.context })
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
platformaticGateway[Symbol.for('skip-override')] = true
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { kMetadata, replaceEnv } from '@platformatic/foundation'
|
|
2
|
+
import { ServiceCapability } from '@platformatic/service'
|
|
3
|
+
import { ensureApplications, platformaticGateway } from './application.js'
|
|
4
|
+
import { notHostConstraints } from './not-host-constraints.js'
|
|
5
|
+
import { packageJson } from './schema.js'
|
|
6
|
+
|
|
7
|
+
const kITC = Symbol.for('plt.runtime.itc')
|
|
8
|
+
|
|
9
|
+
export class GatewayCapability extends ServiceCapability {
|
|
10
|
+
#meta
|
|
11
|
+
#dependencies
|
|
12
|
+
|
|
13
|
+
constructor (root, config, context) {
|
|
14
|
+
super(root, config, context)
|
|
15
|
+
this.type = 'gateway'
|
|
16
|
+
this.version = packageJson.version
|
|
17
|
+
|
|
18
|
+
this.applicationFactory = this.context.applicationFactory ?? platformaticGateway
|
|
19
|
+
this.fastifyOptions ??= {}
|
|
20
|
+
this.fastifyOptions.routerOptions ??= {}
|
|
21
|
+
this.fastifyOptions.routerOptions.constraints = { notHost: notHostConstraints }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async getBootstrapDependencies () {
|
|
25
|
+
await ensureApplications(this.applicationId, this.config)
|
|
26
|
+
|
|
27
|
+
const composedApplications = this.config.gateway?.applications
|
|
28
|
+
const dependencies = []
|
|
29
|
+
|
|
30
|
+
if (Array.isArray(composedApplications)) {
|
|
31
|
+
dependencies.push(
|
|
32
|
+
...(await Promise.all(
|
|
33
|
+
composedApplications.map(async application => {
|
|
34
|
+
return this.#parseDependency(application.id, application.origin)
|
|
35
|
+
})
|
|
36
|
+
))
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this.#dependencies = dependencies
|
|
41
|
+
return this.#dependencies
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
registerMeta (meta) {
|
|
45
|
+
this.#meta = Object.assign(this.#meta ?? {}, meta)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async getMeta () {
|
|
49
|
+
const applicationMeta = super.getMeta()
|
|
50
|
+
const gatewayMeta = this.#meta ? { gateway: this.#meta } : undefined
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
...applicationMeta,
|
|
54
|
+
...gatewayMeta
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async isHealthy () {
|
|
59
|
+
// If no dependencies (still booting), assume healthy
|
|
60
|
+
if (this.#dependencies) {
|
|
61
|
+
const composedApplications = this.#dependencies.map(dep => dep.id)
|
|
62
|
+
const workers = await globalThis[kITC].send('getWorkers')
|
|
63
|
+
|
|
64
|
+
for (const worker of Object.values(workers)) {
|
|
65
|
+
if (composedApplications.includes(worker.application) && !worker.status.startsWith('start')) {
|
|
66
|
+
globalThis[kITC].notify('event', { event: 'unhealthy' })
|
|
67
|
+
return false
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
globalThis[kITC].notify('event', { event: 'healthy' })
|
|
73
|
+
return true
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async #parseDependency (id, urlString) {
|
|
77
|
+
let url = `http://${id}.plt.local`
|
|
78
|
+
|
|
79
|
+
if (urlString) {
|
|
80
|
+
const remoteUrl = await replaceEnv(urlString, this.config[kMetadata].env)
|
|
81
|
+
|
|
82
|
+
if (remoteUrl) {
|
|
83
|
+
url = remoteUrl
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { id, url, local: url.endsWith('.plt.local') }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { fetchOpenApiSchemas } from './openapi-fetch-schemas.js'
|
|
2
|
+
|
|
3
|
+
export function createCommands (id) {
|
|
4
|
+
return {
|
|
5
|
+
commands: {
|
|
6
|
+
[`${id}:fetch-openapi-schemas`]: fetchOpenApiSchemas
|
|
7
|
+
},
|
|
8
|
+
help: {
|
|
9
|
+
[`${id}:fetch-openapi-schemas`]: {
|
|
10
|
+
usage: `${id}:fetch-openapi-schemas`,
|
|
11
|
+
description: 'Fetch OpenAPI schemas from remote applications'
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { loadConfiguration } from '@platformatic/foundation'
|
|
2
|
+
import { writeFile } from 'node:fs/promises'
|
|
3
|
+
import { request } from 'undici'
|
|
4
|
+
import { FailedToFetchOpenAPISchemaError } from '../errors.js'
|
|
5
|
+
import { schema } from '../schema.js'
|
|
6
|
+
import { upgrade } from '../upgrade.js'
|
|
7
|
+
import { prefixWithSlash } from '../utils.js'
|
|
8
|
+
|
|
9
|
+
export async function fetchOpenApiSchema (application) {
|
|
10
|
+
const { origin, openapi } = application
|
|
11
|
+
|
|
12
|
+
const openApiUrl = origin + prefixWithSlash(openapi.url)
|
|
13
|
+
const { statusCode, body } = await request(openApiUrl)
|
|
14
|
+
|
|
15
|
+
if (statusCode !== 200 && statusCode !== 201) {
|
|
16
|
+
throw new FailedToFetchOpenAPISchemaError(openApiUrl)
|
|
17
|
+
}
|
|
18
|
+
const schema = await body.json()
|
|
19
|
+
|
|
20
|
+
if (openapi.file !== undefined) {
|
|
21
|
+
await writeFile(openapi.file, JSON.stringify(schema, null, 2))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return schema
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function fetchOpenApiSchemas (logger, configFile, _args, { colorette }) {
|
|
28
|
+
const { bold } = colorette
|
|
29
|
+
const config = await loadConfiguration(configFile, schema, { upgrade })
|
|
30
|
+
const { applications } = config.gateway
|
|
31
|
+
|
|
32
|
+
const applicationsWithValidOpenApi = applications.filter(({ openapi }) => openapi && openapi.url && openapi.file)
|
|
33
|
+
|
|
34
|
+
const fetchOpenApiRequests = applicationsWithValidOpenApi.map(application => fetchOpenApiSchema(application))
|
|
35
|
+
|
|
36
|
+
const fetchOpenApiResults = await Promise.allSettled(fetchOpenApiRequests)
|
|
37
|
+
|
|
38
|
+
logger.info('Fetching schemas for all applications.')
|
|
39
|
+
|
|
40
|
+
fetchOpenApiResults.forEach((result, index) => {
|
|
41
|
+
const applicationId = applicationsWithValidOpenApi[index].id
|
|
42
|
+
if (result.status === 'rejected') {
|
|
43
|
+
logger.error(`Failed to fetch OpenAPI schema for application with id ${bold(applicationId)}: ${result.reason}`)
|
|
44
|
+
} else {
|
|
45
|
+
logger.info(`Successfully fetched OpenAPI schema for application with id ${bold(applicationId)}`)
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
}
|
package/lib/errors.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import createError from '@fastify/error'
|
|
2
|
+
|
|
3
|
+
export const ERROR_PREFIX = 'PLT_GATEWAY'
|
|
4
|
+
|
|
5
|
+
export const FastifyInstanceIsAlreadyListeningError = createError(
|
|
6
|
+
`${ERROR_PREFIX}_FASTIFY_INSTANCE_IS_ALREADY_LISTENING`,
|
|
7
|
+
'Fastify instance is already listening. Cannot call "addGatewayOnRouteHook"!'
|
|
8
|
+
)
|
|
9
|
+
export const FailedToFetchOpenAPISchemaError = createError(
|
|
10
|
+
`${ERROR_PREFIX}_FAILED_TO_FETCH_OPENAPI_SCHEMA`,
|
|
11
|
+
'Failed to fetch OpenAPI schema from %s'
|
|
12
|
+
)
|
|
13
|
+
export const ValidationErrors = createError(`${ERROR_PREFIX}_VALIDATION_ERRORS`, 'Validation errors: %s')
|
|
14
|
+
export const PathAlreadyExistsError = createError(`${ERROR_PREFIX}_PATH_ALREADY_EXISTS`, 'Path "%s" already exists')
|
|
15
|
+
export const CouldNotReadOpenAPIConfigError = createError(
|
|
16
|
+
`${ERROR_PREFIX}_COULD_NOT_READ_OPENAPI_CONFIG`,
|
|
17
|
+
'Could not read openapi config for "%s" application'
|
|
18
|
+
)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import fp from 'fastify-plugin'
|
|
2
|
+
import rfdc from 'rfdc'
|
|
3
|
+
import { FastifyInstanceIsAlreadyListeningError } from './errors.js'
|
|
4
|
+
|
|
5
|
+
const deepClone = rfdc()
|
|
6
|
+
|
|
7
|
+
async function gatewayHookPlugin (app) {
|
|
8
|
+
const onRoutesHooks = {}
|
|
9
|
+
|
|
10
|
+
app.addHook('onRoute', routeOptions => {
|
|
11
|
+
if (routeOptions.schema) {
|
|
12
|
+
routeOptions.schema = deepClone(routeOptions.schema)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const method = routeOptions.method
|
|
16
|
+
const openApiPath = routeOptions.config?.openApiPath
|
|
17
|
+
|
|
18
|
+
const onRouteHooks = onRoutesHooks[openApiPath]?.[method]
|
|
19
|
+
if (Array.isArray(onRouteHooks)) {
|
|
20
|
+
for (const onRouteHook of onRouteHooks) {
|
|
21
|
+
onRouteHook(routeOptions)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
let isApplicationReady = false
|
|
27
|
+
app.addHook('onReady', () => {
|
|
28
|
+
isApplicationReady = true
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
function addGatewayOnRouteHook (openApiPath, methods, hook) {
|
|
32
|
+
/* c8 ignore next 5 */
|
|
33
|
+
if (isApplicationReady) {
|
|
34
|
+
throw new FastifyInstanceIsAlreadyListeningError()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (onRoutesHooks[openApiPath] === undefined) {
|
|
38
|
+
onRoutesHooks[openApiPath] = {}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const routeHooks = onRoutesHooks[openApiPath]
|
|
42
|
+
|
|
43
|
+
for (let method of methods) {
|
|
44
|
+
method = method.toUpperCase()
|
|
45
|
+
|
|
46
|
+
if (routeHooks[method] === undefined) {
|
|
47
|
+
routeHooks[method] = []
|
|
48
|
+
}
|
|
49
|
+
routeHooks[method].push(hook)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
Object.defineProperty(app.platformatic, 'addGatewayOnRouteHook', {
|
|
54
|
+
value: addGatewayOnRouteHook,
|
|
55
|
+
writable: false,
|
|
56
|
+
configurable: false
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
Object.defineProperty(app.platformatic, 'addComposerOnRouteHook', {
|
|
60
|
+
value: addGatewayOnRouteHook,
|
|
61
|
+
writable: false,
|
|
62
|
+
configurable: false
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const gatewayHook = fp(gatewayHookPlugin)
|
package/lib/generator.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { Generator as ServiceGenerator } from '@platformatic/service'
|
|
2
|
+
|
|
3
|
+
export class Generator extends ServiceGenerator {
|
|
4
|
+
constructor (opts) {
|
|
5
|
+
super({
|
|
6
|
+
...opts,
|
|
7
|
+
module: '@platformatic/gateway'
|
|
8
|
+
})
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
getDefaultConfig () {
|
|
12
|
+
const defaultBaseConfig = super.getDefaultConfig()
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
...defaultBaseConfig,
|
|
16
|
+
plugin: false,
|
|
17
|
+
routes: false,
|
|
18
|
+
tests: false
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async _beforePrepare () {
|
|
23
|
+
if (this.config.isUpdating) {
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
await super._beforePrepare()
|
|
28
|
+
|
|
29
|
+
this.addEnvVars(
|
|
30
|
+
{
|
|
31
|
+
PLT_EXAMPLE_ORIGIN: 'http://127.0.0.1:3043'
|
|
32
|
+
},
|
|
33
|
+
{ overwrite: false, default: true }
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
this.config.dependencies = {
|
|
37
|
+
'@platformatic/gateway': `^${this.platformaticVersion}`
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async _afterPrepare () {
|
|
42
|
+
if (this.config.isUpdating) {
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
await super._afterPrepare()
|
|
47
|
+
const PLT_ENVIRONMENT_TEMPLATE = `
|
|
48
|
+
import { type FastifyInstance } from 'fastify'
|
|
49
|
+
import { PlatformaticApplication, PlatformaticGatewayConfig } from '@platformatic/gateway'
|
|
50
|
+
|
|
51
|
+
declare module 'fastify' {
|
|
52
|
+
interface FastifyInstance {
|
|
53
|
+
platformatic: PlatformaticApplication<PlatformaticGatewayConfig>
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
`
|
|
57
|
+
|
|
58
|
+
const README = `
|
|
59
|
+
# Platformatic Gateway API
|
|
60
|
+
|
|
61
|
+
This is a generated [Platformatic Gateway](https://docs.platformatic.dev/docs/gateway/overview) application.
|
|
62
|
+
|
|
63
|
+
## Requirements
|
|
64
|
+
|
|
65
|
+
Platformatic supports macOS, Linux and Windows ([WSL](https://docs.microsoft.com/windows/wsl/) recommended).
|
|
66
|
+
You'll need to have [Node.js](https://nodejs.org/) >= v18.8.0 or >= v20.6.0
|
|
67
|
+
|
|
68
|
+
## Setup
|
|
69
|
+
|
|
70
|
+
1. Install dependencies:
|
|
71
|
+
|
|
72
|
+
\`\`\`bash
|
|
73
|
+
npm install
|
|
74
|
+
\`\`\`
|
|
75
|
+
|
|
76
|
+
## Usage
|
|
77
|
+
|
|
78
|
+
Run the API with:
|
|
79
|
+
|
|
80
|
+
\`\`\`bash
|
|
81
|
+
npm start
|
|
82
|
+
\`\`\`
|
|
83
|
+
|
|
84
|
+
### Explore
|
|
85
|
+
- ⚡ The Platformatic Gateway server is running at http://localhost:3042/
|
|
86
|
+
- 📔 View the REST API's Swagger documentation at http://localhost:3042/documentation/
|
|
87
|
+
`
|
|
88
|
+
|
|
89
|
+
this.addFile({ path: '', file: 'plt-env.d.ts', contents: PLT_ENVIRONMENT_TEMPLATE })
|
|
90
|
+
this.addFile({ path: '', file: 'README.md', contents: README })
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async _getConfigFileContents () {
|
|
94
|
+
const config = await super._getConfigFileContents()
|
|
95
|
+
delete config.service
|
|
96
|
+
config.$schema = `https://schemas.platformatic.dev/@platformatic/gateway/${this.platformaticVersion}.json`
|
|
97
|
+
|
|
98
|
+
config.gateway = {
|
|
99
|
+
applications: [
|
|
100
|
+
{
|
|
101
|
+
id: 'example',
|
|
102
|
+
origin: `{${this.getEnvVarName('PLT_EXAMPLE_ORIGIN')}}`,
|
|
103
|
+
openapi: {
|
|
104
|
+
url: '/documentation/json'
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
],
|
|
108
|
+
refreshTimeout: 1000
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (this.runtime !== null) {
|
|
112
|
+
config.gateway.applications = this.runtime.applications
|
|
113
|
+
.filter(applicationMeta => applicationMeta.application.module !== '@platformatic/gateway')
|
|
114
|
+
.map(applicationMeta => {
|
|
115
|
+
return {
|
|
116
|
+
id: applicationMeta.name,
|
|
117
|
+
openapi: {
|
|
118
|
+
url: '/documentation/json',
|
|
119
|
+
prefix: `/${applicationMeta.name}`
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return config
|
|
126
|
+
}
|
|
127
|
+
}
|