@lokalise/fastify-bullboard-plugin 1.0.0 → 1.0.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/dist/basicAuth.d.ts +11 -0
- package/dist/basicAuth.js +40 -0
- package/dist/basicAuth.js.map +1 -0
- package/dist/bullBoard.d.ts +15 -0
- package/dist/bullBoard.js +64 -0
- package/dist/bullBoard.js.map +1 -0
- package/dist/errorReporterPlugin.d.ts +5 -0
- package/dist/errorReporterPlugin.js +51 -0
- package/dist/errorReporterPlugin.js.map +1 -0
- package/{src/index.ts → dist/index.d.ts} +3 -3
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/package.json +6 -1
- package/src/basicAuth.spec.ts +0 -79
- package/src/basicAuth.ts +0 -67
- package/src/bullBoard.spec.ts +0 -67
- package/src/bullBoard.ts +0 -112
- package/src/errorReporterPlugin.spec.ts +0 -127
- package/src/errorReporterPlugin.ts +0 -73
- package/test/FakeErrorReporter.ts +0 -18
- package/tsconfig.build.json +0 -5
- package/tsconfig.json +0 -7
- package/vitest.config.ts +0 -22
|
@@ -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,15 @@
|
|
|
1
|
+
import type { Queue, QueueOptions, RedisConnection } from 'bullmq';
|
|
2
|
+
import type { Redis } from 'ioredis';
|
|
3
|
+
export type BullBoardOptions = {
|
|
4
|
+
queueConstructor: QueueProConstructor | QueueConstructor;
|
|
5
|
+
redisInstances: Redis[];
|
|
6
|
+
basePath: string;
|
|
7
|
+
refreshIntervalInSeconds?: number;
|
|
8
|
+
};
|
|
9
|
+
export interface QueueProConstructor {
|
|
10
|
+
new (name: string, opts?: Record<string, unknown>): Queue;
|
|
11
|
+
}
|
|
12
|
+
export interface QueueConstructor {
|
|
13
|
+
new (name: string, opts?: QueueOptions, Connection?: typeof RedisConnection): Queue;
|
|
14
|
+
}
|
|
15
|
+
export declare const bullBoard: import("fastify").FastifyPluginCallback<BullBoardOptions, import("fastify").RawServerDefault, import("fastify").FastifyTypeProviderDefault, import("fastify").FastifyBaseLogger>;
|
|
@@ -0,0 +1,64 @@
|
|
|
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, 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, pluginOptions) {
|
|
32
|
+
const { refreshIntervalInSeconds, redisInstances, 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(redisInstances, 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 plugin = async (fastify, pluginOptions) => {
|
|
46
|
+
const { basePath, redisInstances, queueConstructor } = pluginOptions;
|
|
47
|
+
const serverAdapter = new FastifyAdapter();
|
|
48
|
+
const bullBoard = createBullBoard({
|
|
49
|
+
queues: await getCurrentQueues(redisInstances, queueConstructor),
|
|
50
|
+
serverAdapter,
|
|
51
|
+
});
|
|
52
|
+
// biome-ignore lint/suspicious/noExplicitAny: bull-board is not exporting this type
|
|
53
|
+
serverAdapter.setErrorHandler(bullBoardErrorHandler);
|
|
54
|
+
serverAdapter.setBasePath(basePath);
|
|
55
|
+
await fastify.register(serverAdapter.registerPlugin(), {
|
|
56
|
+
prefix: basePath,
|
|
57
|
+
});
|
|
58
|
+
await scheduleUpdates(fastify, bullBoard, pluginOptions);
|
|
59
|
+
};
|
|
60
|
+
export const bullBoard = fp(plugin, {
|
|
61
|
+
fastify: '>=4.0.0',
|
|
62
|
+
name: 'bull-board',
|
|
63
|
+
});
|
|
64
|
+
//# 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 @@
|
|
|
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
package/src/basicAuth.spec.ts
DELETED
|
@@ -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
|
-
})
|
package/src/bullBoard.spec.ts
DELETED
|
@@ -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
|
-
}
|
package/tsconfig.build.json
DELETED
package/tsconfig.json
DELETED
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
|
-
})
|