@platformatic/service 1.13.7 → 1.14.0

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/help/help.txt CHANGED
@@ -1,6 +1,9 @@
1
1
  Available commands:
2
2
 
3
3
  * `help` - show this help message.
4
- * `help <command>` - shows more information about a command.
4
+ * `help <command>` - show more information about a command.
5
5
  * `start` - start the server.
6
6
  * `schema config` - generate the schema configuration file.
7
+ * `compile` - compile the typescript files.
8
+ * `versions bump` - bump a new version of the API.
9
+ * `versions update` - update the latest version of the API.
@@ -0,0 +1,25 @@
1
+ Bump a new version of platformatic application API.
2
+
3
+ ``` bash
4
+ $ platformatic service versions bump
5
+ ```
6
+
7
+ As a result, a new application API version will be created, and mappers for the previous version will be generated.
8
+
9
+ Options:
10
+
11
+ * `-c, --config <path>` - Path to the configuration file.
12
+ * `-v, --version <string>` - The name of the version to bump. Default: if first version, then `v1`, else `vX`.
13
+ * `-p, --prefix <string>` - The prefix to use for the new version. Default: if first version, then `/v1`, else `/vX`.
14
+ * `--openai` - Use OpenAI to generate the version mappers plugins. Default: false.
15
+ * `--user-api-key <string>` - Platformatic user API key. If not specified, the key will be loaded from the `~/.platformatic/config` file.
16
+
17
+ If not specified, the configuration will be loaded from any of the following, in the current directory.
18
+
19
+ * `platformatic.db.json`, or
20
+ * `platformatic.db.yml`, or
21
+ * `platformatic.db.tml`
22
+
23
+ You can find more details about the configuration format here:
24
+ * [Platformatic DB Configuration](https://docs.platformatic.dev/docs/reference/db/configuration)
25
+
@@ -0,0 +1,23 @@
1
+ Update the latest version of platformatic application API.
2
+
3
+ ``` bash
4
+ $ platformatic service versions update
5
+ ```
6
+
7
+ As a result, the latest application API version will be updated, and mappers for the previous version will be generated.
8
+
9
+ Options:
10
+
11
+ * `-c, --config <path>` - Path to the configuration file.
12
+ * `--openai <boolean>` - Use OpenAI to generate the version mappers plugins. Default: false.
13
+ * `--user-api-key <string>` - Platformatic user API key. If not specified, the key will be loaded from the `~/.platformatic/config` file.
14
+
15
+ If not specified, the configuration will be loaded from any of the following, in the current directory.
16
+
17
+ * `platformatic.db.json`, or
18
+ * `platformatic.db.yml`, or
19
+ * `platformatic.db.tml`
20
+
21
+ You can find more details about the configuration format here:
22
+ * [Platformatic DB Configuration](https://docs.platformatic.dev/docs/reference/db/configuration)
23
+
package/index.js CHANGED
@@ -13,6 +13,7 @@ const setupMetrics = require('./lib/plugins/metrics')
13
13
  const setupTsCompiler = require('./lib/plugins/typescript')
14
14
  const setupHealthCheck = require('./lib/plugins/health-check')
15
15
  const loadPlugins = require('./lib/plugins/plugins')
16
+ const loadVersions = require('./lib/plugins/versions')
16
17
  const { telemetry } = require('@platformatic/telemetry')
17
18
 
18
19
  const { schema } = require('./lib/schema')
@@ -45,17 +46,15 @@ async function platformaticService (app, opts, toLoad) {
45
46
  const serviceConfig = config.service || {}
46
47
 
47
48
  if (isKeyEnabled('openapi', serviceConfig)) {
48
- app.register(setupOpenAPI, serviceConfig.openapi)
49
+ const openapi = serviceConfig.openapi
50
+ const versions = config.versions
51
+ app.register(setupOpenAPI, { openapi, versions })
49
52
  }
50
53
 
51
54
  if (isKeyEnabled('graphql', serviceConfig)) {
52
55
  app.register(setupGraphQL, serviceConfig.graphql)
53
56
  }
54
57
 
55
- if (isKeyEnabled('clients', config)) {
56
- app.register(setupClients, config.clients)
57
- }
58
-
59
58
  if (config.plugins) {
60
59
  let registerTsCompiler = false
61
60
  const typescript = config.plugins.paths && config.plugins.typescript
@@ -72,6 +71,17 @@ async function platformaticService (app, opts, toLoad) {
72
71
  app.register(loadPlugins)
73
72
  }
74
73
 
74
+ await app.register(async (app) => {
75
+ if (config.versions) {
76
+ // TODO: Add typescript mappers support
77
+ await app.register(loadVersions)
78
+ }
79
+ })
80
+
81
+ if (isKeyEnabled('clients', config)) {
82
+ app.register(setupClients, config.clients)
83
+ }
84
+
75
85
  if (isKeyEnabled('cors', config.server)) {
76
86
  app.register(setupCors, config.server.cors)
77
87
  }
@@ -0,0 +1,178 @@
1
+ 'use strict'
2
+
3
+ const { join, relative } = require('node:path')
4
+ const { mkdir, readFile, writeFile } = require('node:fs/promises')
5
+ const pino = require('pino')
6
+ const pretty = require('pino-pretty')
7
+ const { loadConfig } = require('@platformatic/config')
8
+ const { analyze, write: writeConfig } = require('@platformatic/metaconfig')
9
+ const { platformaticService } = require('../index.js')
10
+ const { getOpenapiSchema } = require('./get-openapi-schema.js')
11
+ const { createMappersPlugins } = require('./update-version.js')
12
+ const { changeOpenapiSchemaPrefix } = require('./utils')
13
+ const errors = require('./errors.js')
14
+
15
+ async function execute ({
16
+ logger,
17
+ configManager,
18
+ version,
19
+ prefix,
20
+ userApiKey,
21
+ openai,
22
+ openaiProxyHost
23
+ }) {
24
+ const config = configManager.current
25
+
26
+ const metaConfig = await analyze({ file: configManager.fullPath })
27
+ const rawConfig = metaConfig.config
28
+
29
+ const versionsDirName = 'versions'
30
+ const versionsDirPath = config.versions?.dir ??
31
+ join(configManager.dirname, versionsDirName)
32
+
33
+ let versionsConfigs = config.versions
34
+ let rawVersionsConfigs = rawConfig.versions
35
+
36
+ if (!config.versions) {
37
+ versionsConfigs = { dir: versionsDirName, configs: [] }
38
+ rawVersionsConfigs = { dir: versionsDirName, configs: [] }
39
+ }
40
+
41
+ let biggestVersion = 0
42
+ for (const versionConfig of versionsConfigs.configs) {
43
+ if (versionConfig.version === version) {
44
+ throw new errors.VersionAlreadyExists(version)
45
+ }
46
+ const versionNumber = parseInt(versionConfig.version.slice(1))
47
+ if (!isNaN(versionNumber)) {
48
+ biggestVersion = Math.max(biggestVersion, versionNumber)
49
+ }
50
+ }
51
+ version = version ?? `v${biggestVersion + 1}`
52
+ prefix = prefix ?? `/${version}`
53
+ prefix = prefix.charAt(0) === '/' ? prefix : `/${prefix}`
54
+
55
+ const versionDir = join(versionsDirPath, version)
56
+ await mkdir(versionDir, { recursive: true })
57
+
58
+ const latestVersionConfig = versionsConfigs.configs.at(-1)
59
+ const rawLatestVersionConfig = rawVersionsConfigs.configs.at(-1)
60
+
61
+ const latestVersion = latestVersionConfig?.version ?? null
62
+ const latestVersionPrefix = latestVersionConfig?.openapi?.prefix ?? ''
63
+
64
+ logger.info('Loading the latest openapi schema.')
65
+ const latestOpenapiSchema = await getOpenapiSchema({
66
+ logger,
67
+ configManager,
68
+ version: latestVersion
69
+ })
70
+
71
+ const newOpenapiSchema = changeOpenapiSchemaPrefix(
72
+ latestOpenapiSchema,
73
+ latestVersionPrefix,
74
+ prefix
75
+ )
76
+ const newOpenapiSchemaPath = join(versionDir, 'openapi.json')
77
+
78
+ logger.info(`Writing "${version}" openapi schema file.`)
79
+ await writeFile(newOpenapiSchemaPath, JSON.stringify(newOpenapiSchema, null, 2))
80
+
81
+ const newVersionConfig = {
82
+ version,
83
+ openapi: {
84
+ path: relative(configManager.dirname, newOpenapiSchemaPath),
85
+ prefix
86
+ }
87
+ }
88
+
89
+ if (latestVersionConfig) {
90
+ newVersionConfig.plugins = rawLatestVersionConfig.plugins
91
+ delete latestVersionConfig.plugins
92
+ delete rawLatestVersionConfig.plugins
93
+ } else if (config.plugins) {
94
+ newVersionConfig.plugins = rawConfig.plugins
95
+ delete config.plugins
96
+ delete rawConfig.plugins
97
+ }
98
+
99
+ versionsConfigs.configs.push(newVersionConfig)
100
+ rawVersionsConfigs.configs.push(newVersionConfig)
101
+
102
+ config.versions = versionsConfigs
103
+ rawConfig.versions = rawVersionsConfigs
104
+
105
+ await Promise.all([configManager.update(), writeConfig(metaConfig)])
106
+
107
+ if (latestVersionConfig) {
108
+ logger.info(`Reading openapi schema for "${latestVersion}"`)
109
+ const prevOpenapiSchemaPath = latestVersionConfig.openapi.path
110
+ const prevOpenapiSchemaFile = await readFile(prevOpenapiSchemaPath, 'utf8')
111
+ const prevOpenapiSchema = JSON.parse(prevOpenapiSchemaFile)
112
+
113
+ await createMappersPlugins({
114
+ logger,
115
+ configManager,
116
+ prevVersion: latestVersion,
117
+ nextVersion: version,
118
+ prevOpenapiSchema,
119
+ nextOpenapiSchema: newOpenapiSchema,
120
+ userApiKey,
121
+ openai,
122
+ openaiProxyHost
123
+ })
124
+ }
125
+ }
126
+
127
+ async function bumpVersion (_args) {
128
+ const logger = pino(pretty({
129
+ translateTime: 'SYS:HH:MM:ss',
130
+ ignore: 'hostname,pid'
131
+ }))
132
+
133
+ try {
134
+ const { configManager, args } = await loadConfig({
135
+ string: ['version', 'prefix', 'openai-proxy-host', 'user-api-key'],
136
+ boolean: ['openai'],
137
+ alias: {
138
+ v: 'version',
139
+ p: 'prefix'
140
+ }
141
+ }, _args, platformaticService)
142
+ await configManager.parseAndValidate()
143
+
144
+ const version = args.version
145
+ const prefix = args.prefix
146
+
147
+ const openai = args.openai ?? false
148
+ const openaiProxyHost = args['openai-proxy-host'] ?? null
149
+
150
+ let userApiKey = args['user-api-key'] ?? null
151
+ /* c8 ignore next 10 */
152
+ if (!userApiKey && openai) {
153
+ logger.info('Reading platformatic user api key')
154
+ const { getUserApiKey } = await import('@platformatic/authenticate')
155
+ try {
156
+ userApiKey = await getUserApiKey()
157
+ } catch (err) {
158
+ logger.error('Failed to read user api key. Please run "plt login" command.')
159
+ return
160
+ }
161
+ }
162
+
163
+ await execute({
164
+ logger,
165
+ configManager,
166
+ version,
167
+ prefix,
168
+ userApiKey,
169
+ openai,
170
+ openaiProxyHost
171
+ })
172
+ } catch (err) {
173
+ logger.error(err.message)
174
+ process.exit(1)
175
+ }
176
+ }
177
+
178
+ module.exports = { bumpVersion, execute }
package/lib/errors.js ADDED
@@ -0,0 +1,16 @@
1
+ 'use strict'
2
+
3
+ const createError = require('@fastify/error')
4
+
5
+ const ERROR_PREFIX = 'PLT_SERVICE'
6
+
7
+ module.exports = {
8
+ VersionNotSpecified: createError(
9
+ `${ERROR_PREFIX}_VERSION_NOT_SPECIFIED_ERROR`,
10
+ 'Version not specified. Use --version option to specify a version.'
11
+ ),
12
+ VersionAlreadyExists: createError(
13
+ `${ERROR_PREFIX}_VERSION_EXISTS_ERROR`,
14
+ 'Version %s already exists.'
15
+ )
16
+ }
@@ -0,0 +1,47 @@
1
+ 'use strict'
2
+
3
+ const { printAndExitLoadConfigError } = require('@platformatic/config')
4
+ const { buildServer } = require('./start')
5
+ const { platformaticService } = require('../index.js')
6
+
7
+ async function getOpenapiSchema ({ logger, configManager, version }) {
8
+ const config = configManager.current
9
+
10
+ let app = null
11
+ try {
12
+ app = await buildServer({ ...config, configManager }, platformaticService)
13
+ await app.ready()
14
+ /* c8 ignore next 4 */
15
+ } catch (err) {
16
+ printAndExitLoadConfigError(err)
17
+ process.exit(1)
18
+ }
19
+
20
+ if (!version) {
21
+ return app.swagger()
22
+ }
23
+
24
+ if (!config.versions) {
25
+ throw new Error('No versions configured')
26
+ }
27
+
28
+ const versionsConfigs = config.versions.configs ?? []
29
+ const versionConfig = versionsConfigs.find(v => v.version === version)
30
+ const versionPrefix = versionConfig.openapi.prefix ?? ''
31
+
32
+ const openapiUrl = versionPrefix
33
+ ? versionPrefix + '/documentation/json'
34
+ : '/documentation/json'
35
+
36
+ const { statusCode, body } = await app.inject({ method: 'GET', url: openapiUrl })
37
+
38
+ /* c8 ignore next 3 */
39
+ if (statusCode !== 200) {
40
+ throw new Error(`Failed to get openapi schema for version ${version}`)
41
+ }
42
+
43
+ const openapiSchema = JSON.parse(body)
44
+ return openapiSchema
45
+ }
46
+
47
+ module.exports = { getOpenapiSchema }
@@ -10,6 +10,7 @@ const fp = require('fastify-plugin')
10
10
  // despite being covered by test/routes.test.js
11
11
  /* c8 ignore next 33 */
12
12
  async function setupOpenAPI (app, opts) {
13
+ const { openapi, versions } = opts
13
14
  const openapiConfig = deepmerge({
14
15
  exposeRoute: true,
15
16
  info: {
@@ -17,7 +18,7 @@ async function setupOpenAPI (app, opts) {
17
18
  description: 'This is a service built on top of Platformatic',
18
19
  version: '1.0.0'
19
20
  }
20
- }, typeof opts === 'object' ? opts : {})
21
+ }, typeof openapi === 'object' ? openapi : {})
21
22
  app.log.trace({ openapi: openapiConfig })
22
23
  const swaggerOptions = {
23
24
  exposeRoute: openapiConfig.exposeRoute,
@@ -30,13 +31,24 @@ async function setupOpenAPI (app, opts) {
30
31
  /* istanbul ignore next */
31
32
  return json.$id || `def-${i}`
32
33
  }
34
+ },
35
+ transform: ({ schema, url }) => {
36
+ // Hide versioned endpoints
37
+ for (const version of versions?.configs ?? []) {
38
+ if (url.startsWith(version.openapi.prefix)) {
39
+ if (!schema) schema = {}
40
+ schema.hide = true
41
+ break
42
+ }
43
+ }
44
+ return { schema, url }
33
45
  }
34
46
  }
35
47
 
36
- if (opts.path) {
48
+ if (openapi.path) {
37
49
  swaggerOptions.mode = 'static'
38
50
  swaggerOptions.specification = {
39
- path: opts.path
51
+ path: openapi.path
40
52
  }
41
53
  }
42
54
  await app.register(Swagger, swaggerOptions)
@@ -44,7 +56,7 @@ async function setupOpenAPI (app, opts) {
44
56
  const { default: theme } = await import('@platformatic/swagger-ui-theme')
45
57
  app.register(SwaggerUI, {
46
58
  ...theme,
47
- ...opts,
59
+ ...openapi,
48
60
  logLevel: 'warn',
49
61
  prefix: '/documentation'
50
62
  })
@@ -0,0 +1,211 @@
1
+ 'use strict'
2
+
3
+ const { join } = require('node:path')
4
+ const { readFile } = require('node:fs/promises')
5
+ const deepClone = require('rfdc')({ proto: true })
6
+ const compareOpenApiSchemas = require('openapi-schema-diff')
7
+ const fp = require('fastify-plugin')
8
+ const {
9
+ changeOpenapiSchemaPrefix,
10
+ convertOpenApiToFastifyPath
11
+ } = require('../utils')
12
+
13
+ const wrapperPath = join(__dirname, 'sandbox-wrapper.js')
14
+
15
+ const Swagger = require('@fastify/swagger')
16
+ const SwaggerUI = require('@fastify/swagger-ui')
17
+
18
+ async function loadVersions (app) {
19
+ const configManager = app.platformatic.configManager
20
+ const config = configManager.current
21
+
22
+ const versions = config.versions ?? {}
23
+ const versionsConfigs = versions.configs ?? []
24
+
25
+ const latestVersionConfig = versionsConfigs.at(-1)
26
+ const latestVersion = latestVersionConfig.version
27
+ const latestVersionPrefix = latestVersionConfig.openapi.prefix ?? ''
28
+
29
+ const latestVersionPlugin = fp(async function (app) {
30
+ app.register(Swagger, {
31
+ exposeRoute: true,
32
+ openapi: {
33
+ info: {
34
+ title: 'Platformatic',
35
+ description: 'This is a service built on top of Platformatic',
36
+ version: latestVersion
37
+ }
38
+ },
39
+ refResolver: {
40
+ buildLocalReference (json, baseUri, fragment, i) {
41
+ /* istanbul ignore next */
42
+ return json.$id || `def-${i}`
43
+ }
44
+ }
45
+ })
46
+
47
+ app.register(SwaggerUI, {
48
+ logLevel: 'warn',
49
+ prefix: '/documentation'
50
+ })
51
+
52
+ if (latestVersionConfig.plugins) {
53
+ await app.register(require(wrapperPath), latestVersionConfig.plugins)
54
+ }
55
+ }, {
56
+ name: latestVersion,
57
+ encapsulate: true
58
+ })
59
+
60
+ await app.register(latestVersionPlugin, {
61
+ prefix: latestVersionPrefix
62
+ })
63
+
64
+ const latestOpenapiSchemaPath = latestVersionConfig.openapi.path
65
+ const latestOpenapiSchemaFile = await readFile(latestOpenapiSchemaPath, 'utf8')
66
+ const latestOpenapiSchema = JSON.parse(latestOpenapiSchemaFile)
67
+
68
+ let nextVersionPrefix = latestVersionPrefix
69
+ let nextNormalizedOpenapiSchema = changeOpenapiSchemaPrefix(
70
+ latestOpenapiSchema,
71
+ latestVersionConfig.openapi.prefix,
72
+ ''
73
+ )
74
+
75
+ for (let i = versionsConfigs.length - 2; i >= 0; i--) {
76
+ const prevVersionConfig = versionsConfigs[i]
77
+ const prevVersion = prevVersionConfig.version
78
+ const prevVersionPrefix = prevVersionConfig.openapi.prefix ?? ''
79
+ const prevOpenapiSchemaPath = prevVersionConfig.openapi.path
80
+
81
+ const prevOpenapiSchemaFile = await readFile(prevOpenapiSchemaPath, 'utf8')
82
+ const prevOpenapiSchema = JSON.parse(prevOpenapiSchemaFile)
83
+
84
+ const prevNormalizedOpenapiSchema = changeOpenapiSchemaPrefix(
85
+ prevOpenapiSchema,
86
+ prevVersionConfig.openapi.prefix,
87
+ ''
88
+ )
89
+
90
+ const schemaDiff = compareOpenApiSchemas(
91
+ prevNormalizedOpenapiSchema,
92
+ nextNormalizedOpenapiSchema
93
+ )
94
+
95
+ const versionPlugin = fp(async function (app) {
96
+ app.register(Swagger, {
97
+ exposeRoute: true,
98
+ openapi: {
99
+ info: {
100
+ title: 'Platformatic',
101
+ description: 'This is a service built on top of Platformatic',
102
+ version: prevVersion
103
+ }
104
+ },
105
+ refResolver: {
106
+ buildLocalReference (json, baseUri, fragment, i) {
107
+ /* istanbul ignore next */
108
+ return json.$id || `def-${i}`
109
+ }
110
+ }
111
+ })
112
+
113
+ app.register(SwaggerUI, {
114
+ logLevel: 'warn',
115
+ prefix: '/documentation'
116
+ })
117
+
118
+ const componentSchemas = prevOpenapiSchema.components?.schemas ?? {}
119
+ for (const componentSchemaId of Object.keys(componentSchemas)) {
120
+ const componentSchema = componentSchemas[componentSchemaId]
121
+ app.addSchema({ $id: componentSchemaId, ...componentSchema })
122
+ }
123
+
124
+ if (prevVersionConfig.plugins) {
125
+ await app.register(require(wrapperPath), prevVersionConfig.plugins)
126
+ }
127
+
128
+ for (const routeDiff of [...schemaDiff.deletedRoutes, ...schemaDiff.changedRoutes]) {
129
+ const method = routeDiff.method.toUpperCase()
130
+ const prevVersionPath = prevVersionPrefix + convertOpenApiToFastifyPath(routeDiff.path)
131
+
132
+ const hasRouteMapper = app.hasRoute({ url: prevVersionPath, method })
133
+ if (!hasRouteMapper) {
134
+ app.log.warn(`Missing route ${method} "${prevVersionPath}" in the "${prevVersion}" API version`)
135
+ }
136
+ }
137
+
138
+ const sameSchema = deepClone(prevNormalizedOpenapiSchema)
139
+ for (const normalizedPath in sameSchema.paths ?? {}) {
140
+ for (const method in sameSchema.paths[normalizedPath] ?? {}) {
141
+ const prevVersionPath = prevVersionPrefix + convertOpenApiToFastifyPath(normalizedPath)
142
+ const hasRouteMapper = app.hasRoute({
143
+ url: prevVersionPath,
144
+ method: method.toUpperCase()
145
+ })
146
+
147
+ const isSameRoute = schemaDiff.sameRoutes.find(
148
+ routeDiff =>
149
+ routeDiff.method === method.toLowerCase() &&
150
+ routeDiff.path === normalizedPath
151
+ )
152
+
153
+ if (!isSameRoute || hasRouteMapper) {
154
+ delete sameSchema.paths[normalizedPath][method]
155
+ }
156
+ }
157
+ if (Object.keys(sameSchema.paths[normalizedPath]).length === 0) {
158
+ delete sameSchema.paths[normalizedPath]
159
+ }
160
+ }
161
+
162
+ if (Object.keys(sameSchema.paths).length > 0) {
163
+ const versionPrefix = nextVersionPrefix
164
+
165
+ await app.register(await import('fastify-openapi-glue'), {
166
+ specification: sameSchema,
167
+ operationResolver: (operationId, method) => {
168
+ return {
169
+ handler: async (req, reply) => {
170
+ const prevVersionUrl = req.raw.url
171
+ const nextVersionUrl = prevVersionUrl.replace(
172
+ prevVersionPrefix,
173
+ versionPrefix
174
+ )
175
+
176
+ const headers = req.headers
177
+ delete headers.connection
178
+ delete headers['content-length']
179
+ delete headers['transfer-encoding']
180
+
181
+ const res = await app.inject({
182
+ method: method.toUpperCase(),
183
+ url: nextVersionUrl,
184
+ headers,
185
+ payload: req.body
186
+ })
187
+
188
+ reply
189
+ .code(res.statusCode)
190
+ .headers(res.headers)
191
+ .send(res.body)
192
+ }
193
+ }
194
+ }
195
+ })
196
+ }
197
+ }, {
198
+ name: prevVersion,
199
+ encapsulate: true
200
+ })
201
+
202
+ await app.register(versionPlugin, {
203
+ prefix: prevVersionPrefix
204
+ })
205
+
206
+ nextVersionPrefix = prevVersionPrefix
207
+ nextNormalizedOpenapiSchema = prevNormalizedOpenapiSchema
208
+ }
209
+ }
210
+
211
+ module.exports = fp(loadVersions)
@@ -5,9 +5,27 @@ const fastifyStatic = require('@fastify/static')
5
5
  const userAgentParser = require('ua-parser-js')
6
6
 
7
7
  module.exports = async (app, opts) => {
8
+ const versions = opts.versions || {}
9
+
8
10
  app.register(fastifyStatic, {
9
11
  root: path.join(__dirname, 'public')
10
12
  })
13
+
14
+ app.route({
15
+ method: 'GET',
16
+ path: '/_platformatic_versions',
17
+ schema: { hide: true },
18
+ handler: () => {
19
+ const openapiUrls = []
20
+ for (const versionConfig of versions?.configs ?? []) {
21
+ const name = versionConfig.version
22
+ const prefix = versionConfig.openapi.prefix
23
+ openapiUrls.push({ name, prefix })
24
+ }
25
+ return openapiUrls
26
+ }
27
+ })
28
+
11
29
  // root endpoint
12
30
  app.route({
13
31
  method: 'GET',