@platformatic/runtime 0.26.0 → 0.27.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.
@@ -0,0 +1,65 @@
1
+ {
2
+ "openapi": "3.0.3",
3
+ "info": {
4
+ "version": "8.3.1",
5
+ "title": "@fastify/swagger"
6
+ },
7
+ "components": {
8
+ "schemas": {}
9
+ },
10
+ "paths": {
11
+ "/users": {
12
+ "get": {
13
+ "responses": {
14
+ "200": {
15
+ "description": "Default Response"
16
+ }
17
+ }
18
+ },
19
+ "post": {
20
+ "responses": {
21
+ "200": {
22
+ "description": "Default Response"
23
+ }
24
+ }
25
+ },
26
+ "put": {
27
+ "responses": {
28
+ "200": {
29
+ "description": "Default Response"
30
+ }
31
+ }
32
+ }
33
+ },
34
+ "/users/{id}": {
35
+ "get": {
36
+ "responses": {
37
+ "200": {
38
+ "description": "Default Response"
39
+ }
40
+ }
41
+ },
42
+ "post": {
43
+ "responses": {
44
+ "200": {
45
+ "description": "Default Response"
46
+ }
47
+ }
48
+ },
49
+ "put": {
50
+ "responses": {
51
+ "200": {
52
+ "description": "Default Response"
53
+ }
54
+ }
55
+ },
56
+ "delete": {
57
+ "responses": {
58
+ "200": {
59
+ "description": "Default Response"
60
+ }
61
+ }
62
+ }
63
+ }
64
+ }
65
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "$schema": "https://platformatic.dev/schemas/v0.23.2/composer",
3
+ "server": {
4
+ "hostname": "127.0.0.1",
5
+ "port": 0,
6
+ "logger": {
7
+ "level": "info"
8
+ }
9
+ },
10
+ "composer": {
11
+ "services": [
12
+ {
13
+ "id": "api1",
14
+ "origin": "http://127.0.0.1",
15
+ "openapi": {
16
+ "file": "./api1.json"
17
+ }
18
+ }
19
+ ],
20
+ "refreshTimeout": 1000
21
+ },
22
+ "watch": false
23
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://platformatic.dev/schemas/v0.22.0/trickortreat",
3
+ "server": {
4
+ "hostname": "127.0.0.1",
5
+ "port": 0,
6
+ "logger": {
7
+ "level": "info",
8
+ "name": "hello server"
9
+ }
10
+ },
11
+ "plugins": {
12
+ "paths": ["./plugin.js"],
13
+ "hotReload": false
14
+ },
15
+ "service": {
16
+ "openapi": true
17
+ },
18
+ "metrics": false,
19
+ "watch": false
20
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "server": {
3
+ "hostname": "127.0.0.1",
4
+ "port": 0,
5
+ "logger": {
6
+ "level": "info",
7
+ "name": "hello server"
8
+ }
9
+ },
10
+ "plugins": {
11
+ "paths": ["./plugin.js"],
12
+ "hotReload": false
13
+ },
14
+ "service": {
15
+ "openapi": true
16
+ },
17
+ "metrics": false,
18
+ "watch": false
19
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "$schema": "https://platformatic.dev/schemas/v0.22.0/db",
3
+ "server": {
4
+ "hostname": "127.0.0.1",
5
+ "port": 0
6
+ },
7
+ "migrations": {
8
+ "dir": "migrations",
9
+ "table": "versions"
10
+ },
11
+ "types": {
12
+ "autogenerate": false
13
+ },
14
+ "db": {
15
+ "connectionString": "sqlite://db.sqlite",
16
+ "graphql": true,
17
+ "ignore": {
18
+ "versions": true
19
+ },
20
+ "events": false
21
+ }
22
+ }
@@ -0,0 +1,9 @@
1
+ 'use strict'
2
+ const { startCommand } = require('../lib/unified-api')
3
+
4
+ async function main () {
5
+ await startCommand(['-c', process.argv[2]])
6
+ process.exit(42)
7
+ }
8
+
9
+ main()
@@ -0,0 +1,9 @@
1
+ 'use strict'
2
+ const { start } = require('../lib/unified-api')
3
+
4
+ async function main () {
5
+ await start(['-c', process.argv[2]])
6
+ process.exit(42)
7
+ }
8
+
9
+ main()
package/index.js CHANGED
@@ -1,33 +1,13 @@
1
1
  'use strict'
2
- const ConfigManager = require('@platformatic/config')
2
+ const { buildServer } = require('./lib/build-server')
3
3
  const { platformaticRuntime } = require('./lib/config')
4
- const { start, startWithConfig } = require('./lib/start')
5
-
6
- async function buildServer (options = {}) {
7
- if (!options.configManager) {
8
- // Instantiate a new config manager from the current options.
9
- const cm = new ConfigManager({
10
- ...platformaticRuntime.configManagerConfig,
11
- source: options
12
- })
13
- await cm.parseAndValidate()
14
-
15
- if (typeof options === 'string') {
16
- options = { configManager: cm }
17
- } else {
18
- options.configManager = cm
19
- }
20
- }
21
-
22
- // The transformConfig() function can't be sent between threads.
23
- delete options.configManager._transformConfig
24
-
25
- return startWithConfig(options.configManager)
26
- }
4
+ const { start } = require('./lib/start')
5
+ const unifiedApi = require('./lib/unified-api')
27
6
 
28
7
  module.exports = {
29
8
  buildServer,
30
9
  platformaticRuntime,
31
10
  schema: platformaticRuntime.schema,
32
- start
11
+ start,
12
+ unifiedApi
33
13
  }
package/lib/app.js CHANGED
@@ -1,14 +1,11 @@
1
1
  'use strict'
2
2
  const { once } = require('node:events')
3
3
  const { dirname } = require('node:path')
4
- const {
5
- addLoggerToTheConfig
6
- } = require('@platformatic/service')
4
+ const { FileWatcher } = require('@platformatic/utils')
7
5
  const {
8
6
  buildServer,
9
7
  loadConfig
10
- } = require('@platformatic/start')
11
- const { FileWatcher } = require('@platformatic/utils')
8
+ } = require('./unified-api')
12
9
 
13
10
  class PlatformaticApp {
14
11
  #hotReload
@@ -16,8 +13,9 @@ class PlatformaticApp {
16
13
  #restarting
17
14
  #started
18
15
  #originalWatch
16
+ #logger
19
17
 
20
- constructor (appConfig, loaderPort) {
18
+ constructor (appConfig, loaderPort, logger) {
21
19
  this.appConfig = appConfig
22
20
  this.config = null
23
21
  this.#hotReload = false
@@ -26,6 +24,7 @@ class PlatformaticApp {
26
24
  this.server = null
27
25
  this.#started = false
28
26
  this.#originalWatch = null
27
+ this.#logger = logger
29
28
  }
30
29
 
31
30
  async restart (force) {
@@ -61,6 +60,7 @@ class PlatformaticApp {
61
60
 
62
61
  this.#originalWatch = config.watch
63
62
  config.watch = false
63
+ this.#setuplogger(configManager)
64
64
 
65
65
  try {
66
66
  // If this is a restart, have the fastify server restart itself. If this
@@ -189,11 +189,7 @@ class PlatformaticApp {
189
189
  }
190
190
  }
191
191
 
192
- // Set the logger if not present (and the config supports it).
193
- if (configManager.current.server) {
194
- addLoggerToTheConfig(configManager.current)
195
- configManager.current.server.logger.name = this.appConfig.id
196
- }
192
+ this.#setuplogger(configManager)
197
193
 
198
194
  this.#hotReload = args.hotReload && this.appConfig.hotReload
199
195
 
@@ -219,6 +215,16 @@ class PlatformaticApp {
219
215
  })
220
216
  }
221
217
 
218
+ #setuplogger (configManager) {
219
+ // Set the logger if not present (and the config supports it).
220
+ if (configManager.current.server) {
221
+ const childLogger = this.#logger.child({
222
+ name: this.appConfig.id
223
+ }, { level: configManager.current.server.logger?.level || 'info' })
224
+ configManager.current.server.logger = childLogger
225
+ }
226
+ }
227
+
222
228
  #startFileWatching () {
223
229
  const server = this.server
224
230
  const { configManager } = server.platformatic
@@ -253,7 +259,7 @@ class PlatformaticApp {
253
259
 
254
260
  #logAndExit (err) {
255
261
  this.config?.configManager?.stopWatching()
256
- console.error(err)
262
+ this.#logger.error({ err })
257
263
  process.exit(1)
258
264
  }
259
265
  }
@@ -0,0 +1,28 @@
1
+ 'use strict'
2
+ const ConfigManager = require('@platformatic/config')
3
+ const { platformaticRuntime } = require('./config')
4
+ const { startWithConfig } = require('./start')
5
+
6
+ async function buildServer (options = {}) {
7
+ if (!options.configManager) {
8
+ // Instantiate a new config manager from the current options.
9
+ const cm = new ConfigManager({
10
+ ...platformaticRuntime.configManagerConfig,
11
+ source: options
12
+ })
13
+ await cm.parseAndValidate()
14
+
15
+ if (typeof options === 'string') {
16
+ options = { configManager: cm }
17
+ } else {
18
+ options.configManager = cm
19
+ }
20
+ }
21
+
22
+ // The transformConfig() function can't be sent between threads.
23
+ delete options.configManager._transformConfig
24
+
25
+ return startWithConfig(options.configManager)
26
+ }
27
+
28
+ module.exports = { buildServer }
package/lib/start.js CHANGED
@@ -35,8 +35,8 @@ async function startWithConfig (configManager) {
35
35
  configManager.fileWatcher?.stopWatching()
36
36
  })
37
37
 
38
- worker.on('error', (err) => {
39
- console.error(err)
38
+ worker.on('error', () => {
39
+ // the error is logged in the worker
40
40
  process.exit(1)
41
41
  })
42
42
 
@@ -0,0 +1,171 @@
1
+ 'use strict'
2
+ const { resolve } = require('node:path')
3
+ const parseArgs = require('minimist')
4
+ const ConfigManager = require('@platformatic/config')
5
+ const {
6
+ platformaticService,
7
+ buildServer,
8
+ loadConfig,
9
+ start,
10
+ schema: serviceSchema
11
+ } = require('@platformatic/service')
12
+ const {
13
+ schema: dbSchema,
14
+ platformaticDB
15
+ } = require('@platformatic/db')
16
+ const {
17
+ schema: composerSchema,
18
+ platformaticComposer
19
+ } = require('@platformatic/composer')
20
+ const { buildServer: runtimeBuildServer } = require('./build-server')
21
+ const { platformaticRuntime } = require('./config')
22
+ const { schema: runtimeSchema } = require('./schema')
23
+ const { start: runtimeStart } = require('./start')
24
+
25
+ const kSupportedAppTypes = new Set(['service', 'db', 'composer', 'runtime'])
26
+
27
+ async function tryGetConfigTypeFromSchema (config) {
28
+ /* c8 ignore next 6 - c8 is not seeing this as covered for some reason. */
29
+ if (typeof config === 'string') {
30
+ // Handle config file paths.
31
+ const loadedConfig = await loadConfig({}, ['-c', config], platformaticService)
32
+
33
+ config = loadedConfig.configManager.current
34
+ }
35
+
36
+ const schema = config?.$schema
37
+
38
+ if (typeof schema !== 'string') {
39
+ throw new Error('configuration is missing a schema')
40
+ }
41
+
42
+ const configType = schema.split('/').pop()
43
+
44
+ if (!kSupportedAppTypes.has(configType)) {
45
+ throw new Error(`unknown configuration type: '${configType}'`)
46
+ }
47
+
48
+ return configType
49
+ }
50
+
51
+ async function getConfigType (args = [], directory) {
52
+ try {
53
+ // The config type was not specified, so we need to figure it out.
54
+ // Try to get the config file from the provided arguments.
55
+ let { config } = parseArgs(args, { alias: { c: 'config' } })
56
+
57
+ if (!config) {
58
+ // Couldn't get the config file from the arguments, so look in the
59
+ // provided directory (or current directory) for any recognized
60
+ // config files.
61
+ const searchDir = directory ?? process.cwd()
62
+ const configFile = await ConfigManager.findConfigFile(searchDir)
63
+
64
+ config = resolve(searchDir, configFile)
65
+ }
66
+
67
+ // At this point, we have the config file. However, several different
68
+ // file formats are supported, so use the config manager to parse the
69
+ // file (without worrying about the validity of the file). We can then
70
+ // use the $schema field to determine the config type.
71
+ const configManager = new ConfigManager({ source: config })
72
+ const configString = await configManager.load()
73
+ const parsedConfig = configManager._parser(configString)
74
+
75
+ return await tryGetConfigTypeFromSchema(parsedConfig)
76
+ } catch (err) {
77
+ const configFiles = ConfigManager.listConfigFiles()
78
+ const msg = `
79
+ Missing config file!
80
+ Be sure to have a config file with one of the following names:
81
+ ${configFiles.map((s) => ' * ' + s).join('\n')}
82
+ Alternatively run "npm create platformatic@latest" to generate a basic plt service config.
83
+ `
84
+
85
+ throw new Error(msg, { cause: err })
86
+ }
87
+ }
88
+
89
+ async function getCurrentSchema (configType) {
90
+ if (configType === 'service') {
91
+ return serviceSchema.schema
92
+ } else if (configType === 'db') {
93
+ return dbSchema
94
+ } else if (configType === 'composer') {
95
+ return composerSchema
96
+ } else if (configType === 'runtime') {
97
+ return runtimeSchema
98
+ }
99
+
100
+ throw new Error(`unknown configuration type: '${configType}'`)
101
+ }
102
+
103
+ /* c8 ignore next 10 - for some reason c8 is not seeing this as covered. */
104
+ async function _buildServer (options) {
105
+ const configType = await tryGetConfigTypeFromSchema(options)
106
+ const app = getApp(configType)
107
+
108
+ if (app === platformaticRuntime) {
109
+ return runtimeBuildServer(options)
110
+ }
111
+
112
+ return buildServer(options, app)
113
+ }
114
+
115
+ function getApp (configType) {
116
+ if (configType === 'service') {
117
+ return platformaticService
118
+ } else if (configType === 'db') {
119
+ return platformaticDB
120
+ } else if (configType === 'composer') {
121
+ return platformaticComposer
122
+ } else if (configType === 'runtime') {
123
+ return platformaticRuntime
124
+ }
125
+
126
+ throw new Error('unknown kind: ' + configType)
127
+ }
128
+
129
+ async function _loadConfig (minimistConfig, args, configType, overrides) {
130
+ // If the config type was specified, then use that. Otherwise, compute it.
131
+ if (typeof configType !== 'string') {
132
+ configType = await getConfigType(args)
133
+ }
134
+
135
+ return loadConfig(minimistConfig, args, getApp(configType), overrides)
136
+ }
137
+
138
+ async function _start (args) {
139
+ const configType = await getConfigType(args)
140
+
141
+ if (configType === 'runtime') {
142
+ return runtimeStart(args)
143
+ }
144
+
145
+ return start(getApp(configType), args)
146
+ }
147
+
148
+ async function startCommand (args) {
149
+ try {
150
+ await _start(args)
151
+ } catch (err) {
152
+ delete err?.stack
153
+ console.error(err?.message)
154
+
155
+ if (err?.cause) {
156
+ console.error(`${err.cause}`)
157
+ }
158
+
159
+ process.exit(1)
160
+ }
161
+ }
162
+
163
+ module.exports = {
164
+ buildServer: _buildServer,
165
+ getConfigType,
166
+ getCurrentSchema,
167
+ loadConfig: _loadConfig,
168
+ start: _start,
169
+ startCommand,
170
+ getApp
171
+ }
package/lib/worker.js CHANGED
@@ -10,12 +10,38 @@ const globalDispatcher = new FastifyUndiciDispatcher({
10
10
  // setting the domain here allows for fail-fast scenarios
11
11
  domain: '.plt.local'
12
12
  })
13
+ const pino = require('pino')
14
+ const { isatty } = require('tty')
15
+
13
16
  const applications = new Map()
14
17
  let entrypoint
15
18
 
16
19
  delete globalThis.LOADER_PORT
17
20
  setGlobalDispatcher(globalDispatcher)
18
21
 
22
+ let transport
23
+
24
+ /* c8 ignore next 5 */
25
+ if (isatty(1)) {
26
+ transport = pino.transport({
27
+ target: 'pino-pretty'
28
+ })
29
+ }
30
+
31
+ const logger = pino(transport)
32
+
33
+ process.once('uncaughtException', (err) => {
34
+ logger.error({ err }, 'runtime error')
35
+ throw err
36
+ })
37
+
38
+ // Tested by test/cli/start.test.mjs by C8 does not see it.
39
+ /* c8 ignore next 4 */
40
+ process.once('unhandledRejection', (err) => {
41
+ logger.error({ err }, 'runtime error')
42
+ throw err
43
+ })
44
+
19
45
  parentPort.on('message', async (msg) => {
20
46
  for (const app of applications.values()) {
21
47
  await app.handleProcessLevelEvent(msg)
@@ -39,6 +65,7 @@ parentPort.on('message', async (msg) => {
39
65
  case 'plt:stop':
40
66
  process.exit() // Exit the worker thread.
41
67
  break
68
+ /* c8 ignore next 3 */
42
69
  case undefined:
43
70
  // Ignore
44
71
  break
@@ -52,7 +79,7 @@ async function main () {
52
79
 
53
80
  for (let i = 0; i < services.length; ++i) {
54
81
  const service = services[i]
55
- const app = new PlatformaticApp(service, loaderPort)
82
+ const app = new PlatformaticApp(service, loaderPort, logger)
56
83
 
57
84
  applications.set(service.id, app)
58
85
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/runtime",
3
- "version": "0.26.0",
3
+ "version": "0.27.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -17,12 +17,12 @@
17
17
  },
18
18
  "homepage": "https://github.com/platformatic/platformatic#readme",
19
19
  "devDependencies": {
20
- "c8": "^7.13.0",
21
- "execa": "^7.0.0",
20
+ "c8": "^8.0.0",
21
+ "execa": "^7.1.1",
22
22
  "snazzy": "^9.0.0",
23
- "split2": "^4.1.0",
24
- "standard": "^17.0.0",
25
- "tsd": "^0.28.0"
23
+ "split2": "^4.2.0",
24
+ "standard": "^17.1.0",
25
+ "tsd": "^0.28.1"
26
26
  },
27
27
  "dependencies": {
28
28
  "@hapi/topo": "^6.0.2",
@@ -30,15 +30,18 @@
30
30
  "commist": "^3.2.0",
31
31
  "desm": "^1.3.0",
32
32
  "es-main": "^1.2.0",
33
- "fastify": "^4.17.0",
34
- "fastify-undici-dispatcher": "^0.4.0",
33
+ "fastify": "^4.18.0",
34
+ "fastify-undici-dispatcher": "^0.4.1",
35
35
  "help-me": "^4.2.0",
36
36
  "minimist": "^1.2.8",
37
- "undici": "^5.20.0",
38
- "@platformatic/config": "0.26.0",
39
- "@platformatic/service": "0.26.0",
40
- "@platformatic/start": "0.26.0",
41
- "@platformatic/utils": "0.26.0"
37
+ "pino": "^8.14.1",
38
+ "pino-pretty": "^10.0.0",
39
+ "undici": "^5.22.1",
40
+ "@platformatic/composer": "0.27.0",
41
+ "@platformatic/config": "0.27.0",
42
+ "@platformatic/db": "0.27.0",
43
+ "@platformatic/service": "0.27.0",
44
+ "@platformatic/utils": "0.27.0"
42
45
  },
43
46
  "standard": {
44
47
  "ignore": [
package/test/app.test.js CHANGED
@@ -5,8 +5,17 @@ const { join } = require('node:path')
5
5
  const { test } = require('node:test')
6
6
  const { PlatformaticApp } = require('../lib/app')
7
7
  const fixturesDir = join(__dirname, '..', 'fixtures')
8
+ const pino = require('pino')
9
+ const split = require('split2')
10
+
11
+ function getLoggerAndStream () {
12
+ const stream = split(JSON.parse)
13
+ const logger = pino(stream)
14
+ return { logger, stream }
15
+ }
8
16
 
9
17
  test('logs errors during startup', async (t) => {
18
+ const { logger, stream } = getLoggerAndStream()
10
19
  const appPath = join(fixturesDir, 'serviceAppThrowsOnStart')
11
20
  const configFile = join(appPath, 'platformatic.service.json')
12
21
  const config = {
@@ -16,9 +25,8 @@ test('logs errors during startup', async (t) => {
16
25
  entrypoint: true,
17
26
  hotReload: true
18
27
  }
19
- const app = new PlatformaticApp(config, null)
28
+ const app = new PlatformaticApp(config, null, logger)
20
29
 
21
- t.mock.method(console, 'error', () => {})
22
30
  t.mock.method(process, 'exit', () => { throw new Error('exited') })
23
31
 
24
32
  await assert.rejects(async () => {
@@ -26,11 +34,18 @@ test('logs errors during startup', async (t) => {
26
34
  }, /exited/)
27
35
  assert.strictEqual(process.exit.mock.calls.length, 1)
28
36
  assert.strictEqual(process.exit.mock.calls[0].arguments[0], 1)
29
- assert.strictEqual(console.error.mock.calls.length, 1)
30
- assert.strictEqual(console.error.mock.calls[0].arguments[0].message, 'boom')
37
+
38
+ stream.end()
39
+ const lines = []
40
+ for await (const line of stream) {
41
+ lines.push(line)
42
+ }
43
+ const lastLine = lines[lines.length - 1]
44
+ assert.strictEqual(lastLine.msg, 'boom')
31
45
  })
32
46
 
33
47
  test('errors when starting an already started application', async (t) => {
48
+ const { logger } = getLoggerAndStream()
34
49
  const appPath = join(fixturesDir, 'monorepo', 'serviceApp')
35
50
  const configFile = join(appPath, 'platformatic.service.json')
36
51
  const config = {
@@ -43,7 +58,7 @@ test('errors when starting an already started application', async (t) => {
43
58
  dependents: [],
44
59
  localServiceEnvVars: new Map([['PLT_WITH_LOGGER_URL', ' ']])
45
60
  }
46
- const app = new PlatformaticApp(config, null)
61
+ const app = new PlatformaticApp(config, null, logger)
47
62
 
48
63
  t.after(app.stop.bind(app))
49
64
  await app.start()
@@ -53,6 +68,7 @@ test('errors when starting an already started application', async (t) => {
53
68
  })
54
69
 
55
70
  test('errors when stopping an already stopped application', async (t) => {
71
+ const { logger } = getLoggerAndStream()
56
72
  const appPath = join(fixturesDir, 'monorepo', 'serviceApp')
57
73
  const configFile = join(appPath, 'platformatic.service.json')
58
74
  const config = {
@@ -65,7 +81,7 @@ test('errors when stopping an already stopped application', async (t) => {
65
81
  dependents: [],
66
82
  localServiceEnvVars: new Map([['PLT_WITH_LOGGER_URL', ' ']])
67
83
  }
68
- const app = new PlatformaticApp(config, null)
84
+ const app = new PlatformaticApp(config, null, logger)
69
85
 
70
86
  await assert.rejects(async () => {
71
87
  await app.stop()
@@ -73,6 +89,7 @@ test('errors when stopping an already stopped application', async (t) => {
73
89
  })
74
90
 
75
91
  test('does not restart while restarting', async (t) => {
92
+ const { logger } = getLoggerAndStream()
76
93
  const appPath = join(fixturesDir, 'monorepo', 'serviceApp')
77
94
  const configFile = join(appPath, 'platformatic.service.json')
78
95
  const config = {
@@ -85,7 +102,7 @@ test('does not restart while restarting', async (t) => {
85
102
  dependents: [],
86
103
  localServiceEnvVars: new Map([['PLT_WITH_LOGGER_URL', ' ']])
87
104
  }
88
- const app = new PlatformaticApp(config, null)
105
+ const app = new PlatformaticApp(config, null, logger)
89
106
 
90
107
  t.after(app.stop.bind(app))
91
108
  await app.start()
@@ -101,6 +118,7 @@ test('does not restart while restarting', async (t) => {
101
118
  })
102
119
 
103
120
  test('restarts on SIGUSR2', async (t) => {
121
+ const { logger } = getLoggerAndStream()
104
122
  const appPath = join(fixturesDir, 'monorepo', 'serviceApp')
105
123
  const configFile = join(appPath, 'platformatic.service.json')
106
124
  const config = {
@@ -113,7 +131,7 @@ test('restarts on SIGUSR2', async (t) => {
113
131
  dependents: [],
114
132
  localServiceEnvVars: new Map([['PLT_WITH_LOGGER_URL', ' ']])
115
133
  }
116
- const app = new PlatformaticApp(config, null)
134
+ const app = new PlatformaticApp(config, null, logger)
117
135
 
118
136
  t.after(app.stop.bind(app))
119
137
  await app.start()
@@ -123,6 +141,7 @@ test('restarts on SIGUSR2', async (t) => {
123
141
  })
124
142
 
125
143
  test('stops on signals other than SIGUSR2', async (t) => {
144
+ const { logger } = getLoggerAndStream()
126
145
  const appPath = join(fixturesDir, 'monorepo', 'serviceApp')
127
146
  const configFile = join(appPath, 'platformatic.service.json')
128
147
  const config = {
@@ -135,7 +154,7 @@ test('stops on signals other than SIGUSR2', async (t) => {
135
154
  dependents: [],
136
155
  localServiceEnvVars: new Map([['PLT_WITH_LOGGER_URL', ' ']])
137
156
  }
138
- const app = new PlatformaticApp(config, null)
157
+ const app = new PlatformaticApp(config, null, logger)
139
158
 
140
159
  t.after(async () => {
141
160
  try {
@@ -151,6 +170,7 @@ test('stops on signals other than SIGUSR2', async (t) => {
151
170
  })
152
171
 
153
172
  test('stops on uncaught exceptions', async (t) => {
173
+ const { logger } = getLoggerAndStream()
154
174
  const appPath = join(fixturesDir, 'monorepo', 'serviceApp')
155
175
  const configFile = join(appPath, 'platformatic.service.json')
156
176
  const config = {
@@ -163,7 +183,7 @@ test('stops on uncaught exceptions', async (t) => {
163
183
  dependents: [],
164
184
  localServiceEnvVars: new Map([['PLT_WITH_LOGGER_URL', ' ']])
165
185
  }
166
- const app = new PlatformaticApp(config, null)
186
+ const app = new PlatformaticApp(config, null, logger)
167
187
 
168
188
  t.after(async () => {
169
189
  try {
@@ -193,8 +213,9 @@ test('supports configuration overrides', async (t) => {
193
213
  }
194
214
 
195
215
  await t.test('throws on non-string config paths', async (t) => {
216
+ const { logger } = getLoggerAndStream()
196
217
  config._configOverrides = new Map([[null, 5]])
197
- const app = new PlatformaticApp(config, null)
218
+ const app = new PlatformaticApp(config, null, logger)
198
219
 
199
220
  t.after(async () => {
200
221
  try {
@@ -210,8 +231,9 @@ test('supports configuration overrides', async (t) => {
210
231
  })
211
232
 
212
233
  await t.test('ignores invalid config paths', async (t) => {
234
+ const { logger } = getLoggerAndStream()
213
235
  config._configOverrides = new Map([['foo.bar.baz', 5]])
214
- const app = new PlatformaticApp(config, null)
236
+ const app = new PlatformaticApp(config, null, logger)
215
237
 
216
238
  t.after(async () => {
217
239
  try {
@@ -225,11 +247,12 @@ test('supports configuration overrides', async (t) => {
225
247
  })
226
248
 
227
249
  await t.test('sets valid config paths', async (t) => {
250
+ const { logger } = getLoggerAndStream()
228
251
  config._configOverrides = new Map([
229
252
  ['server.keepAliveTimeout', 1],
230
253
  ['server.port', 0]
231
254
  ])
232
- const app = new PlatformaticApp(config, null)
255
+ const app = new PlatformaticApp(config, null, logger)
233
256
 
234
257
  t.after(async () => {
235
258
  try {
@@ -29,14 +29,14 @@ test('handles startup errors', async (t) => {
29
29
  const { execa } = await import('execa')
30
30
  const config = join(import.meta.url, '..', '..', 'fixtures', 'configs', 'service-throws-on-start.json')
31
31
  const child = execa(process.execPath, [cliPath, 'start', '-c', config], { encoding: 'utf8' })
32
- let stderr = ''
32
+ let stdout = ''
33
33
  let found = false
34
34
 
35
- for await (const messages of on(child.stderr, 'data')) {
35
+ for await (const messages of on(child.stdout, 'data')) {
36
36
  for (const message of messages) {
37
- stderr += message
37
+ stdout += message
38
38
 
39
- if (/Error: boom/.test(stderr)) {
39
+ if (/Error: boom/.test(stdout)) {
40
40
  found = true
41
41
  break
42
42
  }
@@ -0,0 +1,321 @@
1
+ 'use strict'
2
+ const assert = require('node:assert')
3
+ const { spawn } = require('node:child_process')
4
+ const { once } = require('node:events')
5
+ const { join } = require('node:path')
6
+ const { test } = require('node:test')
7
+ const {
8
+ buildServer,
9
+ getConfigType,
10
+ getCurrentSchema,
11
+ loadConfig
12
+ } = require('../lib/unified-api')
13
+ const { version } = require('../package.json')
14
+ const fixturesDir = join(__dirname, '..', 'fixtures')
15
+
16
+ test('getConfigType()', async (t) => {
17
+ await t.test('throws if there is no $schema', async () => {
18
+ const configFile = join(fixturesDir, 'configs', 'no-schema.config.json')
19
+ let err
20
+
21
+ try {
22
+ await getConfigType(['-c', configFile])
23
+ } catch (error) {
24
+ err = error
25
+ }
26
+
27
+ assert(err)
28
+ assert.strictEqual(err.cause.message, 'configuration is missing a schema')
29
+ })
30
+
31
+ await t.test('throws if the schema type is unsupported', async () => {
32
+ const configFile = join(fixturesDir, 'configs', 'invalid-schema-type.config.json')
33
+ let err
34
+
35
+ try {
36
+ await getConfigType(['-c', configFile])
37
+ } catch (error) {
38
+ err = error
39
+ }
40
+
41
+ assert(err)
42
+ assert.strictEqual(err.cause.message, 'unknown configuration type: \'trickortreat\'')
43
+ })
44
+
45
+ await t.test('gets type from config via args', async () => {
46
+ const configFile = join(fixturesDir, 'monorepo', 'serviceApp', 'platformatic.service.json')
47
+ const type = await getConfigType(['-c', configFile])
48
+
49
+ assert.strictEqual(type, 'service')
50
+ })
51
+
52
+ await t.test('gets type from config in provided directory', async () => {
53
+ const configDir = join(fixturesDir, 'monorepo', 'serviceApp')
54
+ const type = await getConfigType(undefined, configDir)
55
+
56
+ assert.strictEqual(type, 'service')
57
+ })
58
+
59
+ await t.test('gets db type from config in cwd', async (t) => {
60
+ const cwd = process.cwd()
61
+
62
+ t.after(() => {
63
+ process.chdir(cwd)
64
+ })
65
+
66
+ const configDir = join(fixturesDir, 'dbApp')
67
+ process.chdir(configDir)
68
+ const type = await getConfigType()
69
+
70
+ assert.strictEqual(type, 'db')
71
+ })
72
+
73
+ await t.test('gets composer type from config in cwd', async (t) => {
74
+ const cwd = process.cwd()
75
+
76
+ t.after(() => {
77
+ process.chdir(cwd)
78
+ })
79
+
80
+ const configDir = join(fixturesDir, 'monorepo', 'composerApp')
81
+ process.chdir(configDir)
82
+ const type = await getConfigType()
83
+
84
+ assert.strictEqual(type, 'composer')
85
+ })
86
+ })
87
+
88
+ test('getCurrentSchema()', async (t) => {
89
+ await t.test('gets service schema', async () => {
90
+ const schema = await getCurrentSchema('service')
91
+
92
+ assert(schema.$id.endsWith(`/v${version}/service`))
93
+ })
94
+
95
+ await t.test('gets db schema', async () => {
96
+ const schema = await getCurrentSchema('db')
97
+
98
+ assert(schema.$id.endsWith(`/v${version}/db`))
99
+ })
100
+
101
+ await t.test('gets composer schema', async () => {
102
+ const schema = await getCurrentSchema('composer')
103
+
104
+ assert(schema.$id.endsWith(`/v${version}/composer`))
105
+ })
106
+
107
+ await t.test('gets runtime schema', async () => {
108
+ const schema = await getCurrentSchema('runtime')
109
+
110
+ assert(schema.$id.endsWith(`/v${version}/runtime`))
111
+ })
112
+
113
+ await t.test('throws for unknown types', async () => {
114
+ await assert.rejects(async () => {
115
+ await getCurrentSchema('not-a-real-type')
116
+ }, /unknown configuration type/)
117
+ })
118
+ })
119
+
120
+ test('loadConfig()', async (t) => {
121
+ await t.test('can explicitly provide config type', async () => {
122
+ const configFile = join(fixturesDir, 'monorepo', 'serviceAppWithLogger', 'platformatic.service.json')
123
+ const config = await loadConfig({}, ['-c', configFile], undefined, 'service')
124
+
125
+ assert.strictEqual(config.args.config, configFile)
126
+ assert.strictEqual(config.configManager.fullPath, configFile)
127
+ assert.strictEqual(config.configManager.current.server.logger.name, 'service-with-logger')
128
+ assert.strictEqual(config.configManager.schemaOptions.useDefaults, true)
129
+ })
130
+
131
+ await t.test('throws if explicit type is wrong', async () => {
132
+ // Prevent the failed validation from logging and exiting the process.
133
+ t.mock.method(process, 'exit', () => {})
134
+ t.mock.method(console, 'table', () => {})
135
+
136
+ const configFile = join(fixturesDir, 'monorepo', 'serviceAppWithLogger', 'platformatic.service.json')
137
+
138
+ await assert.rejects(async () => {
139
+ await loadConfig({}, ['-c', configFile], 'kaboom')
140
+ })
141
+ })
142
+
143
+ await t.test('can load a platformatic service project', async () => {
144
+ const configFile = join(fixturesDir, 'monorepo', 'serviceAppWithLogger', 'platformatic.service.json')
145
+ const config = await loadConfig({}, ['-c', configFile])
146
+
147
+ assert.strictEqual(config.args.config, configFile)
148
+ assert.strictEqual(config.configManager.fullPath, configFile)
149
+ assert.strictEqual(config.configManager.current.server.logger.name, 'service-with-logger')
150
+ assert.strictEqual(config.configManager.schemaOptions.useDefaults, true)
151
+ })
152
+
153
+ await t.test('can load a platformatic db project', async () => {
154
+ const configFile = join(fixturesDir, 'dbApp', 'platformatic.db.json')
155
+ const config = await loadConfig({}, ['-c', configFile])
156
+
157
+ assert.strictEqual(config.args.config, configFile)
158
+ assert.strictEqual(config.configManager.fullPath, configFile)
159
+ assert.strictEqual(config.configManager.current.db.graphql, true)
160
+ })
161
+
162
+ await t.test('can load a platformatic composer project', async () => {
163
+ const configFile = join(fixturesDir, 'composerApp', 'platformatic.composer.json')
164
+ const config = await loadConfig({}, ['-c', configFile])
165
+
166
+ assert.strictEqual(config.args.config, configFile)
167
+ assert.strictEqual(config.configManager.fullPath, configFile)
168
+ assert.strictEqual(config.configManager.current.composer.refreshTimeout, 1000)
169
+ })
170
+
171
+ await t.test('can load a platformatic runtime project', async () => {
172
+ const configFile = join(fixturesDir, 'configs', 'monorepo.json')
173
+ const config = await loadConfig({}, ['-c', configFile])
174
+
175
+ assert.strictEqual(config.args.config, configFile)
176
+ assert.strictEqual(config.configManager.fullPath, configFile)
177
+ assert.strictEqual(config.configManager.current.entrypoint, 'serviceApp')
178
+ })
179
+ })
180
+
181
+ test('buildServer()', async (t) => {
182
+ await t.test('can build a service server', async (t) => {
183
+ const configFile = join(fixturesDir, 'monorepo', 'serviceAppWithLogger', 'platformatic.service.json')
184
+ const config = await loadConfig({}, ['-c', configFile])
185
+ const server = await buildServer(config.configManager.current)
186
+
187
+ t.after(async () => {
188
+ await server.close()
189
+ })
190
+
191
+ const address = await server.start()
192
+ // The address should be a valid URL.
193
+ new URL(address) // eslint-disable-line no-new
194
+ })
195
+
196
+ await t.test('can build a db server', async (t) => {
197
+ const configFile = join(fixturesDir, 'dbApp', 'platformatic.db.json')
198
+ const config = await loadConfig({}, ['-c', configFile])
199
+ const server = await buildServer(config.configManager.current)
200
+
201
+ t.after(async () => {
202
+ await server.close()
203
+ })
204
+
205
+ const address = await server.start()
206
+ // The address should be a valid URL.
207
+ new URL(address) // eslint-disable-line no-new
208
+ })
209
+
210
+ await t.test('can build a composer server', async (t) => {
211
+ const configFile = join(fixturesDir, 'composerApp', 'platformatic.composer.json')
212
+ const config = await loadConfig({}, ['-c', configFile])
213
+ const server = await buildServer(config.configManager.current)
214
+
215
+ t.after(async () => {
216
+ await server.close()
217
+ })
218
+
219
+ const address = await server.start()
220
+ // The address should be a valid URL.
221
+ new URL(address) // eslint-disable-line no-new
222
+ })
223
+
224
+ await t.test('can build a runtime application', async (t) => {
225
+ const configFile = join(fixturesDir, 'configs', 'monorepo.json')
226
+ const config = await loadConfig({}, ['-c', configFile])
227
+ const server = await buildServer(config.configManager.current)
228
+
229
+ t.after(async () => {
230
+ await server.close()
231
+ })
232
+
233
+ const address = await server.start()
234
+ // The address should be a valid URL.
235
+ new URL(address) // eslint-disable-line no-new
236
+ })
237
+
238
+ await t.test('input can be a filename', async (t) => {
239
+ const configFile = join(fixturesDir, 'monorepo', 'serviceAppWithLogger', 'platformatic.service.json')
240
+ const server = await buildServer(configFile)
241
+
242
+ t.after(async () => {
243
+ await server.close()
244
+ })
245
+
246
+ const address = await server.start()
247
+ // The address should be a valid URL.
248
+ new URL(address) // eslint-disable-line no-new
249
+ })
250
+ })
251
+
252
+ test('start()', async (t) => {
253
+ await t.test('can start a service server', async (t) => {
254
+ const scriptFile = join(fixturesDir, 'starter.js')
255
+ const configFile = join(fixturesDir, 'monorepo', 'serviceAppWithLogger', 'platformatic.service.json')
256
+ const child = spawn(process.execPath, [scriptFile, configFile])
257
+ child.stdout.pipe(process.stdout)
258
+ child.stderr.pipe(process.stderr)
259
+ const [exitCode] = await once(child, 'exit')
260
+
261
+ assert.strictEqual(exitCode, 42)
262
+ })
263
+
264
+ await t.test('can start a db server', async (t) => {
265
+ const scriptFile = join(fixturesDir, 'starter.js')
266
+ const configFile = join(fixturesDir, 'dbApp', 'platformatic.db.json')
267
+ const child = spawn(process.execPath, [scriptFile, configFile])
268
+ const [exitCode] = await once(child, 'exit')
269
+
270
+ assert.strictEqual(exitCode, 42)
271
+ })
272
+
273
+ await t.test('can start a composer server', async () => {
274
+ const scriptFile = join(fixturesDir, 'starter.js')
275
+ const configFile = join(fixturesDir, 'composerApp', 'platformatic.composer.json')
276
+ const child = spawn(process.execPath, [scriptFile, configFile])
277
+ const [exitCode] = await once(child, 'exit')
278
+
279
+ assert.strictEqual(exitCode, 42)
280
+ })
281
+
282
+ await t.test('can start a runtime application', async () => {
283
+ const scriptFile = join(fixturesDir, 'starter.js')
284
+ const configFile = join(fixturesDir, 'configs', 'monorepo.json')
285
+ const child = spawn(process.execPath, [scriptFile, configFile])
286
+ const [exitCode] = await once(child, 'exit')
287
+
288
+ assert.strictEqual(exitCode, 42)
289
+ })
290
+ })
291
+
292
+ test('startCommand()', async (t) => {
293
+ await t.test('can start a server', async (t) => {
294
+ const scriptFile = join(fixturesDir, 'start-command.js')
295
+ const configFile = join(fixturesDir, 'monorepo', 'serviceAppWithLogger', 'platformatic.service.json')
296
+ const child = spawn(process.execPath, [scriptFile, configFile])
297
+ child.stderr.pipe(process.stderr)
298
+ const [exitCode] = await once(child, 'exit')
299
+
300
+ assert.strictEqual(exitCode, 42)
301
+ })
302
+
303
+ await t.test('exits on error', async (t) => {
304
+ const scriptFile = join(fixturesDir, 'start-command.js')
305
+ const configFile = join(fixturesDir, 'serviceApp', 'platformatic.not-found.json')
306
+ const child = spawn(process.execPath, [scriptFile, configFile])
307
+ const [exitCode] = await once(child, 'exit')
308
+
309
+ assert.strictEqual(exitCode, 1)
310
+ })
311
+
312
+ await t.test('can start a runtime application', async (t) => {
313
+ const scriptFile = join(fixturesDir, 'start-command.js')
314
+ const configFile = join(fixturesDir, 'configs', 'monorepo.json')
315
+ const child = spawn(process.execPath, [scriptFile, configFile])
316
+ child.stderr.pipe(process.stderr)
317
+ const [exitCode] = await once(child, 'exit')
318
+
319
+ assert.strictEqual(exitCode, 42)
320
+ })
321
+ })