@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
package/lib/proxy.js DELETED
@@ -1,265 +0,0 @@
1
- import httpProxy from '@fastify/http-proxy'
2
- import { ensureLoggableError, loadModule } from '@platformatic/foundation'
3
- import fp from 'fastify-plugin'
4
- import { createRequire } from 'node:module'
5
- import { workerData } from 'node:worker_threads'
6
- import { getGlobalDispatcher } from 'undici'
7
- import { initMetrics } from './metrics.js'
8
-
9
- const kITC = Symbol.for('plt.runtime.itc')
10
- const kProxyRoute = Symbol('plt.composer.proxy.route')
11
-
12
- const urlPattern = /^https?:\/\//
13
-
14
- async function resolveServiceProxyParameters (service) {
15
- // Get meta information from the service, if any, to eventually hook up to a TCP port
16
- const meta = (await globalThis[kITC]?.send('getServiceMeta', service.id))?.composer ?? { prefix: service.id }
17
-
18
- // If no prefix could be found, assume the service id
19
- let prefix = (service.proxy?.prefix ?? meta.prefix ?? service.id).replace(/(\/$)/g, '')
20
- let rewritePrefix = ''
21
- let internalRewriteLocationHeader = true
22
-
23
- if (meta.wantsAbsoluteUrls) {
24
- const basePath = workerData.config.basePath
25
-
26
- // Strip the runtime basepath from the prefix when it comes from the service meta
27
- if (basePath && !service.proxy?.prefix && prefix.startsWith(basePath)) {
28
- prefix = prefix.substring(basePath.length)
29
- }
30
-
31
- // The rewritePrefix purposely ignores service.proxy?.prefix to let
32
- // the service always being able to configure their value
33
- rewritePrefix = meta.prefix ?? service.id
34
- internalRewriteLocationHeader = false
35
- }
36
-
37
- if (service.proxy?.ws?.hooks) {
38
- const hooks = await loadModule(createRequire(import.meta.filename), service.proxy.ws.hooks.path)
39
- service.proxy.ws.hooks = hooks
40
- }
41
-
42
- return {
43
- origin: service.origin,
44
- url: meta.url,
45
- prefix,
46
- rewritePrefix,
47
- internalRewriteLocationHeader,
48
- needsRootTrailingSlash: meta.needsRootTrailingSlash,
49
- needsRefererBasedRedirect: meta.needsRefererBasedRedirect,
50
- upstream: service.proxy?.upstream,
51
- ws: service.proxy?.ws
52
- }
53
- }
54
-
55
- let metrics
56
-
57
- async function proxyPlugin (app, opts) {
58
- const meta = { proxies: {} }
59
- const hostnameLessProxies = []
60
-
61
- for (const service of opts.services) {
62
- if (!service.proxy) {
63
- // When a service defines no expose config at all
64
- // we assume a proxy exposed with a prefix equals to its id or meta.prefix
65
- if (service.proxy === false || service.openapi || service.graphql) {
66
- continue
67
- }
68
- }
69
-
70
- const parameters = await resolveServiceProxyParameters(service)
71
- const {
72
- prefix,
73
- origin,
74
- url,
75
- rewritePrefix,
76
- internalRewriteLocationHeader,
77
- needsRootTrailingSlash,
78
- needsRefererBasedRedirect,
79
- ws
80
- } = parameters
81
- meta.proxies[service.id] = parameters
82
-
83
- const basePath = `/${prefix ?? ''}`.replaceAll(/\/+/g, '/').replace(/\/$/, '')
84
- const dispatcher = getGlobalDispatcher()
85
-
86
- let preRewrite = null
87
-
88
- if (needsRootTrailingSlash) {
89
- preRewrite = function preRewrite (url) {
90
- if (url === basePath) {
91
- url += '/'
92
- }
93
-
94
- return url
95
- }
96
- }
97
-
98
- /*
99
- Some frontends, like Astro (https://github.com/withastro/astro/issues/11445)
100
- generate invalid paths in development mode which ignore the basePath.
101
- In that case we try to properly redirect the browser by trying to understand the prefix
102
- from the Referer header.
103
- */
104
- if (needsRefererBasedRedirect) {
105
- app.addHook('preHandler', function refererBasedRedirectPreHandler (req, reply, done) {
106
- // If the URL is already targeted to the service, do nothing
107
- if (req.url.startsWith(basePath)) {
108
- done()
109
- return
110
- }
111
-
112
- // Use the referer to understand the desired intent
113
- const referer = req.headers.referer
114
-
115
- if (!referer) {
116
- done()
117
- return
118
- }
119
-
120
- const path = new URL(referer).pathname
121
-
122
- // If we have a match redirect
123
- if (path.startsWith(basePath)) {
124
- reply.redirect(`${basePath}${req.url}`, 308)
125
- }
126
-
127
- done()
128
- })
129
- }
130
-
131
- // Do not show proxied services in Swagger
132
- if (!service.openapi) {
133
- app.addHook('onRoute', routeOptions => {
134
- if (routeOptions.config?.[kProxyRoute] && routeOptions.url.startsWith(basePath)) {
135
- routeOptions.schema ??= {}
136
- routeOptions.schema.hide = true
137
- }
138
- })
139
- }
140
-
141
- const toReplace = url
142
- ? new RegExp(
143
- url
144
- .replace(/127\.0\.0\.1/, 'localhost')
145
- .replace(/\[::\]/, 'localhost')
146
- .replace('http://', 'https?://')
147
- )
148
- : null
149
-
150
- if (!metrics) {
151
- metrics = initMetrics(globalThis.platformatic?.prometheus)
152
- }
153
-
154
- const proxyOptions = {
155
- prefix,
156
- rewritePrefix,
157
- upstream: service.proxy?.upstream ?? origin,
158
- preRewrite,
159
-
160
- websocket: true,
161
- wsUpstream: ws?.upstream ?? url ?? origin,
162
- wsReconnect: ws?.reconnect,
163
- wsHooks: {
164
- onConnect: (...args) => {
165
- metrics?.activeWsConnections?.inc()
166
- ws?.hooks?.onConnect(...args)
167
- },
168
- onDisconnect: (...args) => {
169
- metrics?.activeWsConnections?.dec()
170
- ws?.hooks?.onDisconnect(...args)
171
- },
172
- onReconnect: ws?.hooks?.onReconnect,
173
- onPong: ws?.hooks?.onPong,
174
- onIncomingMessage: ws?.hooks?.onIncomingMessage,
175
- onOutgoingMessage: ws?.hooks?.onOutgoingMessage
176
- },
177
-
178
- undici: dispatcher,
179
- destroyAgent: false,
180
- config: {
181
- [kProxyRoute]: true
182
- },
183
-
184
- internalRewriteLocationHeader: false,
185
- replyOptions: {
186
- rewriteHeaders: headers => {
187
- let location = headers.location
188
- if (location) {
189
- if (toReplace) {
190
- location = location.replace(toReplace, '')
191
- }
192
- if (!urlPattern.test(location) && internalRewriteLocationHeader) {
193
- location = location.replace(rewritePrefix, prefix)
194
- }
195
- headers.location = location
196
- }
197
- return headers
198
- },
199
- rewriteRequestHeaders: (request, headers) => {
200
- const targetUrl = `${origin}${request.url}`
201
- const context = request.span?.context
202
- const { span, telemetryHeaders } = app.openTelemetry?.startHTTPSpanClient(
203
- targetUrl,
204
- request.method,
205
- context
206
- ) || { span: null, telemetryHeaders: {} }
207
- // We need to store the span in a different object
208
- // to correctly close it in the onResponse hook
209
- // Note that we have 2 spans:
210
- // - request.span: the span of the request to the proxy
211
- // - request.proxedCallSpan: the span of the request to the proxied service
212
- request.proxedCallSpan = span
213
-
214
- headers = {
215
- ...headers,
216
- ...telemetryHeaders,
217
- 'x-forwarded-for': request.ip,
218
- 'x-forwarded-host': request.host,
219
- 'x-forwarded-proto': request.protocol
220
- }
221
-
222
- request.log.trace({ headers }, 'rewritten headers before proxying')
223
-
224
- return headers
225
- },
226
- onResponse: (_, reply, res) => {
227
- app.openTelemetry?.endHTTPSpanClient(reply.request.proxedCallSpan, {
228
- statusCode: reply.statusCode,
229
- headers: res.headers
230
- })
231
- reply.send(res.stream)
232
- },
233
- onError: (reply, { error }) => {
234
- app.log.error({ error: ensureLoggableError(error) }, 'Error while proxying request to another service')
235
- return reply.send(error)
236
- }
237
- }
238
- }
239
-
240
- hostnameLessProxies.push(proxyOptions)
241
-
242
- const host = service.proxy?.hostname
243
-
244
- if (host) {
245
- await app.register(httpProxy, {
246
- ...proxyOptions,
247
- prefix: '/',
248
- constraints: { host }
249
- })
250
- }
251
- }
252
-
253
- const hostnames = opts.services.map(s => s.proxy?.hostname).filter(Boolean)
254
- for (const options of hostnameLessProxies) {
255
- if (hostnames.length > 0) {
256
- options.constraints = { notHost: hostnames }
257
- }
258
-
259
- await app.register(httpProxy, options)
260
- }
261
-
262
- opts.stackable?.registerMeta(meta)
263
- }
264
-
265
- export const proxy = fp(proxyPlugin)
package/lib/root.js DELETED
@@ -1,75 +0,0 @@
1
- import fastifyStatic from '@fastify/static'
2
- import fastifyView from '@fastify/view'
3
- import userAgentParser from 'my-ua-parser'
4
- import { join } from 'node:path'
5
- import nunjucks from 'nunjucks'
6
-
7
- export default function root (app) {
8
- app.register(fastifyStatic, {
9
- root: join(import.meta.dirname, '../public')
10
- })
11
- app.register(fastifyView, {
12
- engine: {
13
- nunjucks
14
- },
15
- root: join(import.meta.dirname, '../public')
16
- })
17
- // root endpoint
18
- app.route({
19
- method: 'GET',
20
- path: '/',
21
- schema: { hide: true },
22
- handler: async (req, reply) => {
23
- const uaString = req.headers['user-agent']
24
- let hasOpenAPIServices = false
25
- let hasGraphQLServices = false
26
- if (uaString) {
27
- const parsed = userAgentParser(uaString)
28
- if (parsed.browser.name !== undefined) {
29
- const serviceTypes = {
30
- proxy: {
31
- title: 'Reverse Proxy',
32
- icon: './images/reverse-proxy.svg',
33
- services: []
34
- },
35
- openapi: {
36
- title: 'OpenAPI',
37
- icon: './images/openapi.svg',
38
- services: []
39
- },
40
- graphql: {
41
- title: 'GraphQL',
42
- icon: './images/graphql.svg',
43
- services: []
44
- }
45
- }
46
-
47
- app.platformatic.config.composer.services.forEach(s => {
48
- if (s.openapi) {
49
- hasOpenAPIServices = true
50
- serviceTypes.openapi.services.push(s)
51
- }
52
- if (s.graphql) {
53
- hasGraphQLServices = true
54
- serviceTypes.graphql.services.push(s)
55
- }
56
- if (s.proxy) {
57
- serviceTypes.proxy.services.push({
58
- ...s,
59
- externalLink: `${s.proxy.prefix}/`
60
- })
61
- }
62
- })
63
-
64
- return reply.view('index.njk', {
65
- hasGraphQLServices,
66
- hasOpenAPIServices,
67
- services: serviceTypes
68
- })
69
- }
70
- }
71
- // Load services
72
- return { message: 'Welcome to Platformatic! Please visit https://docs.platformatic.dev' }
73
- }
74
- })
75
- }
package/lib/schema.js DELETED
@@ -1,258 +0,0 @@
1
- #! /usr/bin/env node
2
-
3
- import { schemaComponents as basicSchemaComponents } from '@platformatic/basic'
4
- import {
5
- fastifyServer as server,
6
- schemaComponents as utilsSchemaComponents,
7
- watch,
8
- wrappedRuntime
9
- } from '@platformatic/foundation'
10
- import { schemaComponents as serviceSchemaComponents } from '@platformatic/service'
11
- import { readFileSync } from 'node:fs'
12
- import { resolve } from 'node:path'
13
-
14
- const { $defs, graphqlBase, openApiBase, plugins } = serviceSchemaComponents
15
-
16
- export const packageJson = JSON.parse(readFileSync(resolve(import.meta.dirname, '../package.json'), 'utf8'))
17
- export const version = packageJson.version
18
-
19
- export const openApiService = {
20
- type: 'object',
21
- properties: {
22
- url: { type: 'string' },
23
- file: { type: 'string', resolvePath: true },
24
- prefix: { type: 'string' },
25
- config: { type: 'string', resolvePath: true }
26
- },
27
- anyOf: [{ required: ['url'] }, { required: ['file'] }],
28
- additionalProperties: false
29
- }
30
-
31
- export const entityResolver = {
32
- type: 'object',
33
- properties: {
34
- name: { type: 'string' },
35
- argsAdapter: {
36
- anyOf: [{ typeof: 'function' }, { type: 'string' }]
37
- },
38
- partialResults: {
39
- anyOf: [{ typeof: 'function' }, { type: 'string' }]
40
- }
41
- },
42
- required: ['name'],
43
- additionalProperties: false
44
- }
45
-
46
- export const entities = {
47
- type: 'object',
48
- patternProperties: {
49
- '^.*$': {
50
- type: 'object',
51
- properties: {
52
- pkey: { type: 'string' },
53
- resolver: entityResolver,
54
- fkeys: {
55
- type: 'array',
56
- items: {
57
- type: 'object',
58
- properties: {
59
- type: { type: 'string' },
60
- field: { type: 'string' },
61
- as: { type: 'string' },
62
- pkey: { type: 'string' },
63
- subgraph: { type: 'string' },
64
- resolver: entityResolver
65
- },
66
- required: ['type']
67
- }
68
- },
69
- many: {
70
- type: 'array',
71
- items: {
72
- type: 'object',
73
- properties: {
74
- type: { type: 'string' },
75
- fkey: { type: 'string' },
76
- as: { type: 'string' },
77
- pkey: { type: 'string' },
78
- subgraph: { type: 'string' },
79
- resolver: entityResolver
80
- },
81
- required: ['type', 'fkey', 'resolver']
82
- }
83
- }
84
- }
85
- }
86
- }
87
- }
88
-
89
- export const graphqlService = {
90
- anyOf: [
91
- { type: 'boolean' },
92
- {
93
- type: 'object',
94
- properties: {
95
- host: { type: 'string' },
96
- name: { type: 'string' },
97
- graphqlEndpoint: { type: 'string', default: '/graphql' },
98
- composeEndpoint: { type: 'string', default: '/.well-known/graphql-composition' },
99
- entities
100
- },
101
- additionalProperties: false
102
- }
103
- ]
104
- }
105
-
106
- export const graphqlComposerOptions = {
107
- type: 'object',
108
- properties: {
109
- ...graphqlBase.properties,
110
- // TODO support subscriptions, subscriptions: { type: 'boolean', default: false },
111
- onSubgraphError: { typeof: 'function' },
112
- defaultArgsAdapter: {
113
- oneOf: [{ typeof: 'function' }, { type: 'string' }]
114
- },
115
- entities,
116
- addEntitiesResolvers: { type: 'boolean', default: false }
117
- },
118
- additionalProperties: false
119
- }
120
-
121
- export const composer = {
122
- type: 'object',
123
- properties: {
124
- services: {
125
- type: 'array',
126
- items: {
127
- type: 'object',
128
- properties: {
129
- id: { type: 'string' },
130
- origin: { type: 'string' },
131
- openapi: openApiService,
132
- graphql: graphqlService,
133
- proxy: {
134
- anyOf: [
135
- { type: 'boolean', const: false },
136
- {
137
- type: 'object',
138
- properties: {
139
- upstream: { type: 'string' },
140
- prefix: { type: 'string' },
141
- hostname: { type: 'string' },
142
- ws: {
143
- type: 'object',
144
- properties: {
145
- upstream: { type: 'string' },
146
- reconnect: {
147
- type: 'object',
148
- properties: {
149
- pingInterval: { type: 'number' },
150
- maxReconnectionRetries: { type: 'number' },
151
- reconnectInterval: { type: 'number' },
152
- reconnectDecay: { type: 'number' },
153
- connectionTimeout: { type: 'number' },
154
- reconnectOnClose: { type: 'boolean' },
155
- logs: { type: 'boolean' }
156
- }
157
- },
158
- hooks: {
159
- type: 'object',
160
- properties: {
161
- path: { type: 'string' }
162
- },
163
- required: ['path'],
164
- additionalProperties: false
165
- }
166
- },
167
- required: [],
168
- additionalProperties: false
169
- }
170
- },
171
- required: [],
172
- additionalProperties: false
173
- }
174
- ]
175
- }
176
- },
177
- required: ['id'],
178
- additionalProperties: false
179
- }
180
- },
181
- openapi: openApiBase,
182
- graphql: graphqlComposerOptions,
183
- addEmptySchema: { type: 'boolean', default: false },
184
- refreshTimeout: { type: 'integer', minimum: 0, default: 1000 }
185
- },
186
- required: [],
187
- default: {},
188
- additionalProperties: false
189
- }
190
-
191
- export const types = {
192
- type: 'object',
193
- properties: {
194
- autogenerate: {
195
- type: 'boolean'
196
- },
197
- dir: {
198
- description: 'The path to the directory the types should be generated in.',
199
- type: 'string',
200
- default: 'types',
201
- resolvePath: true
202
- }
203
- },
204
- additionalProperties: false
205
- }
206
-
207
- export const schemaComponents = {
208
- openApiService,
209
- entityResolver,
210
- entities,
211
- graphqlService,
212
- graphqlComposerOptions,
213
- composer,
214
- types
215
- }
216
-
217
- export const schema = {
218
- $id: `https://schemas.platformatic.dev/@platformatic/composer/${packageJson.version}.json`,
219
- $schema: 'http://json-schema.org/draft-07/schema#',
220
- title: 'Platformatic Composer Config',
221
- type: 'object',
222
- properties: {
223
- basePath: {
224
- type: 'string'
225
- },
226
- server,
227
- composer,
228
- types,
229
- plugins,
230
- application: basicSchemaComponents.application,
231
- runtime: wrappedRuntime,
232
- telemetry: utilsSchemaComponents.telemetry,
233
- watch: {
234
- anyOf: [
235
- watch,
236
- {
237
- type: 'boolean'
238
- },
239
- {
240
- type: 'string'
241
- }
242
- ]
243
- },
244
- $schema: {
245
- type: 'string'
246
- },
247
- module: {
248
- type: 'string'
249
- }
250
- },
251
- additionalProperties: false,
252
- $defs
253
- }
254
-
255
- /* c8 ignore next 3 */
256
- if (process.argv[1] === import.meta.filename) {
257
- console.log(JSON.stringify(schema, null, 2))
258
- }
package/lib/stackable.js DELETED
@@ -1,88 +0,0 @@
1
- import { kMetadata, replaceEnv } from '@platformatic/foundation'
2
- import { ServiceStackable } from '@platformatic/service'
3
- import { ensureServices, platformaticComposer } 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 ComposerStackable extends ServiceStackable {
10
- #meta
11
- #dependencies
12
-
13
- constructor (root, config, context) {
14
- super(root, config, context)
15
- this.type = 'composer'
16
- this.version = packageJson.version
17
-
18
- this.applicationFactory = this.context.applicationFactory ?? platformaticComposer
19
- this.fastifyOptions ??= {}
20
- this.fastifyOptions.constraints = { notHost: notHostConstraints }
21
- }
22
-
23
- async getBootstrapDependencies () {
24
- await ensureServices(this.serviceId, this.config)
25
-
26
- const composedServices = this.config.composer?.services
27
- const dependencies = []
28
-
29
- if (Array.isArray(composedServices)) {
30
- dependencies.push(
31
- ...(await Promise.all(
32
- composedServices.map(async service => {
33
- return this.#parseDependency(service.id, service.origin)
34
- })
35
- ))
36
- )
37
- }
38
-
39
- this.#dependencies = dependencies
40
- return this.#dependencies
41
- }
42
-
43
- registerMeta (meta) {
44
- this.#meta = Object.assign(this.#meta ?? {}, meta)
45
- }
46
-
47
- async getMeta () {
48
- const serviceMeta = super.getMeta()
49
- const composerMeta = this.#meta ? { composer: this.#meta } : undefined
50
-
51
- return {
52
- ...serviceMeta,
53
- ...composerMeta
54
- }
55
- }
56
-
57
- async isHealthy () {
58
- // If no dependencies (still booting), assume healthy
59
- if (this.#dependencies) {
60
- const composedServices = this.#dependencies.map(dep => dep.id)
61
- const workers = await globalThis[kITC].send('getWorkers')
62
-
63
- for (const worker of Object.values(workers)) {
64
- if (composedServices.includes(worker.service) && !worker.status.startsWith('start')) {
65
- globalThis[kITC].notify('event', { event: 'unhealthy' })
66
- return false
67
- }
68
- }
69
- }
70
-
71
- globalThis[kITC].notify('event', { event: 'healthy' })
72
- return true
73
- }
74
-
75
- async #parseDependency (id, urlString) {
76
- let url = `http://${id}.plt.local`
77
-
78
- if (urlString) {
79
- const remoteUrl = await replaceEnv(urlString, this.config[kMetadata].env)
80
-
81
- if (remoteUrl) {
82
- url = remoteUrl
83
- }
84
- }
85
-
86
- return { id, url, local: url.endsWith('.plt.local') }
87
- }
88
- }
package/lib/upgrade.js DELETED
@@ -1,20 +0,0 @@
1
- import { abstractLogger } from '@platformatic/foundation'
2
- import { resolve } from 'node:path'
3
- import { semgrator } from 'semgrator'
4
-
5
- export async function upgrade (logger, config, version) {
6
- const iterator = semgrator({
7
- version,
8
- path: resolve(import.meta.dirname, 'versions'),
9
- input: config,
10
- logger: logger?.child({ name: '@platformatic/composer' }) ?? abstractLogger
11
- })
12
-
13
- let result
14
-
15
- for await (const updated of iterator) {
16
- result = updated.result
17
- }
18
-
19
- return result
20
- }