@platformatic/composer 2.72.0 → 3.0.0-alpha.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.
- package/config.d.ts +1 -19
- package/eslint.config.js +11 -2
- package/index.d.ts +58 -17
- package/index.js +29 -212
- package/lib/application.js +186 -0
- package/lib/commands/index.js +15 -0
- package/lib/commands/openapi-fetch-schemas.js +47 -0
- package/lib/composer-hook.js +9 -9
- package/lib/errors.js +15 -10
- package/lib/generator.js +158 -0
- package/lib/graphql-fetch.js +44 -46
- package/lib/graphql-generator.js +12 -10
- package/lib/graphql.js +13 -9
- package/lib/{proxy/not-host-constraints.js → not-host-constraints.js} +3 -5
- package/lib/openapi-composer.js +39 -41
- package/lib/openapi-config-schema.js +26 -30
- package/lib/openapi-generator.js +115 -112
- package/lib/openapi-load-config.js +14 -14
- package/lib/openapi-modifier.js +12 -21
- package/lib/openapi-scalar.js +3 -5
- package/lib/proxy.js +21 -34
- package/lib/{root-endpoint/index.js → root.js} +12 -12
- package/lib/schema.js +34 -24
- package/lib/stackable.js +29 -39
- package/lib/upgrade.js +6 -8
- package/lib/utils.js +5 -16
- package/lib/versions/2.0.0.js +4 -6
- package/package.json +14 -18
- package/schema.json +2 -73
- package/.c8rc +0 -6
- package/composer.mjs +0 -54
- package/help/create.txt +0 -11
- package/help/help.txt +0 -7
- package/help/openapi schemas fetch.txt +0 -9
- package/help/start.txt +0 -54
- package/index.test-d.ts +0 -23
- package/lib/create.mjs +0 -84
- package/lib/generator/README.md +0 -30
- package/lib/generator/composer-generator.d.ts +0 -11
- package/lib/generator/composer-generator.js +0 -128
- package/lib/metrics.js +0 -12
- package/lib/openapi-fetch-schemas.mjs +0 -61
- /package/{lib/root-endpoint/public → public}/images/dark_mode.svg +0 -0
- /package/{lib/root-endpoint/public → public}/images/ellipse.svg +0 -0
- /package/{lib/root-endpoint/public → public}/images/external-link.svg +0 -0
- /package/{lib/root-endpoint/public → public}/images/favicon.ico +0 -0
- /package/{lib/root-endpoint/public → public}/images/graphiql.svg +0 -0
- /package/{lib/root-endpoint/public → public}/images/graphql.svg +0 -0
- /package/{lib/root-endpoint/public → public}/images/light_mode.svg +0 -0
- /package/{lib/root-endpoint/public → public}/images/openapi.svg +0 -0
- /package/{lib/root-endpoint/public → public}/images/platformatic-logo-dark.svg +0 -0
- /package/{lib/root-endpoint/public → public}/images/platformatic-logo-light.svg +0 -0
- /package/{lib/root-endpoint/public → public}/images/reverse-proxy.svg +0 -0
- /package/{lib/root-endpoint/public → public}/images/triangle_dark.svg +0 -0
- /package/{lib/root-endpoint/public → public}/images/triangle_light.svg +0 -0
- /package/{lib/root-endpoint/public → public}/index.html +0 -0
- /package/{lib/root-endpoint/public → public}/index.njk +0 -0
- /package/{lib/root-endpoint/public → public}/main.css +0 -0
package/config.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* and run json-schema-to-typescript to regenerate this file.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
export interface
|
|
8
|
+
export interface PlatformaticComposerConfig {
|
|
9
9
|
basePath?: string;
|
|
10
10
|
server?: {
|
|
11
11
|
hostname?: string;
|
|
@@ -367,24 +367,6 @@ export interface PlatformaticComposer {
|
|
|
367
367
|
addEmptySchema?: boolean;
|
|
368
368
|
refreshTimeout?: number;
|
|
369
369
|
};
|
|
370
|
-
metrics?:
|
|
371
|
-
| boolean
|
|
372
|
-
| {
|
|
373
|
-
port?: number | string;
|
|
374
|
-
hostname?: string;
|
|
375
|
-
endpoint?: string;
|
|
376
|
-
server?: "own" | "parent" | "hide";
|
|
377
|
-
defaultMetrics?: {
|
|
378
|
-
enabled: boolean;
|
|
379
|
-
};
|
|
380
|
-
auth?: {
|
|
381
|
-
username: string;
|
|
382
|
-
password: string;
|
|
383
|
-
};
|
|
384
|
-
labels?: {
|
|
385
|
-
[k: string]: string;
|
|
386
|
-
};
|
|
387
|
-
};
|
|
388
370
|
types?: {
|
|
389
371
|
autogenerate?: boolean;
|
|
390
372
|
/**
|
package/eslint.config.js
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
import neostandard from 'neostandard'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
export default neostandard({
|
|
4
|
+
ts: true,
|
|
5
|
+
ignores: [
|
|
6
|
+
...neostandard.resolveIgnoresFromGitignore(),
|
|
7
|
+
'test/tmp/**/*',
|
|
8
|
+
'test/fixtures/*/dist/**/*',
|
|
9
|
+
'**/dist/*',
|
|
10
|
+
'tmp/**/*'
|
|
11
|
+
]
|
|
12
|
+
})
|
package/index.d.ts
CHANGED
|
@@ -1,17 +1,58 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
1
|
+
import { BaseStackable } from '@platformatic/basic'
|
|
2
|
+
import { BaseGenerator } from '@platformatic/generators'
|
|
3
|
+
import {
|
|
4
|
+
Configuration,
|
|
5
|
+
ConfigurationOptions,
|
|
6
|
+
ServerInstance as ServiceInstance,
|
|
7
|
+
ServiceStackable
|
|
8
|
+
} from '@platformatic/service'
|
|
9
|
+
import { JSONSchemaType } from 'ajv'
|
|
10
|
+
import { FastifyError, FastifyInstance } from 'fastify'
|
|
11
|
+
import { PlatformaticComposerConfig } from './config'
|
|
12
|
+
|
|
13
|
+
export { PlatformaticApplication } from '@platformatic/service'
|
|
14
|
+
export { PlatformaticComposerConfig } from './config'
|
|
15
|
+
|
|
16
|
+
export type ComposerStackable = ServiceStackable<PlatformaticComposerConfig>
|
|
17
|
+
|
|
18
|
+
export type ServerInstance = ServiceInstance<PlatformaticComposerConfig>
|
|
19
|
+
|
|
20
|
+
type ComposerConfiguration = Configuration<PlatformaticComposerConfig>
|
|
21
|
+
|
|
22
|
+
export declare function loadConfiguration (
|
|
23
|
+
root: string | PlatformaticServiceConfig,
|
|
24
|
+
source?: string | PlatformaticServiceConfig,
|
|
25
|
+
context?: ConfigurationOptions
|
|
26
|
+
): Promise<ComposerConfiguration>
|
|
27
|
+
|
|
28
|
+
export function create (
|
|
29
|
+
root: string,
|
|
30
|
+
source?: string | PlatformaticComposerConfig,
|
|
31
|
+
context?: ConfigurationOptions
|
|
32
|
+
): Promise<ComposerStackable>
|
|
33
|
+
|
|
34
|
+
export declare function platformaticComposer (app: FastifyInstance, stackable: BaseStackable): 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<PlatformaticComposerConfig>
|
|
41
|
+
|
|
42
|
+
export declare const schemaComponents: {
|
|
43
|
+
openApiService: JSONSchemaType<object>
|
|
44
|
+
entityResolver: JSONSchemaType<object>
|
|
45
|
+
entities: JSONSchemaType<object>
|
|
46
|
+
graphqlService: JSONSchemaType<object>
|
|
47
|
+
graphqlComposerOptions: JSONSchemaType<object>
|
|
48
|
+
composer: 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
CHANGED
|
@@ -1,217 +1,34 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const errors = require('./lib/errors')
|
|
19
|
-
const upgrade = require('./lib/upgrade')
|
|
20
|
-
|
|
21
|
-
const EXPERIMENTAL_GRAPHQL_COMPOSER_FEATURE_MESSAGE = 'graphql composer is an experimental feature'
|
|
22
|
-
|
|
23
|
-
async function platformaticComposer (app, opts) {
|
|
24
|
-
const configManager = app.platformatic.configManager
|
|
25
|
-
const config = configManager.current
|
|
26
|
-
let hasGraphqlServices, hasOpenapiServices
|
|
27
|
-
|
|
28
|
-
// When no services are specified, get the list from the runtime.
|
|
29
|
-
await ensureServices(opts.context?.stackable?.serviceId, config)
|
|
30
|
-
|
|
31
|
-
const { services } = configManager.current.composer
|
|
32
|
-
|
|
33
|
-
for (const service of services) {
|
|
34
|
-
if (!service.origin) {
|
|
35
|
-
service.origin = `http://${service.id}.plt.local`
|
|
36
|
-
}
|
|
37
|
-
if (service.openapi && !hasOpenapiServices) {
|
|
38
|
-
hasOpenapiServices = true
|
|
39
|
-
}
|
|
40
|
-
if (service.graphql && !hasGraphqlServices) {
|
|
41
|
-
hasGraphqlServices = true
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
await app.register(composerHook)
|
|
46
|
-
|
|
47
|
-
let generatedComposedOpenAPI = null
|
|
48
|
-
if (hasOpenapiServices) {
|
|
49
|
-
generatedComposedOpenAPI = await openApiGenerator(app, config.composer)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (isKeyEnabled('healthCheck', config.server)) {
|
|
53
|
-
if (typeof config.server.healthCheck !== 'object') {
|
|
54
|
-
config.server.healthCheck = {}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const stackable = opts.context.stackable
|
|
58
|
-
config.server.healthCheck.fn = stackable.isHealthy.bind(stackable)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
app.register(serviceProxy, { ...config.composer, context: opts.context })
|
|
62
|
-
await app.register(platformaticService, { config: { ...config, openapi: false }, context: opts.context })
|
|
63
|
-
|
|
64
|
-
if (generatedComposedOpenAPI) {
|
|
65
|
-
await app.register(openApiComposer, { opts: config.composer, generated: generatedComposedOpenAPI })
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (hasGraphqlServices) {
|
|
69
|
-
app.log.warn(EXPERIMENTAL_GRAPHQL_COMPOSER_FEATURE_MESSAGE)
|
|
70
|
-
app.register(graphql, config.composer)
|
|
71
|
-
await app.register(graphqlGenerator, config.composer)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (!app.hasRoute({ url: '/', method: 'GET' }) && !app.hasRoute({ url: '/*', method: 'GET' })) {
|
|
75
|
-
await app.register(require('./lib/root-endpoint'), config)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (!opts.context?.isProduction) {
|
|
79
|
-
await watchServices(app, config)
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
platformaticComposer[Symbol.for('skip-override')] = true
|
|
84
|
-
platformaticComposer.schema = schema
|
|
85
|
-
platformaticComposer.configType = 'composer'
|
|
86
|
-
platformaticComposer.isPLTService = true
|
|
87
|
-
platformaticComposer.configManagerConfig = {
|
|
88
|
-
version: require('./package.json').version,
|
|
89
|
-
schema,
|
|
90
|
-
allowToWatch: ['.env'],
|
|
91
|
-
schemaOptions: {
|
|
92
|
-
useDefaults: true,
|
|
93
|
-
coerceTypes: true,
|
|
94
|
-
allErrors: true,
|
|
95
|
-
strict: false
|
|
96
|
-
},
|
|
97
|
-
transformConfig: platformaticService.configManagerConfig.transformConfig,
|
|
98
|
-
upgrade
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// TODO review no need to be async
|
|
102
|
-
async function buildComposerServer (options) {
|
|
103
|
-
// TODO ConfigManager is not been used, it's attached to platformaticComposer, can be removed
|
|
104
|
-
return buildServer(options, platformaticComposer, ConfigManager)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
async function detectServicesUpdate ({ app, services, fetchOpenApiSchema, fetchGraphqlSubgraphs }) {
|
|
108
|
-
let changed
|
|
109
|
-
|
|
110
|
-
const graphqlServices = []
|
|
111
|
-
// assumes services here are fetchable
|
|
112
|
-
for (const service of services) {
|
|
113
|
-
const { id, origin, openapi, graphql } = service
|
|
114
|
-
|
|
115
|
-
if (openapi) {
|
|
116
|
-
const currentSchema = app.openApiSchemas.find(schema => schema.id === id)?.originSchema || null
|
|
117
|
-
|
|
118
|
-
let fetchedSchema = null
|
|
119
|
-
try {
|
|
120
|
-
fetchedSchema = await fetchOpenApiSchema({ origin, openapi })
|
|
121
|
-
} catch (err) {
|
|
122
|
-
app.log.error({ err }, 'failed to fetch schema (watch) for service ' + id)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (!changed && !deepEqual(fetchedSchema, currentSchema)) {
|
|
126
|
-
changed = true
|
|
127
|
-
// it stops at first schema difference since all the schemas will be updated on reload
|
|
128
|
-
break
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (graphql) {
|
|
133
|
-
graphqlServices.push(service)
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (!changed && graphqlServices.length > 0) {
|
|
138
|
-
const graphqlSupergraph = await fetchGraphqlSubgraphs(graphqlServices, app.graphqlComposerOptions, app)
|
|
139
|
-
if (!isSameGraphqlSchema(graphqlSupergraph, app.graphqlSupergraph)) {
|
|
140
|
-
changed = true
|
|
141
|
-
app.graphqlSupergraph = graphqlSupergraph
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return changed
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* poll services to detect changes, every `opts.composer.refreshTimeout`
|
|
150
|
-
* polling is disabled on refreshTimeout = 0
|
|
151
|
-
* or there are no network openapi nor graphql remote services (the services are from file or they don't have a schema/graph to fetch)
|
|
152
|
-
*/
|
|
153
|
-
async function watchServices (app, opts) {
|
|
154
|
-
const { services, refreshTimeout } = opts.composer
|
|
155
|
-
if (refreshTimeout < 1) {
|
|
156
|
-
return
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const watching = services.filter(isFetchable)
|
|
160
|
-
if (watching.length < 1) {
|
|
161
|
-
return
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (!globalThis[Symbol.for('plt.runtime.id')]) {
|
|
165
|
-
app.log.warn('Watching services is only supported when running within a Platformatic Runtime.')
|
|
166
|
-
return
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const { fetchOpenApiSchema } = await import('./lib/openapi-fetch-schemas.mjs')
|
|
170
|
-
|
|
171
|
-
app.log.info({ services: watching }, 'start watching services')
|
|
172
|
-
|
|
173
|
-
const timer = setInterval(async () => {
|
|
174
|
-
try {
|
|
175
|
-
if (await detectServicesUpdate({ app, services: watching, fetchOpenApiSchema, fetchGraphqlSubgraphs })) {
|
|
176
|
-
clearInterval(timer)
|
|
177
|
-
app.log.info('detected services changes, restarting ...')
|
|
178
|
-
|
|
179
|
-
globalThis[Symbol.for('plt.runtime.itc')].notify('changed')
|
|
180
|
-
}
|
|
181
|
-
} catch (error) {
|
|
182
|
-
app.log.error(
|
|
183
|
-
{
|
|
184
|
-
err: {
|
|
185
|
-
message: error.message,
|
|
186
|
-
stack: error.stack
|
|
187
|
-
}
|
|
188
|
-
},
|
|
189
|
-
'failed to get services info'
|
|
190
|
-
)
|
|
191
|
-
}
|
|
192
|
-
}, refreshTimeout).unref()
|
|
193
|
-
|
|
194
|
-
app.addHook('onClose', async () => {
|
|
195
|
-
clearInterval(timer)
|
|
1
|
+
import { resolve, validationOptions } from '@platformatic/basic'
|
|
2
|
+
import { transform } from '@platformatic/service'
|
|
3
|
+
import { kMetadata, loadConfiguration as utilsLoadConfiguration } from '@platformatic/utils'
|
|
4
|
+
import { schema } from './lib/schema.js'
|
|
5
|
+
import { ComposerStackable } from './lib/stackable.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, 'composer')
|
|
10
|
+
|
|
11
|
+
return utilsLoadConfiguration(source, context?.schema ?? schema, {
|
|
12
|
+
validationOptions,
|
|
13
|
+
transform,
|
|
14
|
+
upgrade,
|
|
15
|
+
replaceEnv: true,
|
|
16
|
+
root,
|
|
17
|
+
...context
|
|
196
18
|
})
|
|
197
19
|
}
|
|
198
20
|
|
|
199
|
-
async function
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
constraints: {
|
|
203
|
-
notHost: notHostConstraints
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
return buildStackable(options, platformaticComposer, ComposerStackable)
|
|
21
|
+
export async function create (configOrRoot, sourceOrConfig, context) {
|
|
22
|
+
const config = await loadConfiguration(configOrRoot, sourceOrConfig, context)
|
|
23
|
+
return new ComposerStackable(config[kMetadata].root, config, context)
|
|
208
24
|
}
|
|
209
25
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
26
|
+
export const skipTelemetryHooks = true
|
|
27
|
+
|
|
28
|
+
export { platformaticComposer } from './lib/application.js'
|
|
29
|
+
export * from './lib/commands/index.js'
|
|
30
|
+
export * from './lib/errors.js'
|
|
31
|
+
export * as errors from './lib/errors.js'
|
|
32
|
+
export { Generator } from './lib/generator.js'
|
|
33
|
+
export { packageJson, schema, schemaComponents, version } from './lib/schema.js'
|
|
34
|
+
export { ComposerStackable } from './lib/stackable.js'
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { platformaticService } from '@platformatic/service'
|
|
2
|
+
import { isKeyEnabled } from '@platformatic/utils'
|
|
3
|
+
import deepEqual from 'fast-deep-equal'
|
|
4
|
+
import { fetchOpenApiSchema } from './commands/openapi-fetch-schemas.js'
|
|
5
|
+
import { composerHook } from './composer-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 { openApiComposer, 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_COMPOSER_FEATURE_MESSAGE = 'graphql composer is an experimental feature'
|
|
15
|
+
|
|
16
|
+
async function detectServicesUpdate ({ app, services, fetchOpenApiSchema, fetchGraphqlSubgraphs }) {
|
|
17
|
+
let changed
|
|
18
|
+
|
|
19
|
+
const graphqlServices = []
|
|
20
|
+
// assumes services here are fetchable
|
|
21
|
+
for (const service of services) {
|
|
22
|
+
const { id, origin, openapi, graphql } = service
|
|
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 service ' + 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
|
+
graphqlServices.push(service)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!changed && graphqlServices.length > 0) {
|
|
47
|
+
const graphqlSupergraph = await fetchGraphqlSubgraphs(graphqlServices, app.graphqlComposerOptions, 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 services to detect changes, every `opts.composer.refreshTimeout`
|
|
59
|
+
* polling is disabled on refreshTimeout = 0
|
|
60
|
+
* or there are no network openapi nor graphql remote services (the services are from file or they don't have a schema/graph to fetch)
|
|
61
|
+
*/
|
|
62
|
+
async function watchServices (app, { config, stackable }) {
|
|
63
|
+
const { services, refreshTimeout } = config.composer
|
|
64
|
+
if (refreshTimeout < 1) {
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const watching = services.filter(isFetchable)
|
|
69
|
+
if (watching.length < 1) {
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!globalThis[Symbol.for('plt.runtime.id')]) {
|
|
74
|
+
app.log.warn('Watching services is only supported when running within a Platformatic Runtime.')
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
stackable.emit('watch:start')
|
|
79
|
+
app.log.info({ services: watching }, 'start watching services')
|
|
80
|
+
|
|
81
|
+
const timer = setInterval(async () => {
|
|
82
|
+
try {
|
|
83
|
+
if (await detectServicesUpdate({ app, services: watching, fetchOpenApiSchema, fetchGraphqlSubgraphs })) {
|
|
84
|
+
clearInterval(timer)
|
|
85
|
+
app.log.info('detected services 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 services info'
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
}, refreshTimeout).unref()
|
|
101
|
+
|
|
102
|
+
app.addHook('onClose', async () => {
|
|
103
|
+
clearInterval(timer)
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function ensureServices (composerId, config) {
|
|
108
|
+
if (config.composer?.services?.length) {
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
composerId ??= globalThis.platformatic?.serviceId
|
|
113
|
+
config.composer ??= {}
|
|
114
|
+
config.composer.services ??= []
|
|
115
|
+
|
|
116
|
+
// When no services are defined, all services are exposed in the composer
|
|
117
|
+
const services = await globalThis[kITC]?.send('listServices')
|
|
118
|
+
|
|
119
|
+
if (services) {
|
|
120
|
+
config.composer.services = services
|
|
121
|
+
.filter(id => id !== composerId) // Remove ourself
|
|
122
|
+
.map(id => ({ id, proxy: { prefix: `/${id}` } }))
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function platformaticComposer (app, stackable) {
|
|
127
|
+
const config = await stackable.getConfig()
|
|
128
|
+
let hasGraphqlServices, hasOpenapiServices
|
|
129
|
+
|
|
130
|
+
// When no services are specified, get the list from the runtime.
|
|
131
|
+
await ensureServices(stackable.serviceId, config)
|
|
132
|
+
|
|
133
|
+
const { services } = config.composer
|
|
134
|
+
|
|
135
|
+
for (const service of services) {
|
|
136
|
+
if (!service.origin) {
|
|
137
|
+
service.origin = `http://${service.id}.plt.local`
|
|
138
|
+
}
|
|
139
|
+
if (service.openapi && !hasOpenapiServices) {
|
|
140
|
+
hasOpenapiServices = true
|
|
141
|
+
}
|
|
142
|
+
if (service.graphql && !hasGraphqlServices) {
|
|
143
|
+
hasGraphqlServices = true
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
await app.register(composerHook)
|
|
148
|
+
|
|
149
|
+
let generatedComposedOpenAPI = null
|
|
150
|
+
if (hasOpenapiServices) {
|
|
151
|
+
generatedComposedOpenAPI = await openApiGenerator(app, config.composer)
|
|
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 = stackable.isHealthy.bind(stackable)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
await app.register(proxy, { ...config.composer, stackable, context: stackable.context })
|
|
163
|
+
|
|
164
|
+
await platformaticService(app, stackable)
|
|
165
|
+
|
|
166
|
+
if (generatedComposedOpenAPI) {
|
|
167
|
+
await app.register(openApiComposer, { opts: config.composer, generated: generatedComposedOpenAPI })
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (hasGraphqlServices) {
|
|
171
|
+
app.log.warn(EXPERIMENTAL_GRAPHQL_COMPOSER_FEATURE_MESSAGE)
|
|
172
|
+
app.register(graphql, config.composer)
|
|
173
|
+
await app.register(graphqlGenerator, config.composer)
|
|
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 (!stackable.context?.isProduction) {
|
|
182
|
+
await watchServices(app, { config, stackable, context: stackable.context })
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
platformaticComposer[Symbol.for('skip-override')] = true
|
|
@@ -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 services'
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { loadConfiguration } from '@platformatic/utils'
|
|
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 { prefixWithSlash } from '../utils.js'
|
|
7
|
+
|
|
8
|
+
export async function fetchOpenApiSchema (service) {
|
|
9
|
+
const { origin, openapi } = service
|
|
10
|
+
|
|
11
|
+
const openApiUrl = origin + prefixWithSlash(openapi.url)
|
|
12
|
+
const { statusCode, body } = await request(openApiUrl)
|
|
13
|
+
|
|
14
|
+
if (statusCode !== 200 && statusCode !== 201) {
|
|
15
|
+
throw new FailedToFetchOpenAPISchemaError(openApiUrl)
|
|
16
|
+
}
|
|
17
|
+
const schema = await body.json()
|
|
18
|
+
|
|
19
|
+
if (openapi.file !== undefined) {
|
|
20
|
+
await writeFile(openapi.file, JSON.stringify(schema, null, 2))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return schema
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function fetchOpenApiSchemas (logger, configFile, _args, { colorette }) {
|
|
27
|
+
const { bold } = colorette
|
|
28
|
+
const config = await loadConfiguration(configFile, schema)
|
|
29
|
+
const { services } = config.composer
|
|
30
|
+
|
|
31
|
+
const servicesWithValidOpenApi = services.filter(({ openapi }) => openapi && openapi.url && openapi.file)
|
|
32
|
+
|
|
33
|
+
const fetchOpenApiRequests = servicesWithValidOpenApi.map(service => fetchOpenApiSchema(service))
|
|
34
|
+
|
|
35
|
+
const fetchOpenApiResults = await Promise.allSettled(fetchOpenApiRequests)
|
|
36
|
+
|
|
37
|
+
logger.info('Fetching schemas for all services.')
|
|
38
|
+
|
|
39
|
+
fetchOpenApiResults.forEach((result, index) => {
|
|
40
|
+
const serviceId = servicesWithValidOpenApi[index].id
|
|
41
|
+
if (result.status === 'rejected') {
|
|
42
|
+
logger.error(`Failed to fetch OpenAPI schema for service with id ${bold(serviceId)}: ${result.reason}`)
|
|
43
|
+
} else {
|
|
44
|
+
logger.info(`Successfully fetched OpenAPI schema for service with id ${bold(serviceId)}`)
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
}
|
package/lib/composer-hook.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
import fp from 'fastify-plugin'
|
|
2
|
+
import rfdc from 'rfdc'
|
|
3
|
+
import { FastifyInstanceIsAlreadyListeningError } from './errors.js'
|
|
2
4
|
|
|
3
|
-
const deepClone =
|
|
4
|
-
const fp = require('fastify-plugin')
|
|
5
|
-
const errors = require('./errors')
|
|
5
|
+
const deepClone = rfdc()
|
|
6
6
|
|
|
7
|
-
async function
|
|
7
|
+
async function composerHookPlugin (app) {
|
|
8
8
|
const onRoutesHooks = {}
|
|
9
9
|
|
|
10
|
-
app.addHook('onRoute',
|
|
10
|
+
app.addHook('onRoute', routeOptions => {
|
|
11
11
|
if (routeOptions.schema) {
|
|
12
12
|
routeOptions.schema = deepClone(routeOptions.schema)
|
|
13
13
|
}
|
|
@@ -31,7 +31,7 @@ async function composeOpenAPI (app) {
|
|
|
31
31
|
function addComposerOnRouteHook (openApiPath, methods, hook) {
|
|
32
32
|
/* c8 ignore next 5 */
|
|
33
33
|
if (isApplicationReady) {
|
|
34
|
-
throw new
|
|
34
|
+
throw new FastifyInstanceIsAlreadyListeningError()
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
if (onRoutesHooks[openApiPath] === undefined) {
|
|
@@ -53,8 +53,8 @@ async function composeOpenAPI (app) {
|
|
|
53
53
|
Object.defineProperty(app.platformatic, 'addComposerOnRouteHook', {
|
|
54
54
|
value: addComposerOnRouteHook,
|
|
55
55
|
writable: false,
|
|
56
|
-
configurable: false
|
|
56
|
+
configurable: false
|
|
57
57
|
})
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
export const composerHook = fp(composerHookPlugin)
|