@platformatic/service 0.33.1 → 0.34.1

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/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  'use strict'
2
2
 
3
3
  const { isKeyEnabled } = require('@platformatic/utils')
4
+ const { readFile } = require('fs/promises')
5
+ const { dirname, join } = require('path')
4
6
 
5
7
  const compiler = require('./lib/compile')
6
8
  const setupCors = require('./lib/plugins/cors')
@@ -65,13 +67,6 @@ async function platformaticService (app, opts, toLoad = []) {
65
67
  await app.register(loadPlugins)
66
68
  }
67
69
 
68
- if (isKeyEnabled('watch', config)) {
69
- // If file watching is enabled here, that means the service was started
70
- // without the runtime because the runtime explicitly disables watching on
71
- // services that it starts. Warn the user that things will not go as planned.
72
- app.log.warn('service was started with file watching enabled but watching is only available via the runtime')
73
- }
74
-
75
70
  if (config.server.cors) {
76
71
  app.register(setupCors, config.server.cors)
77
72
  }
@@ -98,11 +93,32 @@ platformaticService.configManagerConfig = {
98
93
  allErrors: true,
99
94
  strict: false
100
95
  },
101
- transformConfig () {
96
+ async transformConfig () {
102
97
  // Set watch to true by default. This is not possible
103
98
  // to do in the schema, because it is uses an anyOf.
104
99
  if (this.current.watch === undefined) {
105
- this.current.watch = true
100
+ this.current.watch = { enabled: false }
101
+ }
102
+
103
+ if (typeof this.current.watch !== 'object') {
104
+ this.current.watch = { enabled: this.current.watch || false }
105
+ }
106
+
107
+ const typescript = this.current.plugins?.typescript
108
+ if (typescript) {
109
+ let outDir = typescript.outDir
110
+ if (outDir === undefined) {
111
+ let tsConfigFile = typescript.tsConfigFile || 'tsconfig.json'
112
+ tsConfigFile = join(dirname(this.fullPath), tsConfigFile)
113
+ try {
114
+ const tsConfig = JSON.parse(await readFile(tsConfigFile, 'utf8'))
115
+ outDir = tsConfig.compilerOptions.outDir
116
+ } catch {}
117
+ outDir ||= 'dist'
118
+ }
119
+
120
+ this.current.watch.ignore ||= []
121
+ this.current.watch.ignore.push(outDir + '/**/*')
106
122
  }
107
123
  }
108
124
  }
@@ -10,11 +10,21 @@ module.exports = fp(async function (app, opts) {
10
10
  plugin = { path: plugin, encapsulate: true }
11
11
  }
12
12
  if ((await stat(plugin.path)).isDirectory()) {
13
+ const patternOptions = patternOptionsFromPlugin(plugin)
14
+
13
15
  app.register(autoload, {
14
16
  dir: plugin.path,
15
17
  encapsulate: plugin.encapsulate !== false,
16
18
  maxDepth: plugin.maxDepth,
17
- options: plugin.options
19
+ options: plugin.options,
20
+ autoHooks: plugin.autoHooks,
21
+ cascadeHooks: plugin.cascadeHooks,
22
+ overwriteHooks: plugin.overwriteHooks,
23
+ routeParams: plugin.routeParams,
24
+ forceESM: plugin.forceESM,
25
+ ignoreFilter: plugin.ignoreFilter,
26
+ matchFilter: plugin.matchFilter,
27
+ ...patternOptions
18
28
  })
19
29
  } else {
20
30
  let loaded = await import(`file://${plugin.path}`)
@@ -23,13 +33,58 @@ module.exports = fp(async function (app, opts) {
23
33
  loaded = loaded.default
24
34
  }
25
35
 
26
- let skipOverride
27
36
  if (plugin.encapsulate === false) {
28
- skipOverride = loaded[Symbol.for('skip-override')]
37
+ const skipOverride = loaded[Symbol.for('skip-override')]
29
38
  loaded[Symbol.for('skip-override')] = true
39
+ await app.register(loaded, plugin.options)
40
+ loaded[Symbol.for('skip-override')] = skipOverride
41
+ } else {
42
+ await app.register(loaded, plugin.options)
30
43
  }
31
- await app.register(loaded, plugin.options)
32
- loaded[Symbol.for('skip-override')] = skipOverride
33
44
  }
34
45
  }
35
46
  })
47
+
48
+ /**
49
+ * Creates an object for pattern specific options. This ensures that
50
+ * only configurations that have been provided are included in the
51
+ * final result. This prevents 'cannot read properties of undefined'
52
+ * errors when undefined configs are provided to the underlying
53
+ * @fastify/autoload plugin.
54
+ */
55
+ function patternOptionsFromPlugin (plugin) {
56
+ const config = {}
57
+
58
+ // Expected keys for autoload plugin options that expect regexp patterns
59
+ const patternOptionKeys = ['ignorePattern', 'indexPattern', 'autoHooksPattern']
60
+
61
+ for (const key of patternOptionKeys) {
62
+ const pattern = plugin[key]
63
+
64
+ // If pattern key not found in plugin object, move on
65
+ if (!pattern) {
66
+ continue
67
+ }
68
+
69
+ // Build an instance of a RegExp. If this comes back undefined,
70
+ // then an invalid value was provided. Move on.
71
+ const regExpPattern = stringPatternToRegExp(pattern)
72
+ if (!regExpPattern) {
73
+ continue
74
+ }
75
+
76
+ // We have a valid RegExp so add the option to the config to pass along to
77
+ // autoload.
78
+ config[key] = regExpPattern
79
+ }
80
+
81
+ return config
82
+ }
83
+
84
+ function stringPatternToRegExp (stringPattern) {
85
+ try {
86
+ return new RegExp(stringPattern)
87
+ } catch (err) {
88
+ return undefined
89
+ }
90
+ }
package/lib/schema.js CHANGED
@@ -328,12 +328,21 @@ const server = {
328
328
  },
329
329
  cors
330
330
  },
331
+ additionalProperties: false,
331
332
  required: ['hostname', 'port']
332
333
  }
333
334
 
334
335
  const watch = {
335
336
  type: 'object',
336
337
  properties: {
338
+ enabled: {
339
+ default: true,
340
+ anyOf: [{
341
+ type: 'boolean'
342
+ }, {
343
+ type: 'string'
344
+ }]
345
+ },
337
346
  allow: {
338
347
  type: 'array',
339
348
  items: {
@@ -379,6 +388,39 @@ const plugins = {
379
388
  maxDepth: {
380
389
  type: 'integer'
381
390
  },
391
+ autoHooks: {
392
+ type: 'boolean'
393
+ },
394
+ autoHooksPattern: {
395
+ type: 'string'
396
+ },
397
+ cascadeHooks: {
398
+ type: 'boolean'
399
+ },
400
+ overwriteHooks: {
401
+ type: 'boolean'
402
+ },
403
+ routeParams: {
404
+ type: 'boolean'
405
+ },
406
+ forceESM: {
407
+ type: 'boolean'
408
+ },
409
+ ignoreFilter: {
410
+ type: 'string'
411
+ },
412
+ matchFilter: {
413
+ type: 'string'
414
+ },
415
+ ignorePattern: {
416
+ type: 'string'
417
+ },
418
+ scriptPattern: {
419
+ type: 'string'
420
+ },
421
+ indexPattern: {
422
+ type: 'string'
423
+ },
382
424
  options: {
383
425
  type: 'object',
384
426
  additionalProperties: true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/service",
3
- "version": "0.33.1",
3
+ "version": "0.34.1",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -65,12 +65,12 @@
65
65
  "pino-pretty": "^10.0.0",
66
66
  "rfdc": "^1.3.0",
67
67
  "ua-parser-js": "^1.0.35",
68
- "@platformatic/client": "0.33.1",
69
- "@platformatic/config": "0.33.1",
70
- "@platformatic/swagger-ui-theme": "0.33.1",
71
- "@platformatic/types": "0.33.1",
72
- "@platformatic/utils": "0.33.1",
73
- "@platformatic/telemetry": "0.33.1"
68
+ "@platformatic/client": "0.34.1",
69
+ "@platformatic/config": "0.34.1",
70
+ "@platformatic/swagger-ui-theme": "0.34.1",
71
+ "@platformatic/types": "0.34.1",
72
+ "@platformatic/utils": "0.34.1",
73
+ "@platformatic/telemetry": "0.34.1"
74
74
  },
75
75
  "standard": {
76
76
  "ignore": [
@@ -342,3 +342,145 @@ test('disable encapsulation for a single file / different order', async ({ teard
342
342
  equal(body, 'bar')
343
343
  }
344
344
  })
345
+
346
+ test('autoload with ignorePattern, indexPattern and autoHooksPattern options', async ({ teardown, equal }) => {
347
+ const config = {
348
+ server: {
349
+ hostname: '127.0.0.1',
350
+ port: 0
351
+ },
352
+ plugins: {
353
+ paths: [
354
+ {
355
+ path: join(__dirname, 'fixtures', 'directories', 'routes'),
356
+
357
+ // Ignore the bar.js which should return a 404 for requests made to /bar
358
+ ignorePattern: '^.*(?:bar).js$',
359
+
360
+ // Set index2.js as the index file which sets the root as /index2
361
+ indexPattern: '^index2(?:.js)$',
362
+
363
+ // Override default autohooks.js with auto.hooks.js which overrides
364
+ // the response body
365
+ autoHooksPattern: '^auto.hooks.js$',
366
+ autoHooks: true
367
+ }
368
+ ]
369
+ },
370
+ watch: false,
371
+ metrics: false
372
+ }
373
+
374
+ const app = await buildServer(config)
375
+ teardown(async () => {
376
+ await app.close()
377
+ })
378
+ await app.start()
379
+
380
+ {
381
+ const res = await request(`${app.url}/`)
382
+ equal(res.statusCode, 200, 'status code')
383
+ const body = await res.body.json()
384
+ equal(body.hello, 'from root', 'body')
385
+ }
386
+
387
+ {
388
+ const res = await request(`${app.url}/foo/bar`)
389
+ equal(res.statusCode, 404, 'status code')
390
+ }
391
+
392
+ {
393
+ const res = await request(`${app.url}/foo/baz/index2`)
394
+ equal(res.statusCode, 200, 'status code')
395
+ const body = await res.body.json()
396
+ equal(body.hello, 'from baz with index2.js', 'body')
397
+ }
398
+
399
+ {
400
+ const res = await request(`${app.url}/oof`)
401
+ equal(res.statusCode, 200, 'status code')
402
+ const body = await res.body.json()
403
+ equal(body.hello, 'from auto.hooks.js', 'body')
404
+ }
405
+ })
406
+
407
+ test('autoload with INVALID ignorePattern, indexPattern and autoHooksPattern options', async ({ teardown, equal }) => {
408
+ const config = {
409
+ server: {
410
+ hostname: '127.0.0.1',
411
+ port: 0
412
+ },
413
+ plugins: {
414
+ paths: [
415
+ {
416
+ path: join(__dirname, 'fixtures', 'directories', 'routes'),
417
+ ignorePattern: '***',
418
+ indexPattern: '***terrible)))_pattern',
419
+ autoHooksPattern: ''
420
+ }
421
+ ]
422
+ },
423
+ watch: false,
424
+ metrics: false
425
+ }
426
+
427
+ const app = await buildServer(config)
428
+ teardown(async () => {
429
+ await app.close()
430
+ })
431
+ await app.start()
432
+
433
+ {
434
+ const res = await request(`${app.url}/`)
435
+ equal(res.statusCode, 200, 'status code')
436
+ }
437
+ })
438
+
439
+ test('loads encapsulated plugin twice', async ({ teardown, equal, strictSame }) => {
440
+ const config = {
441
+ server: {
442
+ hostname: '127.0.0.1',
443
+ port: 0,
444
+ // Windows CI is slow
445
+ pluginTimeout: 60 * 1000
446
+ },
447
+ service: {
448
+ openapi: true
449
+ },
450
+ plugins: {
451
+ paths: [{
452
+ path: join(__dirname, 'fixtures', 'directories', 'routes')
453
+ }, {
454
+ path: join(__dirname, 'fixtures', 'directories', 'plugins', 'decorator.js')
455
+ }]
456
+ }
457
+ }
458
+
459
+ {
460
+ // First time plugin is loaded from file
461
+ const app = await buildServer(config)
462
+ teardown(async () => {
463
+ await app.close()
464
+ })
465
+ await app.start()
466
+
467
+ const res = await request(`${app.url}/foo/with-decorator`)
468
+ equal(res.statusCode, 200, 'status code')
469
+ const body = await res.body.json()
470
+ strictSame(body, { hello: 'bar' })
471
+ }
472
+
473
+ {
474
+ // Second time plugin is loaded from cache
475
+ const app = await buildServer(config)
476
+ teardown(async () => {
477
+ await app.close()
478
+ })
479
+ await app.start()
480
+
481
+ const res = await request(`${app.url}/foo/with-decorator`)
482
+ equal(res.statusCode, 200, 'status code')
483
+ const body = await res.body.json()
484
+ strictSame(body, { hello: 'bar' })
485
+ }
486
+ })
@@ -6,7 +6,7 @@ const { buildServer } = require('..')
6
6
  const { request } = require('undici')
7
7
  const { join } = require('path')
8
8
  const os = require('os')
9
- const { writeFile } = require('fs/promises')
9
+ const { writeFile, rm } = require('fs/promises')
10
10
 
11
11
  test('config reloads', async ({ teardown, equal, pass, same }) => {
12
12
  const file = join(os.tmpdir(), `${process.pid}-1.js`)
@@ -295,3 +295,22 @@ test('config reloads', async ({ teardown, equal, pass, same }) => {
295
295
  same(await res.body.text(), 'ciao mondo', 'response')
296
296
  }
297
297
  })
298
+
299
+ test('do not watch typescript outDir', async ({ teardown, equal, pass, same }) => {
300
+ process.env.PLT_CLIENT_URL = 'http://localhost:3042'
301
+ const targetDir = join(__dirname, '..', 'fixtures', 'hello-client-ts')
302
+
303
+ try {
304
+ await rm(join(targetDir, 'dist'), { recursive: true })
305
+ } catch {}
306
+
307
+ const app = await buildServer(join(targetDir, 'platformatic.service.json'))
308
+ teardown(async () => {
309
+ await app.close()
310
+ })
311
+
312
+ same(app.platformatic.configManager.current.watch, {
313
+ enabled: false,
314
+ ignore: ['dist/**/*']
315
+ })
316
+ })
@@ -0,0 +1,11 @@
1
+ 'use strict'
2
+
3
+ /*
4
+ * Used for providing an alternative `index.js` naming convention
5
+ * when specified from the autoload `indexPattern` property.
6
+ */
7
+ module.exports = async function (fastify, opts) {
8
+ fastify.get('/index2', async function (request, reply) {
9
+ return { hello: 'from baz with index2.js' }
10
+ })
11
+ }
@@ -0,0 +1,5 @@
1
+ module.exports = async function (app, opts) {
2
+ app.addHook('onRequest', async (req, reply) => {
3
+ reply.send({ hello: 'from auto.hooks.js' })
4
+ })
5
+ }
@@ -0,0 +1,7 @@
1
+ 'use strict'
2
+
3
+ module.exports = async function (fastify, opts) {
4
+ fastify.get('/', async function (request, reply) {
5
+ return { hello: 'from oof' }
6
+ })
7
+ }
@@ -81,7 +81,7 @@ test('should setup telemetry if configured', async ({ teardown, equal, pass, sam
81
81
  equal(finishedSpans.length, 1)
82
82
  const span = finishedSpans[0]
83
83
  equal(span.name, 'GET /')
84
- equal(span.attributes['req.method'], 'GET')
85
- equal(span.attributes['req.url'], '/')
86
- equal(span.attributes['reply.statusCode'], 200)
84
+ equal(span.attributes['http.request.method'], 'GET')
85
+ equal(span.attributes['url.path'], '/')
86
+ equal(span.attributes['http.response.status_code'], 200)
87
87
  })