@platformatic/runtime 0.31.1 → 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
+ 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
 
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.1",
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.1",
28
- "@platformatic/sql-graphql": "0.31.1"
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.1",
44
- "@platformatic/config": "0.31.1",
45
- "@platformatic/db": "0.31.1",
46
- "@platformatic/service": "0.31.1",
47
- "@platformatic/utils": "0.31.1"
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 () => {
@@ -68,7 +68,7 @@ test('composer', async (t) => {
68
68
  const res = await request(entryUrl)
69
69
 
70
70
  assert.strictEqual(res.statusCode, 200)
71
- 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' })
72
72
  }
73
73
 
74
74
  {