@platformatic/service 0.17.1 → 0.18.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.
Files changed (35) hide show
  1. package/help/schema.txt +1 -1
  2. package/index.js +21 -1
  3. package/lib/compile.js +2 -2
  4. package/lib/graphql.js +21 -0
  5. package/lib/load-config.js +6 -1
  6. package/lib/openapi-schema-defs.js +1140 -0
  7. package/lib/openapi.js +42 -0
  8. package/lib/schema.js +229 -25
  9. package/lib/utils.js +12 -1
  10. package/package.json +8 -4
  11. package/test/cli/compile.test.mjs +55 -33
  12. package/test/cli/gen-schema.test.mjs +1 -2
  13. package/test/cli/watch.test.mjs +13 -6
  14. package/test/config.test.js +2 -1
  15. package/test/fixtures/bad-typescript-plugin/dist/tsconfig.tsbuildinfo +1 -1
  16. package/test/fixtures/hello-world-resolver.js +16 -0
  17. package/test/fixtures/typescript-autoload/platformatic.service.json +13 -0
  18. package/test/fixtures/typescript-autoload/routes/plugin.ts +5 -0
  19. package/test/fixtures/typescript-autoload/tsconfig.json +22 -0
  20. package/test/graphql.test.js +154 -0
  21. package/test/helper.js +1 -2
  22. package/test/routes.test.js +123 -5
  23. package/test/tmp/typescript-plugin-clone-3/dist/tsconfig.tsbuildinfo +1 -1
  24. package/test/tmp/typescript-plugin-clone-4/dist/tsconfig.tsbuildinfo +1 -1
  25. package/test/tmp/typescript-plugin-clone-7/inner-folder/dist/plugin.js +18 -0
  26. package/test/tmp/typescript-plugin-clone-7/inner-folder/dist/plugin.js.map +1 -0
  27. package/test/tmp/typescript-plugin-clone-7/inner-folder/platformatic.service.json +13 -0
  28. package/test/tmp/typescript-plugin-clone-7/inner-folder/plugin.ts +5 -0
  29. package/test/tmp/typescript-plugin-clone-7/inner-folder/tsconfig.json +22 -0
  30. package/test/tmp/typescript-plugin-clone-8/dist/routes/plugin.js +7 -0
  31. package/test/tmp/typescript-plugin-clone-8/dist/routes/plugin.js.map +1 -0
  32. package/test/tmp/typescript-plugin-clone-8/dist/tsconfig.tsbuildinfo +1 -0
  33. package/test/tmp/typescript-plugin-clone-8/platformatic.service.json +13 -0
  34. package/test/tmp/typescript-plugin-clone-8/routes/plugin.ts +5 -0
  35. package/test/tmp/typescript-plugin-clone-8/tsconfig.json +22 -0
package/lib/openapi.js ADDED
@@ -0,0 +1,42 @@
1
+ 'use strict'
2
+
3
+ const Swagger = require('@fastify/swagger')
4
+ const SwaggerUI = require('@fastify/swagger-ui')
5
+ const deepmerge = require('@fastify/deepmerge')({ all: true })
6
+ const fp = require('fastify-plugin')
7
+
8
+ // For some unknown reason, c8 is not detecting any of this
9
+ // pf
10
+ // despite being covered by test/routes.test.js
11
+ /* c8 ignore next 33 */
12
+ async function setupOpenAPI (app, opts) {
13
+ const openapiConfig = deepmerge({
14
+ exposeRoute: true,
15
+ info: {
16
+ title: 'Platformatic',
17
+ description: 'This is a service built on top of Platformatic',
18
+ version: '1.0.0'
19
+ }
20
+ }, typeof opts === 'object' ? opts : {})
21
+ app.log.trace({ openapi: openapiConfig })
22
+ await app.register(Swagger, {
23
+ exposeRoute: openapiConfig.exposeRoute,
24
+ openapi: {
25
+ ...openapiConfig
26
+ },
27
+ refResolver: {
28
+ buildLocalReference (json, baseUri, fragment, i) {
29
+ // TODO figure out if we need def-${i}
30
+ /* istanbul ignore next */
31
+ return json.$id || `def-${i}`
32
+ }
33
+ }
34
+ })
35
+
36
+ app.register(SwaggerUI, {
37
+ ...opts,
38
+ prefix: '/documentation'
39
+ })
40
+ }
41
+
42
+ module.exports = fp(setupOpenAPI)
package/lib/schema.js CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  const pkg = require('../package.json')
6
6
  const version = 'v' + pkg.version
7
+ const openApiDefs = require('./openapi-schema-defs')
7
8
 
8
9
  const cors = {
9
10
  type: 'object',
@@ -123,6 +124,119 @@ const server = {
123
124
  }
124
125
  ]
125
126
  },
127
+ ignoreTrailingSlash: {
128
+ type: 'boolean'
129
+ },
130
+ ignoreDuplicateSlashes: {
131
+ type: 'boolean'
132
+ },
133
+ connectionTimeout: {
134
+ type: 'integer'
135
+ },
136
+ keepAliveTimeout: {
137
+ type: 'integer'
138
+ },
139
+ maxRequestsPerSocket: {
140
+ type: 'integer'
141
+ },
142
+ forceCloseConnections: {
143
+ anyOf: [
144
+ { type: 'boolean' },
145
+ { type: 'string', pattern: '^idle$' }
146
+ ]
147
+ },
148
+ requestTimeout: {
149
+ type: 'integer'
150
+ },
151
+ bodyLimit: {
152
+ type: 'integer'
153
+ },
154
+ maxParamLength: {
155
+ type: 'integer'
156
+ },
157
+ disableRequestLogging: {
158
+ type: 'boolean'
159
+ },
160
+ exposeHeadRoutes: {
161
+ type: 'boolean'
162
+ },
163
+ logger: {
164
+ anyOf: [
165
+ { type: 'boolean' },
166
+ {
167
+ type: 'object',
168
+ properties: {
169
+ level: {
170
+ type: 'string'
171
+ }
172
+ },
173
+ additionalProperties: true
174
+ }
175
+ ]
176
+ },
177
+ serializerOpts: {
178
+ type: 'object',
179
+ properties: {
180
+ schema: {
181
+ type: 'object'
182
+ },
183
+ ajv: {
184
+ type: 'object'
185
+ },
186
+ rounding: {
187
+ type: 'string',
188
+ enum: ['floor', 'ceil', 'round', 'trunc'],
189
+ default: 'trunc'
190
+ },
191
+ debugMode: {
192
+ type: 'boolean'
193
+ },
194
+ mode: {
195
+ type: 'string',
196
+ enum: ['debug', 'standalone']
197
+ },
198
+ largeArraySize: {
199
+ anyOf: [
200
+ { type: 'integer' },
201
+ { type: 'string' }
202
+ ],
203
+ default: 20000
204
+ },
205
+ largeArrayMechanism: {
206
+ type: 'string',
207
+ enum: ['default', 'json-stringify'],
208
+ default: 'default'
209
+ }
210
+ }
211
+ },
212
+ caseSensitive: {
213
+ type: 'boolean'
214
+ },
215
+ requestIdHeader: {
216
+ anyOf: [
217
+ { type: 'string' },
218
+ { type: 'boolean', const: false }
219
+ ]
220
+ },
221
+ requestIdLogLabel: {
222
+ type: 'string'
223
+ },
224
+ jsonShorthand: {
225
+ type: 'boolean'
226
+ },
227
+ trustProxy: {
228
+ anyOf: [
229
+ { type: 'boolean' },
230
+ { type: 'string' },
231
+ {
232
+ type: 'array',
233
+ items: {
234
+ type: 'string'
235
+ }
236
+ },
237
+ { type: 'integer' }
238
+ ]
239
+ },
126
240
  cors
127
241
  },
128
242
  required: ['hostname', 'port']
@@ -131,28 +245,25 @@ const server = {
131
245
  const watch = {
132
246
  type: 'object',
133
247
  properties: {
134
- type: 'object',
135
- properties: {
136
- allow: {
137
- type: 'array',
138
- items: {
139
- type: 'string'
140
- },
141
- minItems: 1,
142
- nullable: true,
143
- default: null
248
+ allow: {
249
+ type: 'array',
250
+ items: {
251
+ type: 'string'
144
252
  },
145
- ignore: {
146
- type: 'array',
147
- items: {
148
- type: 'string'
149
- },
150
- nullable: true,
151
- default: null
152
- }
253
+ minItems: 1,
254
+ nullable: true,
255
+ default: null
153
256
  },
154
- additionalProperties: false
155
- }
257
+ ignore: {
258
+ type: 'array',
259
+ items: {
260
+ type: 'string'
261
+ },
262
+ nullable: true,
263
+ default: null
264
+ }
265
+ },
266
+ additionalProperties: false
156
267
  }
157
268
 
158
269
  const plugins = {
@@ -219,15 +330,100 @@ const metrics = {
219
330
  ]
220
331
  }
221
332
 
333
+ const openApiBase = {
334
+ type: 'object',
335
+ properties: {
336
+ info: {
337
+ $ref: '#/$defs/info'
338
+ },
339
+ jsonSchemaDialect: {
340
+ type: 'string',
341
+
342
+ default: 'https://spec.openapis.org/oas/3.1/dialect/base'
343
+ },
344
+ servers: {
345
+ type: 'array',
346
+ items: {
347
+ $ref: '#/$defs/server'
348
+ },
349
+ default: [
350
+ {
351
+ url: '/'
352
+ }
353
+ ]
354
+ },
355
+ paths: {
356
+ $ref: '#/$defs/paths'
357
+ },
358
+ webhooks: {
359
+ type: 'object',
360
+ additionalProperties: {
361
+ $ref: '#/$defs/path-item-or-reference'
362
+ }
363
+ },
364
+ components: {
365
+ $ref: '#/$defs/components'
366
+ },
367
+ security: {
368
+ type: 'array',
369
+ items: {
370
+ $ref: '#/$defs/security-requirement'
371
+ }
372
+ },
373
+ tags: {
374
+ type: 'array',
375
+ items: {
376
+ $ref: '#/$defs/tag'
377
+ }
378
+ },
379
+ externalDocs: {
380
+ $ref: '#/$defs/external-documentation'
381
+ },
382
+ prefix: {
383
+ type: 'string',
384
+ description: 'Base URL for the OpenAPI'
385
+ }
386
+ }
387
+ }
388
+
389
+ const openapi = {
390
+ anyOf: [{
391
+ ...openApiBase,
392
+ additionalProperties: false
393
+ }, {
394
+ type: 'boolean'
395
+ }]
396
+ }
397
+
398
+ const graphql = {
399
+ anyOf: [{
400
+ type: 'boolean'
401
+ }, {
402
+ type: 'object',
403
+ properties: {
404
+ graphiql: {
405
+ type: 'boolean'
406
+ }
407
+ }
408
+ }]
409
+ }
410
+
411
+ const service = {
412
+ type: 'object',
413
+ properties: {
414
+ openapi,
415
+ graphql
416
+ },
417
+ additionalProperties: false
418
+ }
419
+
222
420
  const platformaticServiceSchema = {
223
421
  $id: `https://platformatic.dev/schemas/${version}/service`,
224
422
  type: 'object',
225
423
  properties: {
226
424
  server,
227
425
  plugins,
228
- metrics
229
- },
230
- additionalProperties: {
426
+ metrics,
231
427
  watch: {
232
428
  anyOf: [watch, {
233
429
  type: 'boolean'
@@ -235,9 +431,15 @@ const platformaticServiceSchema = {
235
431
  },
236
432
  hotReload: {
237
433
  type: 'boolean'
238
- }
434
+ },
435
+ $schema: {
436
+ type: 'string'
437
+ },
438
+ service
239
439
  },
240
- required: ['server']
440
+ additionalProperties: false,
441
+ required: ['server'],
442
+ $defs: openApiDefs
241
443
  }
242
444
 
243
445
  module.exports.schema = platformaticServiceSchema
@@ -246,6 +448,8 @@ module.exports.cors = cors
246
448
  module.exports.server = server
247
449
  module.exports.plugins = plugins
248
450
  module.exports.watch = watch
451
+ module.exports.openApiDefs = openApiDefs
452
+ module.exports.openApiBase = openApiBase
249
453
 
250
454
  if (require.main === module) {
251
455
  console.log(JSON.stringify(platformaticServiceSchema, null, 2))
package/lib/utils.js CHANGED
@@ -46,10 +46,21 @@ function getJSPluginPath (configPath, tsPluginPath, compileDir) {
46
46
  return tsPluginPath
47
47
  }
48
48
 
49
+ const isTs = tsPluginPath.endsWith('ts')
50
+ let newBaseName
51
+
52
+ // TODO: investigate why c8 does not see those
53
+ /* c8 ignore next 5 */
54
+ if (isTs) {
55
+ newBaseName = basename(tsPluginPath, '.ts') + '.js'
56
+ } else {
57
+ newBaseName = basename(tsPluginPath)
58
+ }
59
+
49
60
  const tsPluginRelativePath = relative(dirname(configPath), tsPluginPath)
50
61
  const jsPluginRelativePath = join(
51
62
  dirname(tsPluginRelativePath),
52
- basename(tsPluginRelativePath, '.ts') + '.js'
63
+ newBaseName
53
64
  )
54
65
  return join(compileDir, jsPluginRelativePath)
55
66
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/service",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "author": "Matteo Collina <hello@matteocollina.com>",
@@ -14,6 +14,7 @@
14
14
  },
15
15
  "homepage": "https://github.com/platformatic/platformatic#readme",
16
16
  "devDependencies": {
17
+ "@matteo.collina/worker": "^3.0.0",
17
18
  "bindings": "^1.5.0",
18
19
  "c8": "^7.13.0",
19
20
  "snazzy": "^9.0.0",
@@ -36,7 +37,9 @@
36
37
  "@fastify/restartable": "^1.4.0",
37
38
  "@fastify/static": "^6.9.0",
38
39
  "@fastify/swagger": "^8.3.1",
40
+ "@fastify/swagger-ui": "^1.4.0",
39
41
  "@fastify/under-pressure": "^8.2.0",
42
+ "@mercuriusjs/federation": "^1.0.1",
40
43
  "close-with-grace": "^1.1.0",
41
44
  "commist": "^3.2.0",
42
45
  "desm": "^1.3.0",
@@ -49,13 +52,14 @@
49
52
  "fastify-sandbox": "^0.11.0",
50
53
  "graphql": "^16.6.0",
51
54
  "help-me": "^4.2.0",
55
+ "mercurius": "^12.2.0",
52
56
  "minimist": "^1.2.8",
53
57
  "pino": "^8.11.0",
54
- "pino-pretty": "^9.3.0",
58
+ "pino-pretty": "^10.0.0",
55
59
  "rfdc": "^1.3.0",
56
60
  "ua-parser-js": "^1.0.33",
57
- "@platformatic/utils": "0.17.1",
58
- "@platformatic/config": "0.17.1"
61
+ "@platformatic/config": "0.18.0",
62
+ "@platformatic/utils": "0.18.0"
59
63
  },
60
64
  "standard": {
61
65
  "ignore": [
@@ -12,6 +12,20 @@ function urlDirname (url) {
12
12
  return path.dirname(fileURLToPath(url))
13
13
  }
14
14
 
15
+ function exitOnTeardown (child) {
16
+ return async () => {
17
+ if (os.platform() === 'win32') {
18
+ try {
19
+ await execa('taskkill', ['/pid', child.pid, '/f', '/t'])
20
+ } catch (err) {
21
+ console.error(`Failed to kill process ${child.pid})`)
22
+ }
23
+ } else {
24
+ child.kill('SIGINT')
25
+ }
26
+ }
27
+ }
28
+
15
29
  t.test('should compile typescript plugin', async (t) => {
16
30
  const testDir = path.join(urlDirname(import.meta.url), '..', 'fixtures', 'typescript-plugin')
17
31
  const cwd = path.join(urlDirname(import.meta.url), '..', 'tmp', 'typescript-plugin-clone-1')
@@ -70,17 +84,7 @@ t.test('should compile typescript plugin with start command', async (t) => {
70
84
 
71
85
  const child = execa('node', [cliPath, 'start'], { cwd })
72
86
 
73
- t.teardown(async () => {
74
- if (os.platform() === 'win32') {
75
- try {
76
- await execa('taskkill', ['/pid', child.pid, '/f', '/t'])
77
- } catch (err) {
78
- console.error(`Failed to kill process ${child.pid})`)
79
- }
80
- } else {
81
- child.kill('SIGINT')
82
- }
83
- })
87
+ t.teardown(exitOnTeardown(child))
84
88
 
85
89
  const splitter = split()
86
90
  child.stdout.pipe(splitter)
@@ -195,17 +199,7 @@ t.test('start command should not compile typescript if `typescript` is false', a
195
199
  await cp(testDir, cwd, { recursive: true })
196
200
 
197
201
  const child = execa('node', [cliPath, 'start'], { cwd })
198
- t.teardown(async () => {
199
- if (os.platform() === 'win32') {
200
- try {
201
- await execa('taskkill', ['/pid', child.pid, '/f', '/t'])
202
- } catch (err) {
203
- console.error(`Failed to kill process ${child.pid})`)
204
- }
205
- } else {
206
- child.kill('SIGINT')
207
- }
208
- })
202
+ t.teardown(exitOnTeardown(child))
209
203
 
210
204
  const jsPluginPath = path.join(cwd, 'dist', 'plugin.js')
211
205
  try {
@@ -227,17 +221,7 @@ t.test('should compile typescript plugin with start command with different cwd',
227
221
 
228
222
  const child = execa('node', [cliPath, 'start', '-c', path.join(dest, 'platformatic.service.json')])
229
223
 
230
- t.teardown(async () => {
231
- if (os.platform() === 'win32') {
232
- try {
233
- await execa('taskkill', ['/pid', child.pid, '/f', '/t'])
234
- } catch (err) {
235
- console.error(`Failed to kill process ${child.pid})`)
236
- }
237
- } else {
238
- child.kill('SIGINT')
239
- }
240
- })
224
+ t.teardown(exitOnTeardown(child))
241
225
 
242
226
  const splitter = split()
243
227
  child.stdout.pipe(splitter)
@@ -253,3 +237,41 @@ t.test('should compile typescript plugin with start command with different cwd',
253
237
  }
254
238
  t.fail('should compile typescript plugin with start command')
255
239
  })
240
+
241
+ t.test('valid tsconfig file inside an inner folder', async (t) => {
242
+ const testDir = path.join(urlDirname(import.meta.url), '..', 'fixtures', 'typescript-plugin')
243
+ const cwd = path.join(urlDirname(import.meta.url), '..', 'tmp', 'typescript-plugin-clone-7/inner-folder')
244
+
245
+ await cp(testDir, cwd, { recursive: true })
246
+
247
+ try {
248
+ await execa('node', [cliPath, 'compile'], { cwd })
249
+ } catch (err) {
250
+ t.fail('should not catch any error')
251
+ }
252
+
253
+ t.pass()
254
+ })
255
+
256
+ t.test('should compile typescript plugin with start command from a folder', async (t) => {
257
+ const testDir = path.join(urlDirname(import.meta.url), '..', 'fixtures', 'typescript-autoload')
258
+ const cwd = path.join(urlDirname(import.meta.url), '..', 'tmp', 'typescript-plugin-clone-8')
259
+
260
+ await cp(testDir, cwd, { recursive: true })
261
+
262
+ const child = execa('node', [cliPath, 'start'], { cwd })
263
+
264
+ t.teardown(exitOnTeardown(child))
265
+
266
+ const splitter = split()
267
+ child.stdout.pipe(splitter)
268
+
269
+ for await (const data of splitter) {
270
+ const sanitized = stripAnsi(data)
271
+ if (sanitized.includes('Typescript plugin loaded')) {
272
+ t.pass()
273
+ return
274
+ }
275
+ }
276
+ t.fail('should compile typescript plugin with start command')
277
+ })
@@ -15,9 +15,8 @@ test('generateJsonSchemaConfig generates the file', async (t) => {
15
15
 
16
16
  const configSchema = await fs.readFile('platformatic.service.schema.json', 'utf8')
17
17
  const schema = JSON.parse(configSchema)
18
- const { required, additionalProperties } = schema
18
+ const { required } = schema
19
19
  t.has(required, ['server'])
20
- t.has(additionalProperties, { watch: {} })
21
20
  const { $id, type } = schema
22
21
  t.equal($id, `https://platformatic.dev/schemas/v${pkg.version}/service`)
23
22
  t.equal(type, 'object')
@@ -1,16 +1,19 @@
1
1
  import os from 'os'
2
2
  import { join, basename } from 'path'
3
3
  import { writeFile, mkdtemp } from 'fs/promises'
4
- import { setTimeout as sleep } from 'timers/promises'
5
4
  import t, { test } from 'tap'
6
5
  import { request } from 'undici'
6
+ import { setTimeout as sleep } from 'timers/promises'
7
7
  import { start } from './helper.mjs'
8
8
 
9
9
  t.jobs = 5
10
10
 
11
- function createLoggingPlugin (text) {
11
+ function createLoggingPlugin (text, reloaded = false) {
12
12
  return `\
13
13
  module.exports = async (app) => {
14
+ if (${reloaded}) {
15
+ app.log.info('RELOADED')
16
+ }
14
17
  app.get('/version', () => '${text}')
15
18
  }
16
19
  `
@@ -43,9 +46,11 @@ test('should watch js files by default', async ({ equal, teardown }) => {
43
46
  const { child, url } = await start('-c', configFilePath)
44
47
  teardown(() => child.kill('SIGINT'))
45
48
 
46
- await writeFile(pluginFilePath, createLoggingPlugin('v2'))
49
+ await writeFile(pluginFilePath, createLoggingPlugin('v2', true))
47
50
 
48
- await sleep(5000)
51
+ for await (const log of child.ndj) {
52
+ if (log.msg === 'RELOADED') break
53
+ }
49
54
 
50
55
  const res = await request(`${url}/version`)
51
56
  const version = await res.body.text()
@@ -128,7 +133,8 @@ test('should not watch ignored file', async ({ teardown, equal }) => {
128
133
  const { child, url } = await start('-c', configFilePath)
129
134
  teardown(() => child.kill('SIGINT'))
130
135
 
131
- await writeFile(pluginFilePath, createLoggingPlugin('v2'))
136
+ await writeFile(pluginFilePath, createLoggingPlugin('v2', true))
137
+
132
138
  await sleep(5000)
133
139
 
134
140
  const res = await request(`${url}/version`)
@@ -273,7 +279,8 @@ test('should not hot reload files with `--hot-reload false`', async ({ teardown,
273
279
  equal(version, 'v1')
274
280
  }
275
281
 
276
- await writeFile(pluginFilePath, createLoggingPlugin('v2'))
282
+ await writeFile(pluginFilePath, createLoggingPlugin('v2', true))
283
+
277
284
  await sleep(5000)
278
285
 
279
286
  {
@@ -217,6 +217,7 @@ test('config is adjusted to handle custom loggers', async (t) => {
217
217
  Object.defineProperty(options.server.logger, 'child', {
218
218
  value: function child () {
219
219
  called = true
220
+ return this
220
221
  },
221
222
  enumerable: false
222
223
  })
@@ -284,7 +285,7 @@ test('custom ConfigManager', async ({ teardown, equal, pass, same }) => {
284
285
  {
285
286
  const res = await request(`${server.url}/`)
286
287
  equal(res.statusCode, 200, 'add status code')
287
- same(await res.body.text(), 'ciao mondo', 'response')
288
+ same(await res.body.text(), 'hello', 'response')
288
289
  }
289
290
  })
290
291