@lokalise/fastify-bullboard-plugin 1.0.0 → 1.1.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.
@@ -0,0 +1,11 @@
1
+ export type AuthConfig = {
2
+ isEnabled: boolean;
3
+ username: string;
4
+ password: string;
5
+ };
6
+ type BasicAuthOptions = {
7
+ config: AuthConfig;
8
+ enableList: Set<string>;
9
+ };
10
+ export declare const basicAuth: import("fastify").FastifyPluginCallback<BasicAuthOptions, import("fastify").RawServerDefault, import("fastify").FastifyTypeProviderDefault, import("fastify").FastifyBaseLogger>;
11
+ export {};
@@ -0,0 +1,40 @@
1
+ import fastifyAuth from '@fastify/auth';
2
+ import fastifyBasicAuth from '@fastify/basic-auth';
3
+ import { AuthFailedError } from '@lokalise/node-core';
4
+ import fp from 'fastify-plugin';
5
+ function validate(config) {
6
+ return (username, password, _req, _reply, done) => {
7
+ if (username === config.username && password === config.password) {
8
+ done();
9
+ }
10
+ else {
11
+ done(new AuthFailedError());
12
+ }
13
+ };
14
+ }
15
+ function enableListAuthHandler(enableList) {
16
+ return (req, _res, done) => {
17
+ if (req.routeOptions.url && !enableList.has(req.routeOptions.url)) {
18
+ return done();
19
+ }
20
+ return done(new AuthFailedError());
21
+ };
22
+ }
23
+ async function plugin(fastify, pluginOptions) {
24
+ if (!pluginOptions.config.isEnabled) {
25
+ return;
26
+ }
27
+ await fastify.register(fastifyAuth);
28
+ await fastify.register(fastifyBasicAuth, {
29
+ validate: validate(pluginOptions.config),
30
+ authenticate: true,
31
+ });
32
+ fastify.after(() => {
33
+ fastify.addHook('onRequest', fastify.auth([enableListAuthHandler(pluginOptions.enableList), fastify.basicAuth]));
34
+ });
35
+ }
36
+ export const basicAuth = fp(plugin, {
37
+ fastify: '>=4.0.0',
38
+ name: 'basic-auth',
39
+ });
40
+ //# sourceMappingURL=basicAuth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"basicAuth.js","sourceRoot":"","sources":["../src/basicAuth.ts"],"names":[],"mappings":"AACA,OAAO,WAAW,MAAM,eAAe,CAAA;AACvC,OAAO,gBAAgB,MAAM,qBAAqB,CAAA;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AAErD,OAAO,EAAE,MAAM,gBAAgB,CAAA;AAa/B,SAAS,QAAQ,CAAC,MAAkB;IAClC,OAAO,CACL,QAAgB,EAChB,QAAgB,EAChB,IAAoB,EACpB,MAAoB,EACpB,IAA2B,EAC3B,EAAE;QACF,IAAI,QAAQ,KAAK,MAAM,CAAC,QAAQ,IAAI,QAAQ,KAAK,MAAM,CAAC,QAAQ,EAAE,CAAC;YACjE,IAAI,EAAE,CAAA;QACR,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,IAAI,eAAe,EAAE,CAAC,CAAA;QAC7B,CAAC;IACH,CAAC,CAAA;AACH,CAAC;AAED,SAAS,qBAAqB,CAAC,UAAuB;IACpD,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE;QACzB,IAAI,GAAG,CAAC,YAAY,CAAC,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC;YAClE,OAAO,IAAI,EAAE,CAAA;QACf,CAAC;QAED,OAAO,IAAI,CAAC,IAAI,eAAe,EAAE,CAAC,CAAA;IACpC,CAAC,CAAA;AACH,CAAC;AAED,KAAK,UAAU,MAAM,CAAC,OAAwB,EAAE,aAA+B;IAC7E,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;QACpC,OAAM;IACR,CAAC;IAED,MAAM,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAA;IACnC,MAAM,OAAO,CAAC,QAAQ,CAAC,gBAAgB,EAAE;QACvC,QAAQ,EAAE,QAAQ,CAAC,aAAa,CAAC,MAAM,CAAC;QACxC,YAAY,EAAE,IAAI;KACnB,CAAC,CAAA;IAEF,OAAO,CAAC,KAAK,CAAC,GAAG,EAAE;QACjB,OAAO,CAAC,OAAO,CACb,WAAW,EACX,OAAO,CAAC,IAAI,CAAC,CAAC,qBAAqB,CAAC,aAAa,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC,CACnF,CAAA;IACH,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,SAAS,GAAG,EAAE,CAAmB,MAAM,EAAE;IACpD,OAAO,EAAE,SAAS;IAClB,IAAI,EAAE,YAAY;CACnB,CAAC,CAAA"}
@@ -0,0 +1,21 @@
1
+ import { type RedisConfig } from '@lokalise/node-core';
2
+ import type { Queue, QueueOptions, RedisConnection } from 'bullmq';
3
+ import type { Redis } from 'ioredis';
4
+ export type BullBoardOptions = {
5
+ queueConstructor: QueueProConstructor | QueueConstructor;
6
+ basePath: string;
7
+ refreshIntervalInSeconds?: number;
8
+ } & ({
9
+ redisInstances: Redis[];
10
+ redisConfigs?: never;
11
+ } | {
12
+ redisInstances?: never;
13
+ redisConfigs: RedisConfig[];
14
+ });
15
+ export interface QueueProConstructor {
16
+ new (name: string, opts?: Record<string, unknown>): Queue;
17
+ }
18
+ export interface QueueConstructor {
19
+ new (name: string, opts?: QueueOptions, Connection?: typeof RedisConnection): Queue;
20
+ }
21
+ export declare const bullBoard: import("fastify").FastifyPluginCallback<BullBoardOptions, import("fastify").RawServerDefault, import("fastify").FastifyTypeProviderDefault, import("fastify").FastifyBaseLogger>;
@@ -0,0 +1,72 @@
1
+ import { constants as httpConstants } from 'node:http2';
2
+ import { createBullBoard } from '@bull-board/api';
3
+ // @ts-ignore
4
+ import { BullMQAdapter } from '@bull-board/api/bullMQAdapter.js';
5
+ import { FastifyAdapter } from '@bull-board/fastify';
6
+ import fastifySchedule from '@fastify/schedule';
7
+ import { backgroundJobProcessorGetActiveQueueIds, createSanitizedRedisClient, QUEUE_GROUP_DELIMITER, } from '@lokalise/background-jobs-common';
8
+ import { resolveGlobalErrorLogObject } from '@lokalise/node-core';
9
+ import fp from 'fastify-plugin';
10
+ import { AsyncTask, SimpleIntervalJob } from 'toad-scheduler';
11
+ function containStatusCode(error) {
12
+ return Object.hasOwn(error, 'statusCode');
13
+ }
14
+ const bullBoardErrorHandler = (error) => {
15
+ const status = containStatusCode(error)
16
+ ? error.statusCode
17
+ : httpConstants.HTTP_STATUS_INTERNAL_SERVER_ERROR;
18
+ return {
19
+ status: status,
20
+ body: { message: error.message, details: error.stack },
21
+ };
22
+ };
23
+ async function getCurrentQueues(redisInstances, queueConstructor) {
24
+ const queueIds = await Promise.all(redisInstances.map((redis) => backgroundJobProcessorGetActiveQueueIds(redis)));
25
+ return queueIds
26
+ .flatMap((ids, index) =>
27
+ // biome-ignore lint/style/noNonNullAssertion: Should exist
28
+ ids.map((id) => new queueConstructor(id, { connection: redisInstances[index] })))
29
+ .map((queue) => new BullMQAdapter(queue, { delimiter: QUEUE_GROUP_DELIMITER }));
30
+ }
31
+ async function scheduleUpdates(fastify, bullBoard, resolvedRedisInstances, pluginOptions) {
32
+ const { refreshIntervalInSeconds, queueConstructor } = pluginOptions;
33
+ if (!refreshIntervalInSeconds || refreshIntervalInSeconds <= 0)
34
+ return;
35
+ const refreshTask = new AsyncTask('Bull-board - update queues', async () => {
36
+ fastify.log.debug({ refreshIntervalInSeconds }, 'Bull-dashboard -> updating queues');
37
+ bullBoard.replaceQueues(await getCurrentQueues(resolvedRedisInstances, queueConstructor));
38
+ }, (e) => fastify.log.error(resolveGlobalErrorLogObject(e)));
39
+ await fastify.register(fastifySchedule);
40
+ fastify.scheduler.addSimpleIntervalJob(new SimpleIntervalJob({ seconds: refreshIntervalInSeconds }, refreshTask, {
41
+ id: 'bull-board-queues-update',
42
+ preventOverrun: true,
43
+ }));
44
+ }
45
+ const resolveRedisInstances = (options) => {
46
+ if (options.redisInstances)
47
+ return options.redisInstances;
48
+ if (options.redisConfigs)
49
+ return options.redisConfigs.map(createSanitizedRedisClient);
50
+ throw new Error('Either `redisInstances` or `redisConfigs` must be provided in BullBoardOptions');
51
+ };
52
+ const plugin = async (fastify, pluginOptions) => {
53
+ const { basePath, queueConstructor } = pluginOptions;
54
+ const resolvedRedisInstances = resolveRedisInstances(pluginOptions);
55
+ const serverAdapter = new FastifyAdapter();
56
+ const bullBoard = createBullBoard({
57
+ queues: await getCurrentQueues(resolvedRedisInstances, queueConstructor),
58
+ serverAdapter,
59
+ });
60
+ // biome-ignore lint/suspicious/noExplicitAny: bull-board is not exporting this type
61
+ serverAdapter.setErrorHandler(bullBoardErrorHandler);
62
+ serverAdapter.setBasePath(basePath);
63
+ await fastify.register(serverAdapter.registerPlugin(), {
64
+ prefix: basePath,
65
+ });
66
+ await scheduleUpdates(fastify, bullBoard, resolvedRedisInstances, pluginOptions);
67
+ };
68
+ export const bullBoard = fp(plugin, {
69
+ fastify: '>=4.0.0',
70
+ name: 'bull-board',
71
+ });
72
+ //# sourceMappingURL=bullBoard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bullBoard.js","sourceRoot":"","sources":["../src/bullBoard.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ import type { ErrorReporter } from '@lokalise/node-core';
2
+ export type ErrorReporterOptions = {
3
+ errorReporter: ErrorReporter;
4
+ };
5
+ export declare const errorReporterPlugin: import("fastify").FastifyPluginCallback<ErrorReporterOptions, import("fastify").RawServerDefault, import("fastify").FastifyTypeProviderDefault, import("fastify").FastifyBaseLogger>;
@@ -0,0 +1,51 @@
1
+ import { constants as httpConstants } from 'node:http2';
2
+ import { isError, isInternalError, isObject, isPublicNonRecoverableError, } from '@lokalise/node-core';
3
+ import fp from 'fastify-plugin';
4
+ import { pino } from 'pino';
5
+ function resolveLogObject(error) {
6
+ if (isInternalError(error)) {
7
+ return {
8
+ message: error.message,
9
+ code: error.errorCode,
10
+ details: error.details ? JSON.stringify(error.details) : undefined,
11
+ error: pino.stdSerializers.err({
12
+ name: error.name,
13
+ message: error.message,
14
+ stack: error.stack,
15
+ }),
16
+ };
17
+ }
18
+ if (isError(error)) {
19
+ return {
20
+ message: error.message,
21
+ error: pino.stdSerializers.err({
22
+ name: error.name,
23
+ message: error.message,
24
+ stack: error.stack,
25
+ }),
26
+ };
27
+ }
28
+ return {
29
+ message: isObject(error) ? error.message : JSON.stringify(error),
30
+ error,
31
+ };
32
+ }
33
+ function plugin(fastify, pluginOptions, next) {
34
+ fastify.addHook('onError', (request, _reply, error, done) => {
35
+ let statusCode = error.statusCode ?? httpConstants.HTTP_STATUS_INTERNAL_SERVER_ERROR;
36
+ if (isPublicNonRecoverableError(error)) {
37
+ statusCode = error.httpStatusCode;
38
+ }
39
+ if (statusCode >= httpConstants.HTTP_STATUS_INTERNAL_SERVER_ERROR) {
40
+ request.log.error(resolveLogObject(error));
41
+ pluginOptions.errorReporter.report({ error });
42
+ }
43
+ done();
44
+ });
45
+ next();
46
+ }
47
+ export const errorReporterPlugin = fp(plugin, {
48
+ fastify: '>=4.0.0',
49
+ name: 'error-reporter',
50
+ });
51
+ //# sourceMappingURL=errorReporterPlugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errorReporterPlugin.js","sourceRoot":"","sources":["../src/errorReporterPlugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,IAAI,aAAa,EAAE,MAAM,YAAY,CAAA;AAGvD,OAAO,EACL,OAAO,EACP,eAAe,EACf,QAAQ,EACR,2BAA2B,GAC5B,MAAM,qBAAqB,CAAA;AAE5B,OAAO,EAAE,MAAM,gBAAgB,CAAA;AAC/B,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAM3B,SAAS,gBAAgB,CAAC,KAAc;IACtC,IAAI,eAAe,CAAC,KAAK,CAAC,EAAE,CAAC;QAC3B,OAAO;YACL,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,IAAI,EAAE,KAAK,CAAC,SAAS;YACrB,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS;YAClE,KAAK,EAAE,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC;gBAC7B,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,OAAO,EAAE,KAAK,CAAC,OAAO;gBACtB,KAAK,EAAE,KAAK,CAAC,KAAK;aACnB,CAAC;SACH,CAAA;IACH,CAAC;IACD,IAAI,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACnB,OAAO;YACL,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,KAAK,EAAE,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC;gBAC7B,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,OAAO,EAAE,KAAK,CAAC,OAAO;gBACtB,KAAK,EAAE,KAAK,CAAC,KAAK;aACnB,CAAC;SACH,CAAA;IACH,CAAC;IAED,OAAO;QACL,OAAO,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;QAChE,KAAK;KACN,CAAA;AACH,CAAC;AAED,SAAS,MAAM,CACb,OAAwB,EACxB,aAAmC,EACnC,IAA2B;IAE3B,OAAO,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;QAC1D,IAAI,UAAU,GAAG,KAAK,CAAC,UAAU,IAAI,aAAa,CAAC,iCAAiC,CAAA;QACpF,IAAI,2BAA2B,CAAC,KAAK,CAAC,EAAE,CAAC;YACvC,UAAU,GAAG,KAAK,CAAC,cAAc,CAAA;QACnC,CAAC;QAED,IAAI,UAAU,IAAI,aAAa,CAAC,iCAAiC,EAAE,CAAC;YAClE,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC,CAAA;YAC1C,aAAa,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC,CAAA;QAC/C,CAAC;QAED,IAAI,EAAE,CAAA;IACR,CAAC,CAAC,CAAA;IAEF,IAAI,EAAE,CAAA;AACR,CAAC;AAED,MAAM,CAAC,MAAM,mBAAmB,GAAG,EAAE,CAAuB,MAAM,EAAE;IAClE,OAAO,EAAE,SAAS;IAClB,IAAI,EAAE,gBAAgB;CACvB,CAAC,CAAA"}
@@ -1,3 +1,3 @@
1
- export { type AuthConfig, basicAuth } from './basicAuth.js'
2
- export { type BullBoardOptions, bullBoard, type QueueProConstructor } from './bullBoard.js'
3
- export { type ErrorReporterOptions, errorReporterPlugin } from './errorReporterPlugin.js'
1
+ export { type AuthConfig, basicAuth } from './basicAuth.js';
2
+ export { type BullBoardOptions, bullBoard, type QueueProConstructor } from './bullBoard.js';
3
+ export { type ErrorReporterOptions, errorReporterPlugin } from './errorReporterPlugin.js';
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { basicAuth } from './basicAuth.js';
2
+ export { bullBoard } from './bullBoard.js';
3
+ export { errorReporterPlugin } from './errorReporterPlugin.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC3D,OAAO,EAAyB,SAAS,EAA4B,MAAM,gBAAgB,CAAA;AAC3F,OAAO,EAA6B,mBAAmB,EAAE,MAAM,0BAA0B,CAAA"}
package/package.json CHANGED
@@ -1,7 +1,12 @@
1
1
  {
2
2
  "name": "@lokalise/fastify-bullboard-plugin",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
+ "files": [
6
+ "dist",
7
+ "README.md",
8
+ "LICENSE.md"
9
+ ],
5
10
  "engines": {
6
11
  "node": ">=20.13.0"
7
12
  },
@@ -1,79 +0,0 @@
1
- import fastify, { type FastifyInstance } from 'fastify'
2
- import { beforeAll, describe, expect } from 'vitest'
3
-
4
- import { basicAuth } from './basicAuth.js'
5
-
6
- const skippedEndpoint = '/skipAuth'
7
- const enabledEndpoint = '/'
8
-
9
- async function initApp(authEnabled: boolean) {
10
- const app = fastify()
11
- await app.register(basicAuth, {
12
- config: { isEnabled: authEnabled, username: 'test', password: 'test' },
13
- enableList: new Set([enabledEndpoint]),
14
- })
15
-
16
- app.route({
17
- method: 'GET',
18
- url: '/',
19
- handler: (_req, reply) => {
20
- return reply.status(200).send({ hello: 'world' })
21
- },
22
- })
23
-
24
- app.route({
25
- method: 'GET',
26
- url: skippedEndpoint,
27
- handler: (_req, reply) => {
28
- return reply.status(200).send({ hello: 'my friend' })
29
- },
30
- })
31
-
32
- await app.ready()
33
- return app
34
- }
35
-
36
- describe('basicAuth', () => {
37
- let app: FastifyInstance
38
-
39
- afterAll(async () => {
40
- await app.close()
41
- })
42
-
43
- const performRequest = async (path: string) => app.inject().get(path).end()
44
-
45
- describe('auth enabled', () => {
46
- beforeAll(async () => {
47
- app = await initApp(true)
48
- })
49
-
50
- it('returns 401 on not skipped endpoints', async () => {
51
- const response = await performRequest('/')
52
- expect(response.statusCode).toBe(401)
53
- })
54
-
55
- it('works on skipped endpoints', async () => {
56
- const response = await performRequest(skippedEndpoint)
57
- expect(response.statusCode).toBe(200)
58
- expect(response.json()).toEqual({ hello: 'my friend' })
59
- })
60
- })
61
-
62
- describe('auth disabled', () => {
63
- beforeAll(async () => {
64
- app = await initApp(false)
65
- })
66
-
67
- it('works on not skipped endpoints', async () => {
68
- const response = await performRequest('/')
69
- expect(response.statusCode).toBe(200)
70
- expect(response.json()).toEqual({ hello: 'world' })
71
- })
72
-
73
- it('works on skipped endpoints', async () => {
74
- const response = await performRequest(skippedEndpoint)
75
- expect(response.statusCode).toBe(200)
76
- expect(response.json()).toEqual({ hello: 'my friend' })
77
- })
78
- })
79
- })
package/src/basicAuth.ts DELETED
@@ -1,67 +0,0 @@
1
- import type { FastifyAuthFunction } from '@fastify/auth'
2
- import fastifyAuth from '@fastify/auth'
3
- import fastifyBasicAuth from '@fastify/basic-auth'
4
- import { AuthFailedError } from '@lokalise/node-core'
5
- import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'
6
- import fp from 'fastify-plugin'
7
-
8
- export type AuthConfig = {
9
- isEnabled: boolean
10
- username: string
11
- password: string
12
- }
13
-
14
- type BasicAuthOptions = {
15
- config: AuthConfig
16
- enableList: Set<string>
17
- }
18
-
19
- function validate(config: AuthConfig) {
20
- return (
21
- username: string,
22
- password: string,
23
- _req: FastifyRequest,
24
- _reply: FastifyReply,
25
- done: (err?: Error) => void,
26
- ) => {
27
- if (username === config.username && password === config.password) {
28
- done()
29
- } else {
30
- done(new AuthFailedError())
31
- }
32
- }
33
- }
34
-
35
- function enableListAuthHandler(enableList: Set<string>): FastifyAuthFunction {
36
- return (req, _res, done) => {
37
- if (req.routeOptions.url && !enableList.has(req.routeOptions.url)) {
38
- return done()
39
- }
40
-
41
- return done(new AuthFailedError())
42
- }
43
- }
44
-
45
- async function plugin(fastify: FastifyInstance, pluginOptions: BasicAuthOptions): Promise<void> {
46
- if (!pluginOptions.config.isEnabled) {
47
- return
48
- }
49
-
50
- await fastify.register(fastifyAuth)
51
- await fastify.register(fastifyBasicAuth, {
52
- validate: validate(pluginOptions.config),
53
- authenticate: true,
54
- })
55
-
56
- fastify.after(() => {
57
- fastify.addHook(
58
- 'onRequest',
59
- fastify.auth([enableListAuthHandler(pluginOptions.enableList), fastify.basicAuth]),
60
- )
61
- })
62
- }
63
-
64
- export const basicAuth = fp<BasicAuthOptions>(plugin, {
65
- fastify: '>=4.0.0',
66
- name: 'basic-auth',
67
- })
@@ -1,67 +0,0 @@
1
- import { Queue } from 'bullmq'
2
- import fastify, { type FastifyInstance } from 'fastify'
3
- import { beforeAll, expect } from 'vitest'
4
- import { type BullBoardOptions, bullBoard, type QueueProConstructor } from './bullBoard.js'
5
-
6
- const QueuePro: QueueProConstructor = Queue as QueueProConstructor
7
-
8
- async function initApp(options: BullBoardOptions) {
9
- const app = fastify()
10
-
11
- await app.register(bullBoard, options)
12
-
13
- await app.ready()
14
- return app
15
- }
16
-
17
- describe('bull board', () => {
18
- let app: FastifyInstance
19
-
20
- afterAll(async () => {
21
- await app.close()
22
- })
23
-
24
- describe('refresh disabled', () => {
25
- beforeAll(async () => {
26
- app = await initApp({
27
- queueConstructor: QueuePro,
28
- redisInstances: [],
29
- basePath: '/test-disabled',
30
- })
31
- })
32
-
33
- it('works', async () => {
34
- const response = await app.inject().get('/test-disabled').end()
35
-
36
- expect(response.statusCode).toBe(200)
37
- expect(response.body.toLowerCase()).includes('<!doctype html>')
38
- expect(response.body.toLowerCase()).includes('<title>bull dashboard</title>')
39
-
40
- expect(app.scheduler).toBeUndefined()
41
- })
42
- })
43
-
44
- describe('refresh enabled', () => {
45
- beforeAll(async () => {
46
- app = await initApp({
47
- queueConstructor: Queue,
48
- redisInstances: [],
49
- basePath: '/test-enabled',
50
- refreshIntervalInSeconds: 1,
51
- })
52
- })
53
-
54
- it('works', async () => {
55
- const response = await app.inject().get('/test-enabled').end()
56
-
57
- expect(response.statusCode).toBe(200)
58
- expect(response.body.toLowerCase()).includes('<!doctype html>')
59
- expect(response.body.toLowerCase()).includes('<title>bull dashboard</title>')
60
-
61
- expect(app.scheduler).toBeDefined()
62
- const jobs = app.scheduler.getAllJobs()
63
- expect(jobs).toHaveLength(1)
64
- expect(jobs[0]!.id).toBe('bull-board-queues-update')
65
- })
66
- })
67
- })
package/src/bullBoard.ts DELETED
@@ -1,112 +0,0 @@
1
- import { constants as httpConstants } from 'node:http2'
2
- import { createBullBoard } from '@bull-board/api'
3
- // @ts-ignore
4
- import { BullMQAdapter } from '@bull-board/api/bullMQAdapter.js'
5
- import { FastifyAdapter } from '@bull-board/fastify'
6
- import fastifySchedule from '@fastify/schedule'
7
- import {
8
- backgroundJobProcessorGetActiveQueueIds,
9
- QUEUE_GROUP_DELIMITER,
10
- } from '@lokalise/background-jobs-common'
11
- import { resolveGlobalErrorLogObject } from '@lokalise/node-core'
12
- import type { Queue, QueueOptions, RedisConnection } from 'bullmq'
13
- import type { FastifyInstance } from 'fastify'
14
- import fp from 'fastify-plugin'
15
- import type { Redis } from 'ioredis'
16
- import { AsyncTask, SimpleIntervalJob } from 'toad-scheduler'
17
-
18
- export type BullBoardOptions = {
19
- queueConstructor: QueueProConstructor | QueueConstructor
20
- redisInstances: Redis[]
21
- basePath: string
22
- refreshIntervalInSeconds?: number
23
- }
24
-
25
- function containStatusCode(error: Error): error is Error & { statusCode: number } {
26
- return Object.hasOwn(error, 'statusCode')
27
- }
28
-
29
- const bullBoardErrorHandler = (error: Error) => {
30
- const status = containStatusCode(error)
31
- ? error.statusCode
32
- : httpConstants.HTTP_STATUS_INTERNAL_SERVER_ERROR
33
- return {
34
- status: status,
35
- body: { message: error.message, details: error.stack },
36
- }
37
- }
38
-
39
- export interface QueueProConstructor {
40
- new (name: string, opts?: Record<string, unknown>): Queue
41
- }
42
-
43
- export interface QueueConstructor {
44
- new (name: string, opts?: QueueOptions, Connection?: typeof RedisConnection): Queue
45
- }
46
-
47
- async function getCurrentQueues(
48
- redisInstances: Redis[],
49
- queueConstructor: QueueProConstructor | QueueConstructor,
50
- ): Promise<BullMQAdapter[]> {
51
- const queueIds = await Promise.all(
52
- redisInstances.map((redis) => backgroundJobProcessorGetActiveQueueIds(redis)),
53
- )
54
-
55
- return queueIds
56
- .flatMap((ids, index) =>
57
- // biome-ignore lint/style/noNonNullAssertion: Should exist
58
- ids.map((id) => new queueConstructor(id, { connection: redisInstances[index]! })),
59
- )
60
- .map((queue) => new BullMQAdapter(queue, { delimiter: QUEUE_GROUP_DELIMITER }))
61
- }
62
-
63
- async function scheduleUpdates(
64
- fastify: FastifyInstance,
65
- bullBoard: ReturnType<typeof createBullBoard>,
66
- pluginOptions: BullBoardOptions,
67
- ) {
68
- const { refreshIntervalInSeconds, redisInstances, queueConstructor } = pluginOptions
69
-
70
- if (!refreshIntervalInSeconds || refreshIntervalInSeconds <= 0) return
71
-
72
- const refreshTask = new AsyncTask(
73
- 'Bull-board - update queues',
74
- async () => {
75
- fastify.log.debug({ refreshIntervalInSeconds }, 'Bull-dashboard -> updating queues')
76
- bullBoard.replaceQueues(await getCurrentQueues(redisInstances, queueConstructor))
77
- },
78
- (e) => fastify.log.error(resolveGlobalErrorLogObject(e)),
79
- )
80
-
81
- await fastify.register(fastifySchedule)
82
- fastify.scheduler.addSimpleIntervalJob(
83
- new SimpleIntervalJob({ seconds: refreshIntervalInSeconds }, refreshTask, {
84
- id: 'bull-board-queues-update',
85
- preventOverrun: true,
86
- }),
87
- )
88
- }
89
-
90
- const plugin = async (fastify: FastifyInstance, pluginOptions: BullBoardOptions) => {
91
- const { basePath, redisInstances, queueConstructor } = pluginOptions
92
-
93
- const serverAdapter = new FastifyAdapter()
94
- const bullBoard = createBullBoard({
95
- queues: await getCurrentQueues(redisInstances, queueConstructor),
96
- serverAdapter,
97
- })
98
- // biome-ignore lint/suspicious/noExplicitAny: bull-board is not exporting this type
99
- serverAdapter.setErrorHandler(bullBoardErrorHandler as any)
100
- serverAdapter.setBasePath(basePath)
101
-
102
- await fastify.register(serverAdapter.registerPlugin(), {
103
- prefix: basePath,
104
- })
105
-
106
- await scheduleUpdates(fastify, bullBoard, pluginOptions)
107
- }
108
-
109
- export const bullBoard = fp<BullBoardOptions>(plugin, {
110
- fastify: '>=4.0.0',
111
- name: 'bull-board',
112
- })
@@ -1,127 +0,0 @@
1
- import { type ErrorReporter, InternalError, PublicNonRecoverableError } from '@lokalise/node-core'
2
- import fastify, { type FastifyInstance, type RouteHandlerMethod } from 'fastify'
3
- import { pino } from 'pino'
4
- import { afterAll, describe, expect, it, type MockInstance, vi } from 'vitest'
5
-
6
- import { FakeErrorReporter } from '../test/FakeErrorReporter.js'
7
- import { errorReporterPlugin } from './errorReporterPlugin.js'
8
-
9
- async function initApp(routeHandler: RouteHandlerMethod, errorReporter: ErrorReporter) {
10
- const app = fastify({
11
- logger: true,
12
- })
13
- await app.register(errorReporterPlugin, { errorReporter })
14
-
15
- app.route({
16
- method: 'GET',
17
- url: '/',
18
- handler: routeHandler,
19
- })
20
-
21
- await app.ready()
22
- return app
23
- }
24
-
25
- describe('errorHandlerPlugin', () => {
26
- let app: FastifyInstance
27
-
28
- afterAll(async () => {
29
- await app.close()
30
- })
31
-
32
- const performRequest = async () => app.inject().get('/').end()
33
-
34
- it('generic errors are reported', async () => {
35
- const fakeErrorReporter = new FakeErrorReporter()
36
- const error = new Error('Generic error')
37
- let logSpy: MockInstance | undefined
38
- app = await initApp((req) => {
39
- logSpy = vi.spyOn(req.log, 'error')
40
- throw error
41
- }, fakeErrorReporter)
42
-
43
- await performRequest()
44
-
45
- expect(fakeErrorReporter.calls).length(1)
46
- expect(fakeErrorReporter.calls[0]).toEqual({ error })
47
- expect(logSpy).toHaveBeenCalledWith({
48
- message: error.message,
49
- error: pino.stdSerializers.err({
50
- name: error.name,
51
- message: error.message,
52
- stack: error.stack,
53
- }),
54
- })
55
- })
56
-
57
- it('internal errors are reported', async () => {
58
- const fakeErrorReporter = new FakeErrorReporter()
59
- const error = new InternalError({
60
- message: 'Internal error',
61
- errorCode: 'INTERNAL_ERROR',
62
- })
63
- let logSpy: MockInstance | undefined
64
- app = await initApp((req) => {
65
- logSpy = vi.spyOn(req.log, 'error')
66
- throw error
67
- }, fakeErrorReporter)
68
-
69
- await performRequest()
70
-
71
- expect(fakeErrorReporter.calls).length(1)
72
- expect(fakeErrorReporter.calls[0]).toEqual({ error })
73
- expect(logSpy).toHaveBeenCalledWith({
74
- message: error.message,
75
- code: error.errorCode,
76
- details: error.details ? JSON.stringify(error.details) : undefined,
77
- error: pino.stdSerializers.err({
78
- name: error.name,
79
- message: error.message,
80
- stack: error.stack,
81
- }),
82
- })
83
- })
84
-
85
- it('5xx errors are reported', async () => {
86
- const fakeErrorReporter = new FakeErrorReporter()
87
- const error = new PublicNonRecoverableError({
88
- httpStatusCode: 500,
89
- message: 'Internal error',
90
- errorCode: 'INTERNAL_ERROR',
91
- })
92
- let logSpy: MockInstance | undefined
93
- app = await initApp((req) => {
94
- logSpy = vi.spyOn(req.log, 'error')
95
- throw error
96
- }, fakeErrorReporter)
97
-
98
- await performRequest()
99
-
100
- expect(fakeErrorReporter.calls).length(1)
101
- expect(fakeErrorReporter.calls[0]).toEqual({ error })
102
- expect(logSpy).toHaveBeenCalledWith({
103
- message: error.message,
104
- error: pino.stdSerializers.err({
105
- name: error.name,
106
- message: error.message,
107
- stack: error.stack,
108
- }),
109
- })
110
- })
111
-
112
- it('4xx erros are ignored', async () => {
113
- const fakeErrorReporter = new FakeErrorReporter()
114
- const error = new PublicNonRecoverableError({
115
- httpStatusCode: 400,
116
- message: 'test',
117
- errorCode: 'TEST',
118
- })
119
- app = await initApp(() => {
120
- throw error
121
- }, fakeErrorReporter)
122
-
123
- await performRequest()
124
-
125
- expect(fakeErrorReporter.calls).length(0)
126
- })
127
- })
@@ -1,73 +0,0 @@
1
- import { constants as httpConstants } from 'node:http2'
2
-
3
- import type { ErrorReporter } from '@lokalise/node-core'
4
- import {
5
- isError,
6
- isInternalError,
7
- isObject,
8
- isPublicNonRecoverableError,
9
- } from '@lokalise/node-core'
10
- import type { FastifyInstance } from 'fastify'
11
- import fp from 'fastify-plugin'
12
- import { pino } from 'pino'
13
-
14
- export type ErrorReporterOptions = {
15
- errorReporter: ErrorReporter
16
- }
17
-
18
- function resolveLogObject(error: unknown): Record<string, unknown> {
19
- if (isInternalError(error)) {
20
- return {
21
- message: error.message,
22
- code: error.errorCode,
23
- details: error.details ? JSON.stringify(error.details) : undefined,
24
- error: pino.stdSerializers.err({
25
- name: error.name,
26
- message: error.message,
27
- stack: error.stack,
28
- }),
29
- }
30
- }
31
- if (isError(error)) {
32
- return {
33
- message: error.message,
34
- error: pino.stdSerializers.err({
35
- name: error.name,
36
- message: error.message,
37
- stack: error.stack,
38
- }),
39
- }
40
- }
41
-
42
- return {
43
- message: isObject(error) ? error.message : JSON.stringify(error),
44
- error,
45
- }
46
- }
47
-
48
- function plugin(
49
- fastify: FastifyInstance,
50
- pluginOptions: ErrorReporterOptions,
51
- next: (err?: Error) => void,
52
- ) {
53
- fastify.addHook('onError', (request, _reply, error, done) => {
54
- let statusCode = error.statusCode ?? httpConstants.HTTP_STATUS_INTERNAL_SERVER_ERROR
55
- if (isPublicNonRecoverableError(error)) {
56
- statusCode = error.httpStatusCode
57
- }
58
-
59
- if (statusCode >= httpConstants.HTTP_STATUS_INTERNAL_SERVER_ERROR) {
60
- request.log.error(resolveLogObject(error))
61
- pluginOptions.errorReporter.report({ error })
62
- }
63
-
64
- done()
65
- })
66
-
67
- next()
68
- }
69
-
70
- export const errorReporterPlugin = fp<ErrorReporterOptions>(plugin, {
71
- fastify: '>=4.0.0',
72
- name: 'error-reporter',
73
- })
@@ -1,18 +0,0 @@
1
- import type { ErrorReport } from '@lokalise/fastify-extras'
2
- import type { ErrorReporter } from '@lokalise/node-core'
3
-
4
- export class FakeErrorReporter implements ErrorReporter {
5
- private _calls: ErrorReport[] = []
6
-
7
- report(errorReport: ErrorReport): void {
8
- this._calls.push(errorReport)
9
- }
10
-
11
- clean(): void {
12
- this._calls = []
13
- }
14
-
15
- get calls(): ErrorReport[] {
16
- return this._calls
17
- }
18
- }
@@ -1,5 +0,0 @@
1
- {
2
- "extends": ["./tsconfig.json", "@lokalise/tsconfig/build-public-lib"],
3
- "include": ["src/**/*"],
4
- "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"]
5
- }
package/tsconfig.json DELETED
@@ -1,7 +0,0 @@
1
- {
2
- "extends": "@lokalise/tsconfig/tsc",
3
- "include": ["src/**/*", "vitest.config.ts"],
4
- "compilerOptions": {
5
- "types": ["vitest/globals"]
6
- }
7
- }
package/vitest.config.ts DELETED
@@ -1,22 +0,0 @@
1
- import { defineConfig } from 'vitest/config'
2
-
3
- // biome-ignore lint/style/noDefaultExport: vite expects default export
4
- export default defineConfig({
5
- test: {
6
- globals: true,
7
- watch: false,
8
- restoreMocks: true,
9
- pool: 'threads',
10
- coverage: {
11
- provider: 'v8',
12
- include: ['src/**/*.ts'],
13
- exclude: ['src/index.ts'],
14
- thresholds: {
15
- lines: 84,
16
- functions: 80,
17
- branches: 86,
18
- statements: 84,
19
- },
20
- },
21
- },
22
- })