@platformatic/runtime 0.31.0 → 0.32.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @platformatic/runtime
2
2
 
3
- Check out the full documentation for Platformatic Runtime on [our website](https://oss.platformatic.dev/docs/getting-started/quick-start-guide).
3
+ Check out the full documentation for Platformatic Runtime on [our website](https://docs.platformatic.dev/docs/getting-started/quick-start-guide).
4
4
 
5
5
  ## Install
6
6
 
@@ -0,0 +1,4 @@
1
+ CREATE TABLE IF NOT EXISTS movies (
2
+ id INTEGER PRIMARY KEY,
3
+ title TEXT NOT NULL fiddlesticks
4
+ );
@@ -0,0 +1 @@
1
+ DROP TABLE movies;
@@ -0,0 +1,28 @@
1
+ {
2
+ "$schema": "https://platformatic.dev/schemas/v0.30.0/db",
3
+ "server": {
4
+ "hostname": "127.0.0.1",
5
+ "port": 3042
6
+ },
7
+ "migrations": {
8
+ "autoApply": true,
9
+ "dir": "migrations",
10
+ "table": "versions"
11
+ },
12
+ "types": {
13
+ "autogenerate": false
14
+ },
15
+ "plugins": {
16
+ "paths": [
17
+ "plugin.js"
18
+ ]
19
+ },
20
+ "db": {
21
+ "connectionString": "sqlite://db.sqlite",
22
+ "graphql": true,
23
+ "ignore": {
24
+ "versions": true
25
+ },
26
+ "events": false
27
+ }
28
+ }
@@ -0,0 +1,5 @@
1
+ /// <reference path="./global.d.ts" />
2
+ 'use strict'
3
+
4
+ /** @param {import('fastify').FastifyInstance} app */
5
+ module.exports = async function (app) {}
@@ -0,0 +1,4 @@
1
+ PLT_SERVER_HOSTNAME=127.0.0.1
2
+ PORT=3043
3
+ PLT_SERVER_LOGGER_LEVEL=info
4
+ PLT_EXAMPLE=world
@@ -0,0 +1,12 @@
1
+ {
2
+ "$schema": "https://platformatic.dev/schemas/v0.31.0/runtime",
3
+ "entrypoint": "rainy-empire",
4
+ "allowCycles": false,
5
+ "hotReload": true,
6
+ "autoload": {
7
+ "path": "services",
8
+ "exclude": [
9
+ "docs"
10
+ ]
11
+ }
12
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "$schema": "https://platformatic.dev/schemas/v0.31.0/service",
3
+ "server": {
4
+ "hostname": "{PLT_SERVER_HOSTNAME}",
5
+ "port": "{PORT}",
6
+ "logger": {
7
+ "level": "{PLT_SERVER_LOGGER_LEVEL}"
8
+ }
9
+ },
10
+ "service": {
11
+ "openapi": true
12
+ },
13
+ "plugins": {
14
+ "paths": [
15
+ {
16
+ "path": "./plugins",
17
+ "encapsulate": false,
18
+ "options": {
19
+ "example": "{PLT_EXAMPLE}"
20
+ }
21
+ },
22
+ "./routes"
23
+ ]
24
+ }
25
+ }
@@ -0,0 +1,6 @@
1
+ /// <reference types="@platformatic/service" />
2
+ 'use strict'
3
+ /** @param {import('fastify').FastifyInstance} fastify */
4
+ module.exports = async function (fastify, opts) {
5
+ fastify.decorate('example', 'foobar')
6
+ }
@@ -0,0 +1,8 @@
1
+ /// <reference types="@platformatic/service" />
2
+ 'use strict'
3
+ /** @param {import('fastify').FastifyInstance} fastify */
4
+ module.exports = async function (fastify, opts) {
5
+ fastify.get('/', async (request, reply) => {
6
+ return { hello: fastify.example }
7
+ })
8
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://platformatic.dev/schemas/v0.31.0/composer",
3
+ "server": {
4
+ "hostname": "{PLT_SERVER_HOSTNAME}",
5
+ "port": "{PORT}",
6
+ "logger": {
7
+ "level": "{PLT_SERVER_LOGGER_LEVEL}"
8
+ }
9
+ },
10
+ "composer": {
11
+ "services": [
12
+ {
13
+ "id": "deeply-splitte",
14
+ "openapi": {
15
+ "url": "/documentation/json"
16
+ }
17
+ }
18
+ ]
19
+ },
20
+ "watch": false
21
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "$schema": "https://platformatic.dev/schemas/v0.31.0/service",
3
+ "server": {
4
+ "hostname": "{PLT_SERVER_HOSTNAME}",
5
+ "port": "{PORT}",
6
+ "logger": {
7
+ "level": "{PLT_SERVER_LOGGER_LEVEL}"
8
+ }
9
+ },
10
+ "service": {
11
+ "openapi": true
12
+ },
13
+ "plugins": {
14
+ "paths": [
15
+ {
16
+ "path": "./plugins",
17
+ "encapsulate": false,
18
+ "options": {
19
+ "example": "{PLT_EXAMPLE}"
20
+ }
21
+ },
22
+ "./routes"
23
+ ]
24
+ }
25
+ }
package/lib/app.js CHANGED
@@ -24,7 +24,9 @@ class PlatformaticApp {
24
24
  this.server = null
25
25
  this.#started = false
26
26
  this.#originalWatch = null
27
- this.#logger = logger
27
+ this.#logger = logger.child({
28
+ name: this.appConfig.id
29
+ })
28
30
  }
29
31
 
30
32
  getStatus () {
@@ -148,12 +150,19 @@ class PlatformaticApp {
148
150
  async #initializeConfig () {
149
151
  const appConfig = this.appConfig
150
152
 
151
- this.config = await loadConfig({}, ['-c', appConfig.config], null, {
152
- watch: true,
153
- onMissingEnv (key) {
154
- return appConfig.localServiceEnvVars.get(key)
155
- }
156
- })
153
+ let _config
154
+ try {
155
+ _config = await loadConfig({}, ['-c', appConfig.config], null, {
156
+ watch: true,
157
+ onMissingEnv (key) {
158
+ return appConfig.localServiceEnvVars.get(key)
159
+ }
160
+ })
161
+ } catch (err) {
162
+ this.#logAndExit(err)
163
+ }
164
+
165
+ this.config = _config
157
166
  const { configManager } = this.config
158
167
 
159
168
  function applyOverrides () {
@@ -206,9 +215,7 @@ class PlatformaticApp {
206
215
  #setuplogger (configManager) {
207
216
  // Set the logger if not present (and the config supports it).
208
217
  if (configManager.current.server) {
209
- const childLogger = this.#logger.child({
210
- name: this.appConfig.id
211
- }, { level: configManager.current.server.logger?.level || 'info' })
218
+ const childLogger = this.#logger.child({}, { level: configManager.current.server.logger?.level || 'info' })
212
219
  configManager.current.server.logger = childLogger
213
220
  }
214
221
  }
package/lib/config.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
  const { readFile, readdir } = require('node:fs/promises')
3
3
  const { basename, join, resolve: pathResolve } = require('node:path')
4
+ const { closest } = require('fastest-levenshtein')
4
5
  const Topo = require('@hapi/topo')
5
6
  const ConfigManager = require('@platformatic/config')
6
7
  const { schema } = require('./schema')
@@ -71,6 +72,15 @@ async function _transformConfig (configManager) {
71
72
  }
72
73
  }
73
74
 
75
+ function missingDependencyErrorMessage (clientName, service, configManager) {
76
+ const closestName = closest(clientName, [...configManager.current.serviceMap.keys()])
77
+ let errorMsg = `service '${service.id}' has unknown dependency: '${clientName}'.`
78
+ if (closestName) {
79
+ errorMsg += ` Did you mean '${closestName}'?`
80
+ }
81
+ return errorMsg
82
+ }
83
+
74
84
  async function parseClientsAndComposer (configManager) {
75
85
  for (let i = 0; i < configManager.current.services.length; ++i) {
76
86
  const service = configManager.current.services[i]
@@ -86,8 +96,7 @@ async function parseClientsAndComposer (configManager) {
86
96
  const dependency = configManager.current.serviceMap.get(clientName)
87
97
 
88
98
  if (dependency === undefined) {
89
- /* c8 ignore next 2 */
90
- throw new Error(`service '${service.id}' has unknown dependency: '${clientName}'`)
99
+ throw new Error(missingDependencyErrorMessage(clientName, service, configManager))
91
100
  }
92
101
 
93
102
  dependency.dependents.push(service.id)
@@ -170,7 +179,7 @@ async function parseClientsAndComposer (configManager) {
170
179
 
171
180
  /* c8 ignore next 4 */
172
181
  if (dependency === undefined) {
173
- reject(new Error(`service '${service.id}' has unknown dependency: '${clientName}'`))
182
+ reject(new Error(missingDependencyErrorMessage(clientName, service, configManager)))
174
183
  return
175
184
  }
176
185
 
@@ -11,6 +11,14 @@ class MessagePortWritable extends Writable {
11
11
  this.metadata = opts.metadata
12
12
  }
13
13
 
14
+ _write (chunk, encoding, callback) {
15
+ this.port.postMessage({
16
+ metadata: this.metadata,
17
+ logs: [chunk.toString().trim()]
18
+ })
19
+ process.nextTick(callback)
20
+ }
21
+
14
22
  _writev (chunks, callback) {
15
23
  // Process the logs here before trying to send them across the thread
16
24
  // boundary. Sometimes the chunks have an undocumented method on them
package/lib/schema.js CHANGED
@@ -67,7 +67,14 @@ const platformaticRuntimeSchema = {
67
67
  type: 'string'
68
68
  },
69
69
  hotReload: {
70
- type: 'boolean'
70
+ anyOf: [
71
+ {
72
+ type: 'boolean'
73
+ },
74
+ {
75
+ type: 'string'
76
+ }
77
+ ]
71
78
  },
72
79
  allowCycles: {
73
80
  type: 'boolean'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/runtime",
3
- "version": "0.31.0",
3
+ "version": "0.32.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -24,8 +24,8 @@
24
24
  "standard": "^17.1.0",
25
25
  "tsd": "^0.28.1",
26
26
  "typescript": "^5.1.6",
27
- "@platformatic/sql-mapper": "0.31.0",
28
- "@platformatic/sql-graphql": "0.31.0"
27
+ "@platformatic/sql-graphql": "0.32.0",
28
+ "@platformatic/sql-mapper": "0.32.0"
29
29
  },
30
30
  "dependencies": {
31
31
  "@hapi/topo": "^6.0.2",
@@ -33,6 +33,7 @@
33
33
  "commist": "^3.2.0",
34
34
  "desm": "^1.3.0",
35
35
  "es-main": "^1.2.0",
36
+ "fastest-levenshtein": "^1.0.16",
36
37
  "fastify": "^4.18.0",
37
38
  "fastify-undici-dispatcher": "^0.4.1",
38
39
  "help-me": "^4.2.0",
@@ -40,11 +41,11 @@
40
41
  "pino": "^8.14.1",
41
42
  "pino-pretty": "^10.0.0",
42
43
  "undici": "^5.22.1",
43
- "@platformatic/composer": "0.31.0",
44
- "@platformatic/config": "0.31.0",
45
- "@platformatic/db": "0.31.0",
46
- "@platformatic/service": "0.31.0",
47
- "@platformatic/utils": "0.31.0"
44
+ "@platformatic/composer": "0.32.0",
45
+ "@platformatic/config": "0.32.0",
46
+ "@platformatic/db": "0.32.0",
47
+ "@platformatic/service": "0.32.0",
48
+ "@platformatic/utils": "0.32.0"
48
49
  },
49
50
  "standard": {
50
51
  "ignore": [
@@ -53,7 +54,8 @@
53
54
  ]
54
55
  },
55
56
  "scripts": {
56
- "test": "npm run lint && c8 -x fixtures -x test node --test && tsd",
57
+ "test": "npm run lint && node --test && tsd",
58
+ "coverage": "npm run lint && c8 -x fixtures -x test node --test && tsd",
57
59
  "lint": "standard | snazzy"
58
60
  }
59
61
  }
package/test/app.test.js CHANGED
@@ -308,3 +308,35 @@ test('restarts on config change without overriding the configManager', async (t)
308
308
  }
309
309
  assert.strictEqual(configManager, app.server.platformatic.configManager)
310
310
  })
311
+
312
+ test('logs errors if an env variable is missing', async (t) => {
313
+ const { logger, stream } = getLoggerAndStream()
314
+ const configFile = join(fixturesDir, 'no-env.service.json')
315
+ const config = {
316
+ id: 'no-env',
317
+ config: configFile,
318
+ path: fixturesDir,
319
+ entrypoint: true,
320
+ hotReload: true
321
+ }
322
+ const app = new PlatformaticApp(config, null, logger)
323
+
324
+ t.mock.method(process, 'exit', () => {
325
+ throw new Error('exited')
326
+ })
327
+
328
+ await assert.rejects(async () => {
329
+ await app.start()
330
+ }, /exited/)
331
+ assert.strictEqual(process.exit.mock.calls.length, 1)
332
+ assert.strictEqual(process.exit.mock.calls[0].arguments[0], 1)
333
+
334
+ stream.end()
335
+ const lines = []
336
+ for await (const line of stream) {
337
+ lines.push(line)
338
+ }
339
+ const lastLine = lines[lines.length - 1]
340
+ assert.strictEqual(lastLine.name, 'no-env')
341
+ assert.strictEqual(lastLine.msg, 'Cannot parse config file. Cannot read properties of undefined (reading \'get\')')
342
+ })
@@ -0,0 +1,69 @@
1
+ import { test } from 'node:test'
2
+ import assert from 'node:assert'
3
+ import { join } from 'desm'
4
+ import path from 'node:path'
5
+ import { cliPath, delDir } from './helper.mjs'
6
+ import { execa } from 'execa'
7
+ import { mkdtemp, cp, mkdir } from 'node:fs/promises'
8
+
9
+ const base = join(import.meta.url, '..', 'tmp')
10
+
11
+ try {
12
+ await mkdir(base, { recursive: true })
13
+ } catch {
14
+ }
15
+
16
+ test('compile with tsconfig custom flags', async (t) => {
17
+ const tmpDir = await mkdtemp(path.join(base, 'test-runtime-compile-'))
18
+ const prev = process.cwd()
19
+ process.chdir(tmpDir)
20
+ t.after(() => {
21
+ process.chdir(prev)
22
+ })
23
+
24
+ t.after(delDir(tmpDir))
25
+
26
+ const folder = join(import.meta.url, '..', '..', 'fixtures', 'typescript-custom-flags')
27
+ await cp(folder, tmpDir, { recursive: true })
28
+
29
+ const { stdout } = await execa(cliPath, ['compile'])
30
+
31
+ const lines = stdout.split('\n').map(JSON.parse)
32
+ const expected = [{
33
+ name: 'movies',
34
+ msg: 'Typescript compilation completed successfully.'
35
+ }, {
36
+ name: 'titles',
37
+ msg: 'Typescript compilation completed successfully.'
38
+ }]
39
+
40
+ for (let i = 0; i < expected.length; i++) {
41
+ assert.deepStrictEqual(lines[i].name, expected[i].name)
42
+ assert.deepStrictEqual(lines[i].msg, expected[i].msg)
43
+ }
44
+ })
45
+
46
+ test('compile single service', async (t) => {
47
+ const tmpDir = await mkdtemp(path.join(base, 'test-runtime-compile-'))
48
+ const prev = process.cwd()
49
+ process.chdir(tmpDir)
50
+ t.after(() => {
51
+ process.chdir(prev)
52
+ })
53
+
54
+ t.after(delDir(tmpDir))
55
+
56
+ const folder = join(import.meta.url, '..', '..', 'fixtures', 'typescript', 'services', 'movies')
57
+ await cp(folder, tmpDir, { recursive: true })
58
+
59
+ const { stdout } = await execa(cliPath, ['compile'])
60
+
61
+ const lines = stdout.split('\n').map(JSON.parse)
62
+ const expected = [{
63
+ msg: 'Typescript compilation completed successfully.'
64
+ }]
65
+
66
+ for (let i = 0; i < expected.length; i++) {
67
+ assert.deepStrictEqual(lines[i].msg, expected[i].msg)
68
+ }
69
+ })
@@ -2,10 +2,9 @@ import { test } from 'node:test'
2
2
  import assert from 'node:assert'
3
3
  import { join } from 'desm'
4
4
  import path from 'node:path'
5
- import { cliPath } from './helper.mjs'
5
+ import { cliPath, delDir } from './helper.mjs'
6
6
  import { execa } from 'execa'
7
- import { mkdtemp, rm, cp, mkdir } from 'node:fs/promises'
8
- import { setTimeout as sleep } from 'node:timers/promises'
7
+ import { mkdtemp, cp, mkdir } from 'node:fs/promises'
9
8
 
10
9
  const base = join(import.meta.url, '..', 'tmp')
11
10
 
@@ -14,25 +13,6 @@ try {
14
13
  } catch {
15
14
  }
16
15
 
17
- function delDir (tmpDir) {
18
- return async function () {
19
- // We give up after 10s.
20
- // This is because on Windows, it's very hard to delete files if the file
21
- // system is not collaborating.
22
- for (let i = 0; i < 10; i++) {
23
- try {
24
- await rm(tmpDir, { recursive: true, force: true })
25
- break
26
- } catch (err) {
27
- if (err.code === 'EBUSY') {
28
- await sleep(1000)
29
- continue
30
- }
31
- }
32
- }
33
- }
34
- }
35
-
36
16
  test('compile without tsconfigs', async () => {
37
17
  const config = join(import.meta.url, '..', '..', 'fixtures', 'configs', 'monorepo.json')
38
18
  await execa(cliPath, ['compile', '-c', config])
@@ -3,6 +3,7 @@ import { Agent, setGlobalDispatcher } from 'undici'
3
3
  import { execa } from 'execa'
4
4
  import split from 'split2'
5
5
  import { join } from 'desm'
6
+ import { rm } from 'node:fs/promises'
6
7
 
7
8
  setGlobalDispatcher(new Agent({
8
9
  keepAliveTimeout: 10,
@@ -44,3 +45,22 @@ export async function start (...args) {
44
45
  }
45
46
  }
46
47
  }
48
+
49
+ export function delDir (tmpDir) {
50
+ return async function () {
51
+ console.time('delDir')
52
+ // We give up after 10s.
53
+ // This is because on Windows, it's very hard to delete files if the file
54
+ // system is not collaborating.
55
+ try {
56
+ await rm(tmpDir, { recursive: true, force: true })
57
+ } catch (err) {
58
+ if (err.code !== 'EBUSY') {
59
+ throw err
60
+ } else {
61
+ console.log('Could not delete directory, retrying', tmpDir)
62
+ }
63
+ }
64
+ console.timeEnd('delDir')
65
+ }
66
+ }
@@ -55,6 +55,14 @@ test('performs a topological sort on services depending on allowCycles', async (
55
55
  await loadConfig({}, ['-c', configFile], platformaticRuntime)
56
56
  })
57
57
  })
58
+
59
+ await t.test('throws by adding the most probable service ', async () => {
60
+ const configFile = join(fixturesDir, 'leven', 'platformatic.runtime.json')
61
+
62
+ await assert.rejects(async () => {
63
+ await loadConfig({}, ['-c', configFile], platformaticRuntime)
64
+ }, 'service \'rainy-empire\' has unordered dependency: \'deeply-splitte\'. Did you mean \'deeply-spittle\'?')
65
+ })
58
66
  })
59
67
 
60
68
  test('can resolve service id from client package.json if not provided', async () => {
@@ -8,6 +8,7 @@ const { MessageChannel } = require('node:worker_threads')
8
8
  const { request } = require('undici')
9
9
  const { loadConfig } = require('@platformatic/service')
10
10
  const { buildServer, platformaticRuntime } = require('..')
11
+ const { wrapConfigInRuntimeConfig } = require('../lib/config')
11
12
  const { startWithConfig } = require('../lib/start')
12
13
  const fixturesDir = join(__dirname, '..', 'fixtures')
13
14
 
@@ -67,7 +68,7 @@ test('composer', async (t) => {
67
68
  const res = await request(entryUrl)
68
69
 
69
70
  assert.strictEqual(res.statusCode, 200)
70
- assert.deepStrictEqual(await res.body.json(), { message: 'Welcome to Platformatic! Please visit https://oss.platformatic.dev' })
71
+ assert.deepStrictEqual(await res.body.json(), { message: 'Welcome to Platformatic! Please visit https://docs.platformatic.dev' })
71
72
  }
72
73
 
73
74
  {
@@ -156,3 +157,30 @@ test('handles uncaught exceptions with db app', async (t) => {
156
157
 
157
158
  assert.strictEqual(exitCode, 42)
158
159
  })
160
+
161
+ test('logs errors during db migrations', async (t) => {
162
+ const configFile = join(fixturesDir, 'dbAppWithMigrationError', 'platformatic.db.json')
163
+ const config = await loadConfig({}, ['-c', configFile], 'db')
164
+ const runtimeConfig = await wrapConfigInRuntimeConfig(config)
165
+ const { port1, port2 } = new MessageChannel()
166
+ runtimeConfig.current.loggingPort = port2
167
+ runtimeConfig.current.loggingMetadata = { foo: 1, bar: 2 }
168
+ const runtime = await startWithConfig(runtimeConfig)
169
+ const messages = []
170
+
171
+ port1.on('message', (msg) => {
172
+ messages.push(msg)
173
+ })
174
+
175
+ await assert.rejects(async () => {
176
+ await runtime.start()
177
+ }, /The runtime exited before the operation completed/)
178
+
179
+ assert.strictEqual(messages.length, 2)
180
+ assert.deepStrictEqual(messages[0].metadata, runtimeConfig.current.loggingMetadata)
181
+ assert.strictEqual(messages[0].logs.length, 1)
182
+ assert.match(messages[0].logs[0], /running 001.do.sql/)
183
+ assert.deepStrictEqual(messages[1].metadata, runtimeConfig.current.loggingMetadata)
184
+ assert.strictEqual(messages[1].logs.length, 1)
185
+ assert.match(messages[1].logs[0], /near \\"fiddlesticks\\": syntax error/)
186
+ })