@platformatic/service 3.4.1 → 3.5.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 (54) hide show
  1. package/LICENSE +1 -1
  2. package/config.d.ts +450 -94
  3. package/eslint.config.js +4 -6
  4. package/index.d.ts +55 -48
  5. package/index.js +44 -179
  6. package/lib/application.js +35 -0
  7. package/lib/capability.js +281 -0
  8. package/lib/compile.js +2 -52
  9. package/lib/generator.js +426 -0
  10. package/lib/plugins/cors.js +5 -8
  11. package/lib/plugins/graphql.js +16 -14
  12. package/lib/plugins/health-check.js +6 -8
  13. package/lib/plugins/openapi.js +43 -32
  14. package/lib/plugins/plugins.js +6 -53
  15. package/lib/{root-endpoint/index.js → plugins/root.js} +9 -8
  16. package/lib/plugins/sandbox-wrapper.js +65 -63
  17. package/lib/schema.js +1075 -203
  18. package/lib/upgrade.js +6 -8
  19. package/lib/utils.js +30 -83
  20. package/lib/versions/0.16.0.js +14 -15
  21. package/lib/versions/{from-zero-twenty-eight-to-will-see.js → 0.28.0.js} +3 -6
  22. package/lib/versions/2.0.0.js +4 -7
  23. package/lib/versions/3.0.0.js +14 -0
  24. package/package.json +28 -36
  25. package/schema.json +1452 -165
  26. package/tsconfig.json +16 -6
  27. package/.c8rc +0 -6
  28. package/help/compile.txt +0 -19
  29. package/help/create.txt +0 -11
  30. package/help/help.txt +0 -8
  31. package/help/schema.txt +0 -9
  32. package/help/start.txt +0 -23
  33. package/index.test-d.ts +0 -107
  34. package/lib/create.mjs +0 -85
  35. package/lib/gen-schema.js +0 -15
  36. package/lib/gen-types.mjs +0 -38
  37. package/lib/generator/README.md +0 -31
  38. package/lib/generator/service-generator.d.ts +0 -11
  39. package/lib/generator/service-generator.js +0 -126
  40. package/lib/openapi-schema-defs.js +0 -1108
  41. package/lib/plugins/clients.js +0 -16
  42. package/lib/plugins/metrics.js +0 -244
  43. package/lib/plugins/typescript.js +0 -20
  44. package/lib/stackable.js +0 -306
  45. package/lib/start.js +0 -175
  46. package/service.mjs +0 -71
  47. /package/{lib/root-endpoint/public → public}/images/dark_mode.svg +0 -0
  48. /package/{lib/root-endpoint/public → public}/images/favicon.ico +0 -0
  49. /package/{lib/root-endpoint/public → public}/images/light_mode.svg +0 -0
  50. /package/{lib/root-endpoint/public → public}/images/platformatic-logo-dark.svg +0 -0
  51. /package/{lib/root-endpoint/public → public}/images/platformatic-logo-light.svg +0 -0
  52. /package/{lib/root-endpoint/public → public}/images/triangle_dark.svg +0 -0
  53. /package/{lib/root-endpoint/public → public}/images/triangle_light.svg +0 -0
  54. /package/{lib/root-endpoint/public → public}/index.html +0 -0
@@ -1,16 +0,0 @@
1
- 'use strict'
2
-
3
- const fp = require('fastify-plugin')
4
- const client = require('@platformatic/client')
5
-
6
- async function setupClients (app, opts) {
7
- for (const { path, url, serviceId, name, schema, type, fullRequest, fullResponse, validateResponse } of opts) {
8
- if (path) {
9
- app.register(require(path), { url, serviceId })
10
- } else {
11
- app.register(client, { url, serviceId, name, path: schema, type, fullRequest, fullResponse, validateResponse })
12
- }
13
- }
14
- }
15
-
16
- module.exports = fp(setupClients)
@@ -1,244 +0,0 @@
1
- 'use strict'
2
-
3
- const os = require('node:os')
4
- const http = require('node:http')
5
- const { eventLoopUtilization } = require('node:perf_hooks').performance
6
- const fastify = require('fastify')
7
- const fp = require('fastify-plugin')
8
-
9
- const metricsPlugin = fp(async function (app, opts = {}) {
10
- const promClient = require('prom-client')
11
-
12
- const register = new promClient.Registry()
13
-
14
- const defaultMetrics = opts.defaultMetrics ?? { enabled: true }
15
-
16
- if (opts.labels) {
17
- const labels = opts.labels ?? {}
18
- register.setDefaultLabels(labels)
19
- }
20
-
21
- app.register(require('fastify-metrics'), {
22
- defaultMetrics: {
23
- ...defaultMetrics,
24
- register,
25
- },
26
- endpoint: null,
27
- name: 'metrics',
28
- clearRegisterOnInit: false,
29
- promClient: {
30
- ...promClient,
31
- register,
32
- },
33
- routeMetrics: {
34
- enabled: true,
35
- customLabels: {
36
- // TODO: check if this is set in prom
37
- telemetry_id: (req) => req.headers['x-plt-telemetry-id'] ?? 'unknown',
38
- },
39
- overrides: {
40
- histogram: {
41
- name: 'http_request_duration_seconds',
42
- registers: [register],
43
- },
44
- summary: {
45
- name: 'http_request_summary_seconds',
46
- registers: [register],
47
- },
48
- },
49
- },
50
- })
51
-
52
- app.register(fp(async (app) => {
53
- const httpLatencyMetric = new app.metrics.client.Summary({
54
- name: 'http_request_all_summary_seconds',
55
- help: 'request duration in seconds summary for all requests',
56
- collect: () => {
57
- process.nextTick(() => httpLatencyMetric.reset())
58
- },
59
- registers: [register],
60
- })
61
-
62
- const ignoredMethods = ['HEAD', 'OPTIONS', 'TRACE', 'CONNECT']
63
- const timers = new WeakMap()
64
- app.addHook('onRequest', async (req) => {
65
- if (ignoredMethods.includes(req.method)) return
66
- const timer = httpLatencyMetric.startTimer()
67
- timers.set(req, timer)
68
- })
69
- app.addHook('onResponse', async (req) => {
70
- if (ignoredMethods.includes(req.method)) return
71
- const timer = timers.get(req)
72
- if (timer) {
73
- timer()
74
- timers.delete(req)
75
- }
76
- })
77
- }, {
78
- encapsulate: false,
79
- }))
80
-
81
- if (defaultMetrics.enabled) {
82
- app.register(async (app) => {
83
- let startELU = eventLoopUtilization()
84
- const eluMetric = new app.metrics.client.Gauge({
85
- name: 'nodejs_eventloop_utilization',
86
- help: 'The event loop utilization as a fraction of the loop time. 1 is fully utilized, 0 is fully idle.',
87
- collect: () => {
88
- const endELU = eventLoopUtilization()
89
- const result = eventLoopUtilization(endELU, startELU).utilization
90
- eluMetric.set(result)
91
- startELU = endELU
92
- },
93
- registers: [register],
94
- })
95
- app.metrics.client.register.registerMetric(eluMetric)
96
-
97
- let previousIdleTime = 0
98
- let previousTotalTime = 0
99
- const cpuMetric = new app.metrics.client.Gauge({
100
- name: 'process_cpu_percent_usage',
101
- help: 'The process CPU percent usage.',
102
- collect: () => {
103
- const cpus = os.cpus()
104
- let idleTime = 0
105
- let totalTime = 0
106
-
107
- cpus.forEach(cpu => {
108
- for (const type in cpu.times) {
109
- totalTime += cpu.times[type]
110
- if (type === 'idle') {
111
- idleTime += cpu.times[type]
112
- }
113
- }
114
- })
115
-
116
- const idleDiff = idleTime - previousIdleTime
117
- const totalDiff = totalTime - previousTotalTime
118
-
119
- const usagePercent = 100 - ((100 * idleDiff) / totalDiff)
120
- const roundedUsage = Math.round(usagePercent * 100) / 100
121
- cpuMetric.set(roundedUsage)
122
-
123
- previousIdleTime = idleTime
124
- previousTotalTime = totalTime
125
- },
126
- registers: [register],
127
- })
128
- app.metrics.client.register.registerMetric(cpuMetric)
129
- })
130
- }
131
-
132
- function cleanMetrics () {
133
- const httpMetrics = ['http_request_duration_seconds', 'http_request_summary_seconds', 'http_request_all_summary_seconds']
134
- const metrics = app.metrics.client.register._metrics
135
- for (const metricName in metrics) {
136
- if (defaultMetrics.enabled || httpMetrics.includes(metricName)) {
137
- delete metrics[metricName]
138
- }
139
- }
140
- }
141
-
142
- app.addHook('onClose', async () => {
143
- cleanMetrics()
144
- })
145
- }, {
146
- encapsulate: false,
147
- })
148
-
149
- // This is a global httpServer to match global
150
- // prometheus. It's an antipattern, so do
151
- // not use it elsewhere.
152
- let httpServer = null
153
-
154
- async function createMetricsServer (app, hostname, port) {
155
- if (httpServer && httpServer.address().port !== port) {
156
- await closeMetricsServer()
157
- }
158
-
159
- if (!httpServer) {
160
- httpServer = http.createServer()
161
- httpServer.listen(port, hostname)
162
- httpServer.unref()
163
- }
164
-
165
- const promServer = fastify({
166
- name: 'Prometheus server',
167
- serverFactory: (handler) => {
168
- httpServer.removeAllListeners('request')
169
- httpServer.removeAllListeners('clientError')
170
- httpServer.on('request', handler)
171
- return httpServer
172
- },
173
- loggerInstance: app.log.child({ name: 'prometheus' }),
174
- })
175
-
176
- app.addHook('onClose', async () => {
177
- await promServer.close()
178
- })
179
-
180
- return promServer
181
- }
182
-
183
- async function closeMetricsServer () {
184
- if (httpServer) {
185
- await new Promise((resolve) => httpServer.close(resolve))
186
- httpServer = null
187
- }
188
- }
189
-
190
- module.exports = fp(async function (app, opts) {
191
- const server = opts.server ?? 'own'
192
- const hostname = opts.hostname ?? '0.0.0.0'
193
- const port = opts.port ?? 9090
194
- const metricsEndpoint = opts.endpoint ?? '/metrics'
195
- const auth = opts.auth ?? null
196
-
197
- app.register(metricsPlugin, opts)
198
-
199
- let metricsServer = app
200
- if (server === 'own') {
201
- metricsServer = await createMetricsServer(app, hostname, port)
202
- } else {
203
- await closeMetricsServer()
204
- }
205
-
206
- if (server !== 'hide') {
207
- let onRequestHook
208
- if (auth) {
209
- const { username, password } = auth
210
-
211
- await metricsServer.register(require('@fastify/basic-auth'), {
212
- validate: function (user, pass, req, reply, done) {
213
- if (username !== user || password !== pass) {
214
- return reply.code(401).send({ message: 'Unauthorized' })
215
- }
216
- return done()
217
- },
218
- })
219
- onRequestHook = metricsServer.basicAuth
220
- }
221
-
222
- metricsServer.register(require('@fastify/accepts'))
223
-
224
- metricsServer.route({
225
- url: metricsEndpoint,
226
- method: 'GET',
227
- logLevel: 'warn',
228
- onRequest: onRequestHook,
229
- handler: async (req, reply) => {
230
- const promRegistry = app.metrics.client.register
231
- const accepts = req.accepts()
232
- if (!accepts.type('text/plain') && accepts.type('application/json')) {
233
- return promRegistry.getMetricsAsJSON()
234
- }
235
- reply.type('text/plain')
236
- return promRegistry.metrics()
237
- },
238
- })
239
- }
240
-
241
- if (server === 'own') {
242
- await metricsServer.ready()
243
- }
244
- })
@@ -1,20 +0,0 @@
1
- 'use strict'
2
-
3
- const fp = require('fastify-plugin')
4
- const compiler = require('@platformatic/ts-compiler')
5
- const { extractTypeScriptCompileOptionsFromConfig } = require('../compile')
6
-
7
- async function setupTsCompiler (app, opts) {
8
- const configManager = app.platformatic.configManager
9
- const config = configManager.current
10
- const workingDir = opts?.context?.directory ?? configManager.dirname
11
-
12
- await compiler.compile({
13
- ...extractTypeScriptCompileOptionsFromConfig(config),
14
- cwd: workingDir,
15
- clean: false,
16
- logger: app.log,
17
- })
18
- }
19
-
20
- module.exports = fp(setupTsCompiler)
package/lib/stackable.js DELETED
@@ -1,306 +0,0 @@
1
- 'use strict'
2
-
3
- const { dirname } = require('node:path')
4
- const { printSchema } = require('graphql')
5
- const pino = require('pino')
6
- const httpMetrics = require('@platformatic/fastify-http-metrics')
7
- const { extractTypeScriptCompileOptionsFromConfig } = require('./compile')
8
- const { compile } = require('@platformatic/ts-compiler')
9
- const { deepmerge } = require('@platformatic/utils')
10
-
11
- class ServiceStackable {
12
- constructor (options) {
13
- this.app = null
14
- this._init = options.init
15
- this.stackable = options.stackable
16
- this.metricsRegistry = null
17
-
18
- this.configManager = options.configManager
19
- this.context = options.context ?? {}
20
- this.context.stackable = this
21
-
22
- this.configManager.on('error', err => {
23
- /* c8 ignore next */
24
- this.stackable.log({
25
- message: 'error reloading the configuration' + err,
26
- level: 'error'
27
- })
28
- })
29
-
30
- this.#updateConfig()
31
-
32
- // Setup globals
33
- this.registerGlobals({
34
- setOpenapiSchema: this.setOpenapiSchema.bind(this),
35
- setGraphqlSchema: this.setGraphqlSchema.bind(this),
36
- setBasePath: this.setBasePath.bind(this)
37
- })
38
- }
39
-
40
- async init () {
41
- this.#initLogger()
42
-
43
- if (this.app === null) {
44
- this.app = await this._init()
45
-
46
- if (this.metricsRegistry) {
47
- this.#setHttpMetrics()
48
- }
49
- }
50
- return this.app
51
- }
52
-
53
- async start (options = {}) {
54
- await this.init()
55
-
56
- if (options.listen === false) {
57
- await this.app.ready()
58
- return
59
- }
60
- await this.app.start()
61
- }
62
-
63
- async stop () {
64
- if (this.app === null) return
65
- await this.app.close()
66
- }
67
-
68
- async build () {
69
- this.#initLogger()
70
- const typeScriptCompileOptions = extractTypeScriptCompileOptionsFromConfig(this.configManager.current)
71
- const cwd = dirname(this.configManager.fullPath)
72
- const compileOptions = {
73
- ...typeScriptCompileOptions,
74
- cwd,
75
- logger: this.logger
76
- }
77
- if (!(await compile(compileOptions))) {
78
- throw new Error(`Failed to compile ${cwd}`)
79
- }
80
- }
81
-
82
- getUrl () {
83
- return this.app !== null ? this.app.url : null
84
- }
85
-
86
- async getInfo () {
87
- const type = this.stackable.configType
88
- const version = this.stackable.configManagerConfig.version ?? null
89
- return { type, version }
90
- }
91
-
92
- async getConfig () {
93
- const config = Object.assign({}, this.configManager.current)
94
- config.server = Object.assign({}, config.server)
95
- const logger = config.server.loggerInstance
96
-
97
- if (logger) {
98
- config.server.logger = {}
99
-
100
- if (logger.level) {
101
- config.server.logger.level = logger.level
102
- }
103
- }
104
-
105
- delete config.server.loggerInstance
106
-
107
- return config
108
- }
109
-
110
- async getEnv () {
111
- return this.configManager.env
112
- }
113
-
114
- getMeta () {
115
- const config = this.configManager.current
116
-
117
- return {
118
- composer: {
119
- prefix: config.basePath ?? this.basePath ?? this.context?.serviceId,
120
- wantsAbsoluteUrls: false,
121
- needsRootRedirect: false
122
- }
123
- }
124
- }
125
-
126
- async getWatchConfig () {
127
- const config = this.configManager.current
128
-
129
- const enabled = config.watch?.enabled !== false && config.plugins !== undefined
130
-
131
- return {
132
- enabled,
133
- path: dirname(this.configManager.fullPath),
134
- allow: config.watch?.allow,
135
- ignore: config.watch?.ignore
136
- }
137
- }
138
-
139
- async getDispatchFunc () {
140
- await this.init()
141
- return this.app
142
- }
143
-
144
- async getOpenapiSchema () {
145
- await this.init()
146
- await this.app.ready()
147
- return this.app.swagger ? this.app.swagger() : null
148
- }
149
-
150
- async getGraphqlSchema () {
151
- await this.init()
152
- await this.app.ready()
153
- return this.app.graphql ? printSchema(this.app.graphql.schema) : null
154
- }
155
-
156
- async collectMetrics ({ registry }) {
157
- this.metricsRegistry = registry
158
-
159
- return {
160
- defaultMetrics: true,
161
- httpMetrics: false
162
- }
163
- }
164
-
165
- async inject (injectParams) {
166
- await this.init()
167
-
168
- const { statusCode, statusMessage, headers, body } = await this.app.inject(injectParams)
169
- return { statusCode, statusMessage, headers, body }
170
- }
171
-
172
- async log (options = {}) {
173
- await this.init()
174
-
175
- const logLevel = options.level ?? 'info'
176
-
177
- const message = options.message
178
- if (!message) return
179
-
180
- this.app.log[logLevel](message)
181
- }
182
-
183
- async updateContext (context) {
184
- this.context = { ...this.context, ...context }
185
- this.#updateConfig()
186
- }
187
-
188
- setOpenapiSchema (schema) {
189
- this.openapiSchema = schema
190
- }
191
-
192
- setGraphqlSchema (schema) {
193
- this.graphqlSchema = schema
194
- }
195
-
196
- setBasePath (basePath) {
197
- this.basePath = basePath
198
- }
199
-
200
- registerGlobals (globals) {
201
- globalThis.platformatic = Object.assign(globalThis.platformatic ?? {}, globals)
202
- }
203
-
204
- #setHttpMetrics () {
205
- this.app.register(httpMetrics, {
206
- registry: this.metricsRegistry,
207
- customLabels: ['telemetry_id'],
208
- getCustomLabels: req => {
209
- const telemetryId = req.headers['x-plt-telemetry-id'] ?? 'unknown'
210
- return { telemetry_id: telemetryId }
211
- }
212
- })
213
-
214
- this.app.register(httpMetrics, {
215
- registry: this.metricsRegistry,
216
- customLabels: ['telemetry_id'],
217
- getCustomLabels: req => {
218
- const telemetryId = req.headers['x-plt-telemetry-id'] ?? 'unknown'
219
- return { telemetry_id: telemetryId }
220
- },
221
- histogram: {
222
- name: 'http_request_all_duration_seconds',
223
- help: 'request duration in seconds summary for all requests',
224
- collect: function () {
225
- process.nextTick(() => this.reset())
226
- }
227
- },
228
- summary: {
229
- name: 'http_request_all_summary_seconds',
230
- help: 'request duration in seconds histogram for all requests',
231
- collect: function () {
232
- process.nextTick(() => this.reset())
233
- }
234
- }
235
- })
236
- }
237
-
238
- #updateConfig () {
239
- if (!this.context) return
240
-
241
- const { serviceId, telemetryConfig, metricsConfig, serverConfig, hasManagementApi, isEntrypoint, isProduction } =
242
- this.context
243
-
244
- const config = this.configManager.current
245
-
246
- if (telemetryConfig) {
247
- config.telemetry = telemetryConfig
248
- }
249
- if (metricsConfig) {
250
- config.metrics = metricsConfig
251
- }
252
- if (serverConfig) {
253
- config.server = deepmerge(config.server ?? {}, serverConfig ?? {})
254
- }
255
-
256
- if ((hasManagementApi && config.metrics === undefined) || config.metrics) {
257
- const labels = config.metrics?.labels || {}
258
- config.metrics = {
259
- server: 'hide',
260
- defaultMetrics: { enabled: isEntrypoint },
261
- ...config.metrics,
262
- labels: { serviceId, ...labels }
263
- }
264
- }
265
-
266
- if (!isEntrypoint) {
267
- config.server = config.server ?? {}
268
- config.server.trustProxy = true
269
- }
270
-
271
- if (isProduction) {
272
- if (config.plugins) {
273
- config.plugins.typescript = false
274
- }
275
- config.watch = { enabled: false }
276
- }
277
-
278
- this.configManager.update(config)
279
- }
280
-
281
- #initLogger () {
282
- if (this.configManager.current.server?.loggerInstance) {
283
- this.logger = this.configManager.current.server?.loggerInstance
284
- return
285
- }
286
-
287
- this.configManager.current.server ??= {}
288
- this.loggerConfig = deepmerge(this.context.loggerConfig ?? {}, this.configManager.current.server?.logger ?? {})
289
-
290
- const pinoOptions = {
291
- level: this.loggerConfig?.level ?? 'trace'
292
- }
293
-
294
- if (this.context?.serviceId) {
295
- pinoOptions.name = this.context.serviceId
296
- }
297
-
298
- this.logger = pino(pinoOptions)
299
-
300
- // Only one of logger and loggerInstance should be set
301
- delete this.configManager.current.server.logger
302
- this.configManager.current.server.loggerInstance = this.logger
303
- }
304
- }
305
-
306
- module.exports = { ServiceStackable }