@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 +4 -1
- package/help/versions bump.txt +25 -0
- package/help/versions update.txt +23 -0
- package/index.js +15 -5
- package/lib/bump-version.js +178 -0
- package/lib/errors.js +16 -0
- package/lib/get-openapi-schema.js +47 -0
- package/lib/plugins/openapi.js +16 -4
- package/lib/plugins/versions.js +211 -0
- package/lib/root-endpoint/index.js +18 -0
- package/lib/root-endpoint/public/index.html +35 -7
- package/lib/schema.js +58 -11
- package/lib/start.js +3 -1
- package/lib/update-version.js +862 -0
- package/lib/utils.js +126 -3
- package/package.json +17 -8
- package/service.mjs +5 -1
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>` -
|
|
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
|
-
|
|
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 }
|
package/lib/plugins/openapi.js
CHANGED
|
@@ -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
|
|
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 (
|
|
48
|
+
if (openapi.path) {
|
|
37
49
|
swaggerOptions.mode = 'static'
|
|
38
50
|
swaggerOptions.specification = {
|
|
39
|
-
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
|
-
...
|
|
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',
|