diff --git a/README.md b/README.md index 844ddf8..ea19d2d 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Note: We have disabled HADOLINT for now as we are getting an error: `qemu: uncau The CI Pipeline uses the `test` target from the Dockerfile to run the tests. You can run it locally with the following command: ```bash -docker compose -f docker-compose.test.yml up --exit-code-from app +docker compose -f docker-compose.test.yml up --exit-code-from app --build ``` Note: This will create a /coverage folder where you can review the coverage details. @@ -37,5 +37,5 @@ docker compose -f docker-compose.dev.yml up #### Supported Services -- MongoDB -- Redis +- MongoDB ([mongoose](https://mongoosejs.com/)) +- Redis ([BullMQ](https://bullmq.io/) and [bull-board](https://github.com/felixmosh/bull-board)) diff --git a/__test__/app.test.ts b/__test__/app.test.ts index 2fa934a..68714ea 100644 --- a/__test__/app.test.ts +++ b/__test__/app.test.ts @@ -1,7 +1,13 @@ import request from "supertest"; import app from "../src/app"; +import { emailQueue, imageProcessingQueue } from "../src/queues"; describe("GET /", () => { + afterAll(async () => { + await emailQueue.close(); + await imageProcessingQueue.close(); + }); + it("should respond with 'Express + TypeScript Server'", async () => { const response = await request(app).get("/"); expect(response.status).toBe(200); diff --git a/__test__/controllers/jobs/createJob.controller.test.ts b/__test__/controllers/jobs/createJob.controller.test.ts new file mode 100644 index 0000000..ab3a050 --- /dev/null +++ b/__test__/controllers/jobs/createJob.controller.test.ts @@ -0,0 +1,92 @@ +/* eslint-disable @typescript-eslint/consistent-type-assertions */ +import { type Request, type Response } from "express"; +import { type Queue } from "bullmq"; +import { queueByName } from "../../../src/queues"; +import createJob from "../../../src/controllers/jobs/createJob.controller"; + +console.error = jest.fn(); + +// Mock the queueByName function +jest.mock("../../../src/queues", () => ({ + queueByName: jest.fn(), +})); + +// Mock the queue's add method +const addMock = jest.fn(); + +// Create a mock queue object +const queueMock = { + add: addMock, +} as unknown as Queue; + +describe("createJob", () => { + let req: Request; + let res: Response; + + beforeEach(() => { + req = { + body: { + type: "email", + data: { key: "value" }, + }, + } as Request; + res = { + json: jest.fn(), + status: jest.fn().mockReturnThis(), + } as unknown as Response; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("should add the job to the appropriate queue and return the jobId", async () => { + // Mock the queueByName function to return the queue + (queueByName as jest.Mock).mockReturnValueOnce(queueMock); + + // Mock the queue's add method to return a job + const jobId = "job123"; + const job = { id: jobId }; + addMock.mockResolvedValueOnce(job); + + await createJob(req, res); + + expect(queueByName).toHaveBeenCalledWith("email"); + expect(addMock).toHaveBeenCalledWith("email", { key: "value" }); + expect(res.json).toHaveBeenCalledWith({ jobId }); + expect(res.status).not.toHaveBeenCalled(); + }); + + it("should handle error when queueByName throws an error", async () => { + const error = new Error("No Queue called unknownQueue"); + + // Mock the queueByName function to throw an error + (queueByName as jest.Mock).mockImplementationOnce(() => { + throw error; + }); + + await createJob(req, res); + + expect(queueByName).toHaveBeenCalledWith("email"); + expect(addMock).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(400); + expect(res.json).toHaveBeenCalledWith({ error }); + expect(console.error).toHaveBeenCalled(); + }); + + it("should handle error when the queue's add method throws an error", async () => { + const error = new Error("Failed to add job"); + addMock.mockRejectedValueOnce(error); + + // Mock the queueByName function to return the queue + (queueByName as jest.Mock).mockReturnValueOnce(queueMock); + + await createJob(req, res); + + expect(queueByName).toHaveBeenCalledWith("email"); + expect(addMock).toHaveBeenCalledWith("email", { key: "value" }); + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ error: "Failed to create job" }); + expect(console.error).toHaveBeenCalledWith("Error creating job:", error); + }); +}); diff --git a/__test__/controllers/jobs/getJob.controller.test.ts b/__test__/controllers/jobs/getJob.controller.test.ts new file mode 100644 index 0000000..d13662e --- /dev/null +++ b/__test__/controllers/jobs/getJob.controller.test.ts @@ -0,0 +1,112 @@ +/* eslint-disable @typescript-eslint/consistent-type-assertions */ +import { type Request, type Response } from "express"; +import { Job, type Queue } from "bullmq"; +import { queueByName } from "../../../src/queues"; +import getJob from "../../../src/controllers/jobs/getJob.controller"; + +// Mock the queueByName function +jest.mock("../../../src/queues", () => ({ + queueByName: jest.fn(), +})); + +// Mock the Job class +jest.mock("bullmq", () => ({ + Job: { + fromId: jest.fn(), + }, +})); + +describe("getJob", () => { + let req: Request; + let res: Response; + + beforeEach(() => { + req = { + params: {}, + } as Request; + res = { + json: jest.fn(), + status: jest.fn().mockReturnThis(), + } as unknown as Response; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("should return job details if job exists", async () => { + const jobId = "job123"; + const queue = {} as Queue; + const job = { + id: jobId, + name: "Test Job", + getState: jest.fn().mockResolvedValue("completed"), + progress: 100, + data: { key: "value" }, + } as unknown as Job; + + // Mock the queueByName function to return the queue + (queueByName as jest.Mock).mockReturnValueOnce(queue); + + // Mock the Job.fromId function to return the job + (Job.fromId as jest.Mock).mockResolvedValueOnce(job); + + await getJob(req, res); + + expect(res.json).toHaveBeenCalledWith({ + id: job.id, + name: job.name, + status: "completed", + progress: job.progress, + data: job.data, + }); + expect(res.status).not.toHaveBeenCalled(); + }); + + it("should return 404 error if job does not exist", async () => { + const queue = {} as Queue; + + // Mock the queueByName function to return the queue + (queueByName as jest.Mock).mockReturnValueOnce(queue); + + // Mock the Job.fromId function to return null + (Job.fromId as jest.Mock).mockResolvedValueOnce(null); + + await getJob(req, res); + + expect(res.status).toHaveBeenCalledWith(404); + expect(res.json).toHaveBeenCalledWith({ error: "Job not found" }); + }); + + it("should handle error when queueByName throws an error", async () => { + const error = new Error("No Queue called unknownQueue"); + + // Mock the queueByName function to throw an error + (queueByName as jest.Mock).mockImplementationOnce(() => { + throw error; + }); + + await getJob(req, res); + + expect(res.status).toHaveBeenCalledWith(400); + expect(res.json).toHaveBeenCalledWith({ error }); + }); + + it("should handle error when Job.fromId throws an error", async () => { + const queue = {} as Queue; + const error = new Error("Failed to fetch job"); + + // Mock the queueByName function to return the queue + (queueByName as jest.Mock).mockReturnValueOnce(queue); + + // Mock the Job.fromId function to throw an error + (Job.fromId as jest.Mock).mockRejectedValueOnce(error); + + await getJob(req, res); + + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ + error: "Failed to get job status", + }); + }); +}); diff --git a/__test__/jobs/email.job.test.ts b/__test__/jobs/email.job.test.ts new file mode 100644 index 0000000..6a4c1ab --- /dev/null +++ b/__test__/jobs/email.job.test.ts @@ -0,0 +1,40 @@ +import { type Job } from "bullmq"; +import emailJob from "../../src/jobs/email.job"; + +describe("emailJob", () => { + let consoleLogSpy: jest.SpyInstance; + let consoleErrorSpy: jest.SpyInstance; + + beforeEach(() => { + consoleLogSpy = jest.spyOn(console, "log"); + consoleErrorSpy = jest.spyOn(console, "error"); + }); + + afterEach(() => { + consoleLogSpy.mockRestore(); + consoleErrorSpy.mockRestore(); + }); + + it("should process email job successfully", async () => { + const jobData = { + to: "example@example.com", + subject: "Test Subject", + body: "Test Body", + }; + + const job: Partial = { + id: "jobId", + data: jobData, + name: "emailJob", + }; + + await emailJob(job as Job); + + expect(consoleLogSpy).toHaveBeenCalledWith( + `Sending email to ${jobData.to}: ${jobData.subject}` + ); + expect(consoleLogSpy).toHaveBeenCalledWith("Body:", jobData.body); + expect(consoleLogSpy).toHaveBeenCalledWith("Email sent successfully"); + expect(consoleErrorSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/__test__/jobs/imageProcessing.job.test.ts b/__test__/jobs/imageProcessing.job.test.ts new file mode 100644 index 0000000..e3ad7a0 --- /dev/null +++ b/__test__/jobs/imageProcessing.job.test.ts @@ -0,0 +1,17 @@ +import { type Job } from "bullmq"; +import imageProcessingJob from "../../src/jobs/imageProcessing.job"; + +describe("imageProcessingJob", () => { + it("should process the image successfully", async () => { + const imageUrl = "https://example.com/image.jpg"; + const jobData = { imageUrl }; + const job: Partial = { + id: "job-id", + name: "image-processing", + data: jobData, + // Mock other attributes as needed + }; + + await expect(imageProcessingJob(job as Job)).resolves.toBeUndefined(); + }); +}); diff --git a/__test__/queues/queueByName.test.ts b/__test__/queues/queueByName.test.ts new file mode 100644 index 0000000..c7e0b30 --- /dev/null +++ b/__test__/queues/queueByName.test.ts @@ -0,0 +1,24 @@ +import emailQueue from "../../src/queues/email.queue"; +import imageProcessingQueue from "../../src/queues/imageProcessing.queue"; +import queueByName from "../../src/queues/queueByName"; + +describe("queueByName", () => { + afterAll(async () => { + await emailQueue.close(); + await imageProcessingQueue.close(); + }); + + it("should return emailQueue for name 'email'", () => { + expect(queueByName("email")).toBe(emailQueue); + }); + + it("should return imageProcessingQueue for name 'imageProcessing'", () => { + expect(queueByName("imageProcessing")).toBe(imageProcessingQueue); + }); + + it("should throw an error for unknown queue name", () => { + const unknownName = "unknownQueue"; + const expectedErrorMessage = `No Queue called ${unknownName}`; + expect(() => queueByName(unknownName)).toThrowError(expectedErrorMessage); + }); +}); diff --git a/__test__/routes/posts.route.test.ts b/__test__/routes/posts.route.test.ts index f5b3e81..0aabaed 100644 --- a/__test__/routes/posts.route.test.ts +++ b/__test__/routes/posts.route.test.ts @@ -3,6 +3,7 @@ import app from "../../src/app"; import mongoose from "mongoose"; import Post from "../../src/models/post.model"; import { env } from "../../src/config"; +import { emailQueue, imageProcessingQueue } from "../../src/queues"; beforeEach(async () => { mongoose.set("strictQuery", true); @@ -15,6 +16,11 @@ afterEach(async () => { }); describe("/posts routes", () => { + afterAll(async () => { + await emailQueue.close(); + await imageProcessingQueue.close(); + }); + test("GET /posts", async () => { const post = await Post.create({ title: "Post 1", diff --git a/docker-compose.test.yml b/docker-compose.test.yml index fa1349f..50c0297 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -12,7 +12,7 @@ services: - MONGODB_PORT=27017 - MONGODB_USER=root - MONGODB_PASS=pass - - REDIS_QUEUE_HOST=localhost + - REDIS_QUEUE_HOST=redis - REDIS_QUEUE_PORT=6379 volumes: - ./coverage:/project/coverage diff --git a/package-lock.json b/package-lock.json index 9110403..ce79bdb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,9 @@ "version": "0.0.1", "license": "ISC", "dependencies": { + "@bull-board/api": "^5.2.0", + "@bull-board/express": "^5.2.0", + "bullmq": "^3.13.4", "env-cmd": "^10.1.0", "express": "^4.18.2", "joi": "^17.9.2", @@ -714,6 +717,235 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@bull-board/api": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@bull-board/api/-/api-5.2.0.tgz", + "integrity": "sha512-1HGF2EF/4zI3+Cj414nQzwFprLXOJTlVdqXUf5UEBS4HtYafWv93mGIwkrD8S4Bpz4VSvM87adF6tQPJ7Ewt+w==", + "dependencies": { + "redis-info": "^3.0.8" + }, + "peerDependencies": { + "@bull-board/ui": "5.2.0" + } + }, + "node_modules/@bull-board/express": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@bull-board/express/-/express-5.2.0.tgz", + "integrity": "sha512-PsrYX4TRtCnpXwywldVKCTBSn8fRBmKFe9J7GTDZ0f03JgWK/QYgOYfbDCHrTBU4pF9YcNECOMXSUenTEBc4DQ==", + "dependencies": { + "@bull-board/api": "5.2.0", + "@bull-board/ui": "5.2.0", + "ejs": "3.1.7", + "express": "4.17.3" + } + }, + "node_modules/@bull-board/express/node_modules/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.8.1", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.9.7", + "raw-body": "2.4.3", + "type-is": "~1.6.18" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@bull-board/express/node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@bull-board/express/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@bull-board/express/node_modules/destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg==" + }, + "node_modules/@bull-board/express/node_modules/express": { + "version": "4.17.3", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.3.tgz", + "integrity": "sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.19.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.4.2", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.9.7", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.17.2", + "serve-static": "1.14.2", + "setprototypeof": "1.2.0", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/@bull-board/express/node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@bull-board/express/node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@bull-board/express/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/@bull-board/express/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@bull-board/express/node_modules/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==", + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@bull-board/express/node_modules/raw-body": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.3.tgz", + "integrity": "sha512-UlTNLIcu0uzb4D2f4WltY6cVjLi+/jEN4lgEUj3E04tpMDpUlkBo/eSn6zou9hum2VMNpCCUone0O0WeJim07g==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "1.8.1", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@bull-board/express/node_modules/send": { + "version": "0.17.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz", + "integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==", + "dependencies": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "1.8.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@bull-board/express/node_modules/serve-static": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz", + "integrity": "sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@bull-board/express/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@bull-board/ui": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@bull-board/ui/-/ui-5.2.0.tgz", + "integrity": "sha512-f2sgs7AjOVch7tFhbmlVCkhZjJWboxwNxWEfAsIUd1WidUC+Ef5J02tpQvu7apzRtu5zcn8IiJtI5HFO6oKaCA==", + "dependencies": { + "@bull-board/api": "5.2.0" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -874,6 +1106,11 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "node_modules/@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -1383,6 +1620,78 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.2.tgz", + "integrity": "sha512-9bfjwDxIDWmmOKusUcqdS4Rw+SETlp9Dy39Xui9BEGEk19dDwH0jhipwFzEff/pFg95NKymc6TOTbRKcWeRqyQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.2.tgz", + "integrity": "sha512-lwriRAHm1Yg4iDf23Oxm9n/t5Zpw1lVnxYU3HnJPTi2lJRkKTrps1KVgvL6m7WvmhYVt/FIsssWay+k45QHeuw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.2.tgz", + "integrity": "sha512-MOI9Dlfrpi2Cuc7i5dXdxPbFIgbDBGgKR5F2yWEa6FVEtSWncfVNKW5AKjImAQ6CZlBK9tympdsZJ2xThBiWWA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.2.tgz", + "integrity": "sha512-FU20Bo66/f7He9Fp9sP2zaJ1Q8L9uLPZQDub/WlUip78JlPeMbVL8546HbZfcW9LNciEXc8d+tThSJjSC+tmsg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.2.tgz", + "integrity": "sha512-gsWNDCklNy7Ajk0vBBf9jEx04RUxuDQfBse918Ww+Qb9HCPoGzS+XJTLe96iN3BVK7grnLiYghP/M4L8VsaHeA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.2.tgz", + "integrity": "sha512-O+6Gs8UeDbyFpbSh2CPEz/UOrrdWPTBYNblZK5CxxLisYt4kGX3Sc+czffFonyjiGSq3jWLwJS/CCJc7tBr4sQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2210,7 +2519,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -2333,6 +2641,11 @@ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "dev": true }, + "node_modules/async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2444,8 +2757,7 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/base64-js": { "version": "1.5.1", @@ -2502,7 +2814,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2630,6 +2941,77 @@ "node": ">=10" } }, + "node_modules/bullmq": { + "version": "3.13.4", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-3.13.4.tgz", + "integrity": "sha512-3B16ZbOQSr9VoWpUXhoLB4khgxAbqwIiboJItA5rU2CEkLRe613w8yGxrivNdjJ6q8RcZtS+QvK0WHqCsuQYTQ==", + "dependencies": { + "cron-parser": "^4.6.0", + "glob": "^8.0.3", + "ioredis": "^5.3.2", + "lodash": "^4.17.21", + "msgpackr": "^1.6.2", + "semver": "^7.3.7", + "tslib": "^2.0.0", + "uuid": "^9.0.0" + } + }, + "node_modules/bullmq/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/bullmq/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/bullmq/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/bullmq/node_modules/semver": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", + "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/bullmq/node_modules/tslib": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.2.tgz", + "integrity": "sha512-5svOrSA2w3iGFDs1HibEVBGbDrAY82bFQ3HZ3ixB+88nsbsWQoKqDRb5UBYAUPEzbBn6dAp5gRNXglySbx1MlA==" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -2692,7 +3074,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -2708,7 +3089,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "engines": { "node": ">=8" } @@ -2717,7 +3097,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -2796,6 +3175,14 @@ "node": ">=12" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -2816,7 +3203,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -2827,8 +3213,7 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/combined-stream": { "version": "1.0.8", @@ -2859,8 +3244,7 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "node_modules/content-disposition": { "version": "0.5.4", @@ -2912,6 +3296,17 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, + "node_modules/cron-parser": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.8.1.tgz", + "integrity": "sha512-jbokKWGcyU4gl6jAfX97E1gDpY12DJ1cLJZmoDzaAln/shZ+S3KBFBuA2Q6WeUN4gJf/8klnV1EfvhA2lK5IRQ==", + "dependencies": { + "luxon": "^3.2.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -2978,6 +3373,14 @@ "node": ">=0.4.0" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -3061,6 +3464,20 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, + "node_modules/ejs": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.7.tgz", + "integrity": "sha512-BIar7R6abbUxDA3bfXrO4DSgwo8I+fB5/1zgujl3HLLjwd6+9iOnrT+t3grn2qbk9vOgBubXOFwX2m9axoFaGw==", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.402", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.402.tgz", @@ -3904,6 +4321,33 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -4024,8 +4468,7 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/fsevents": { "version": "2.3.2", @@ -4450,7 +4893,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -4474,6 +4916,50 @@ "node": ">= 0.4" } }, + "node_modules/ioredis": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.2.tgz", + "integrity": "sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==", + "dependencies": { + "@ioredis/commands": "^1.1.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/ioredis/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/ioredis/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/ip": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", @@ -4882,6 +5368,23 @@ "node": ">=8" } }, + "node_modules/jake": { + "version": "10.8.6", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.6.tgz", + "integrity": "sha512-G43Ub9IYEFfu72sua6rzooi8V8Gz2lkfk48rW20vEWCGizeaEPlKB1Kh8JIA84yQbiAEfqlPmSpGgCKKxH3rDA==", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jest": { "version": "29.5.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.5.0.tgz", @@ -5615,6 +6118,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -5631,7 +6149,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -5639,6 +6156,14 @@ "node": ">=10" } }, + "node_modules/luxon": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.3.0.tgz", + "integrity": "sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg==", + "engines": { + "node": ">=12" + } + }, "node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -5776,7 +6301,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -5904,6 +6428,35 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/msgpackr": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.9.2.tgz", + "integrity": "sha512-xtDgI3Xv0AAiZWLRGDchyzBwU6aq0rwJ+W+5Y4CZhEWtkl/hJtFFLc+3JtGTw7nz1yquxs7nL8q/yA2aqpflIQ==", + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.2.tgz", + "integrity": "sha512-SdzXp4kD/Qf8agZ9+iTu6eql0m3kWm1A2y1hkpTeVNENutaB0BwHlSvAIaMxwntmRUAUjon2V4L8Z/njd0Ct8A==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.0.7" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.2", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.2", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.2", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.2", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.2", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.2" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -5924,6 +6477,17 @@ "node": ">= 0.6" } }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.7.tgz", + "integrity": "sha512-YlCCc6Wffkx0kHkmam79GKvDQ6x+QZkMjFGrIMxgFNILFvGSbCp2fCBC55pGTT9gVaz8Na5CLmxt/urtzRv36w==", + "optional": true, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -6080,7 +6644,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "dependencies": { "wrappy": "1" } @@ -6532,6 +7095,33 @@ "node": ">=8.10.0" } }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-info": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redis-info/-/redis-info-3.1.0.tgz", + "integrity": "sha512-ER4L9Sh/vm63DkIE0bkSjxluQlioBiBgf5w1UuldaW/3vPcecdljVDisZhmnCMvsxHNiARTTDDHGg9cGwTfrKg==", + "dependencies": { + "lodash": "^4.17.11" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/regexp.prototype.flags": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", @@ -6934,6 +7524,11 @@ "node": ">=8" } }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -7563,6 +8158,14 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -7728,8 +8331,7 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/write-file-atomic": { "version": "4.0.2", @@ -7756,8 +8358,7 @@ "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yargs": { "version": "17.7.2", diff --git a/package.json b/package.json index e7bada1..fb246e0 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "build": "npx tsc", "start": "node dist/index.js", "dev": "env-cmd nodemon ./src/index.ts", - "test": "env-cmd jest --coverage", + "test": "env-cmd jest --coverage --detectOpenHandles", "prettier": "npx prettier --write .", "lint": "npx eslint ." }, @@ -27,6 +27,9 @@ }, "homepage": "https://github.com/TogetherCrew/typescript-service#readme", "dependencies": { + "@bull-board/api": "^5.2.0", + "@bull-board/express": "^5.2.0", + "bullmq": "^3.13.4", "env-cmd": "^10.1.0", "express": "^4.18.2", "joi": "^17.9.2", diff --git a/src/app.ts b/src/app.ts index 60b007f..bc560b7 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,9 +1,9 @@ import express, { type Express, type Request, type Response } from "express"; -import postsRoute from "./routes/posts.route"; +import routes from "./routes"; const app: Express = express(); - -app.use("/posts", postsRoute); +app.use(express.json()); +app.use(routes); app.get("/", (req: Request, res: Response) => { res.send("Express + TypeScript Server"); diff --git a/src/controllers/jobs/createJob.controller.ts b/src/controllers/jobs/createJob.controller.ts new file mode 100644 index 0000000..b603e40 --- /dev/null +++ b/src/controllers/jobs/createJob.controller.ts @@ -0,0 +1,27 @@ +import { type Request, type Response } from "express"; +import { type Queue } from "bullmq"; +import { queueByName } from "../../queues"; + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +const createJob = async function (req: Request, res: Response) { + try { + const { type, data } = req.body; // Assuming you send the job type and data in the request body + + let queue: Queue; + try { + queue = queueByName(type); + } catch (error) { + console.error(error); + return res.status(400).json({ error }); + } + + const job = await queue.add(type, data); // Add the job to the appropriate queue + + return res.json({ jobId: job.id }); + } catch (error) { + console.error("Error creating job:", error); + return res.status(500).json({ error: "Failed to create job" }); + } +}; + +export default createJob; diff --git a/src/controllers/jobs/getJob.controller.ts b/src/controllers/jobs/getJob.controller.ts new file mode 100644 index 0000000..92105e7 --- /dev/null +++ b/src/controllers/jobs/getJob.controller.ts @@ -0,0 +1,38 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { type Request, type Response } from "express"; +import { Job, type Queue } from "bullmq"; +import { queueByName } from "../../queues"; + +const getJob = async function (req: Request, res: Response) { + try { + const { type, jobId } = req.params; // Assuming the jobId is passed as a route parameter + + let queue: Queue; + try { + queue = queueByName(type); + } catch (error) { + console.error("Error getting job queue:", error); + return res.status(400).json({ error }); + } + + // Fetch the job by its id + const job = await Job.fromId(queue, jobId); + + if (job == null) { + return res.status(404).json({ error: "Job not found" }); + } + + return res.json({ + id: job.id, + name: job.name, + status: await job.getState(), + progress: job.progress, + data: job.data, + }); + } catch (error) { + console.error("Error getting job status:", error); + return res.status(500).json({ error: "Failed to get job status" }); + } +}; + +export default getJob; diff --git a/src/controllers/jobs/index.ts b/src/controllers/jobs/index.ts new file mode 100644 index 0000000..6e05d85 --- /dev/null +++ b/src/controllers/jobs/index.ts @@ -0,0 +1,4 @@ +import createJob from "./createJob.controller"; +import getJob from "./getJob.controller"; + +export { createJob, getJob }; diff --git a/src/index.ts b/src/index.ts index baf84b8..cb5d8c3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ import mongoose from "mongoose"; import app from "./app"; import { env } from "./config"; +import "./workers"; mongoose.set("strictQuery", true); @@ -9,7 +10,7 @@ main().catch((err) => { console.error(err); }); -async function main(): Promise { +export async function main(): Promise { await mongoose.connect(env.MONGODB_URL); app.listen(env.PORT, () => { console.log( diff --git a/src/jobs/email.job.ts b/src/jobs/email.job.ts new file mode 100644 index 0000000..5dc8cde --- /dev/null +++ b/src/jobs/email.job.ts @@ -0,0 +1,23 @@ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +import { type Job } from "bullmq"; + +const emailJob = async (job: Job): Promise => { + // Define the processing logic for email jobs + const { to, subject, body } = job.data; // Assuming the job data contains the recipient, subject, and body of the email + + try { + // Perform email processing logic here + console.log(`Sending email to ${to}: ${subject}`); + console.log("Body:", body); + + // Simulating email sending time + await new Promise((resolve) => setTimeout(resolve, 2000)); + + console.log("Email sent successfully"); + } catch (error) { + console.error("Error processing email:", error); + throw new Error("Failed to process email"); // Throw an error if email processing fails + } +}; + +export default emailJob; diff --git a/src/jobs/imageProcessing.job.ts b/src/jobs/imageProcessing.job.ts new file mode 100644 index 0000000..3da8ff3 --- /dev/null +++ b/src/jobs/imageProcessing.job.ts @@ -0,0 +1,22 @@ +import { type Job } from "bullmq"; + +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +const imageProcessingJob = async function (job: Job): Promise { + // Define the processing logic for image processing jobs + const { imageUrl } = job.data; // Assuming the job data contains the URL of the image to be processed + + try { + // Perform image processing logic here + console.log(`Processing image: ${imageUrl}`); + + // Simulating image processing time + await new Promise((resolve) => setTimeout(resolve, 3000)); + + console.log("Image processing completed"); + } catch (error) { + console.error("Error processing image:", error); + throw new Error("Failed to process image"); // Throw an error if image processing fails + } +}; + +export default imageProcessingJob; diff --git a/src/queues/connection.ts b/src/queues/connection.ts new file mode 100644 index 0000000..f741e04 --- /dev/null +++ b/src/queues/connection.ts @@ -0,0 +1,9 @@ +import { type RedisOptions } from "bullmq"; +import { env } from "../config"; + +const connection: RedisOptions = { + host: env.REDIS_QUEUE_HOST, + port: env.REDIS_QUEUE_PORT, +}; + +export default connection; diff --git a/src/queues/email.queue.ts b/src/queues/email.queue.ts new file mode 100644 index 0000000..7e063d2 --- /dev/null +++ b/src/queues/email.queue.ts @@ -0,0 +1,6 @@ +import { Queue } from "bullmq"; +import connection from "./connection"; + +const emailQueue = new Queue("email", { connection }); + +export default emailQueue; diff --git a/src/queues/imageProcessing.queue.ts b/src/queues/imageProcessing.queue.ts new file mode 100644 index 0000000..6abb770 --- /dev/null +++ b/src/queues/imageProcessing.queue.ts @@ -0,0 +1,6 @@ +import { Queue } from "bullmq"; +import connection from "./connection"; + +const imageProcessingQueue = new Queue("imageProcessing", { connection }); + +export default imageProcessingQueue; diff --git a/src/queues/index.ts b/src/queues/index.ts new file mode 100644 index 0000000..e077c6d --- /dev/null +++ b/src/queues/index.ts @@ -0,0 +1,5 @@ +import emailQueue from "./email.queue"; +import imageProcessingQueue from "./imageProcessing.queue"; +import queueByName from "./queueByName"; + +export { emailQueue, imageProcessingQueue, queueByName }; diff --git a/src/queues/queueByName.ts b/src/queues/queueByName.ts new file mode 100644 index 0000000..c1f98ba --- /dev/null +++ b/src/queues/queueByName.ts @@ -0,0 +1,16 @@ +import { type Queue } from "bullmq"; +import emailQueue from "./email.queue"; +import imageProcessingQueue from "./imageProcessing.queue"; + +const queueByName = (name: string): Queue => { + switch (name) { + case emailQueue.name: + return emailQueue; + case imageProcessingQueue.name: + return imageProcessingQueue; + default: + throw new Error(`No Queue called ${name}`); + } +}; + +export default queueByName; diff --git a/src/routes/bull-board.ts b/src/routes/bull-board.ts new file mode 100644 index 0000000..760d71f --- /dev/null +++ b/src/routes/bull-board.ts @@ -0,0 +1,18 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { ExpressAdapter } from "@bull-board/express"; +import { createBullBoard } from "@bull-board/api"; +import { BullMQAdapter } from "@bull-board/api/bullMQAdapter"; +import { emailQueue, imageProcessingQueue } from "../queues"; + +const serverAdapter = new ExpressAdapter(); +serverAdapter.setBasePath("/admin/queues"); + +createBullBoard({ + queues: [ + new BullMQAdapter(emailQueue), + new BullMQAdapter(imageProcessingQueue), + ], + serverAdapter, +}); + +export default serverAdapter; diff --git a/src/routes/index.ts b/src/routes/index.ts new file mode 100644 index 0000000..9bd0bd5 --- /dev/null +++ b/src/routes/index.ts @@ -0,0 +1,12 @@ +import express from "express"; +import jobsRoute from "./jobs.route"; +import postsRoute from "./posts.route"; +import serverAdapter from "./bull-board"; + +const router = express.Router(); + +router.use("/jobs", jobsRoute); +router.use("/posts", postsRoute); +router.use("/admin/queues", serverAdapter.getRouter()); + +export default router; diff --git a/src/routes/jobs.route.ts b/src/routes/jobs.route.ts new file mode 100644 index 0000000..4777479 --- /dev/null +++ b/src/routes/jobs.route.ts @@ -0,0 +1,10 @@ +/* eslint-disable @typescript-eslint/no-misused-promises */ +import express from "express"; +import { createJob, getJob } from "../controllers/jobs"; + +const router = express.Router(); + +router.post("/", createJob); +router.get("/:type/:jobId", getJob); + +export default router; diff --git a/src/workers/email.worker.ts b/src/workers/email.worker.ts new file mode 100644 index 0000000..a548f95 --- /dev/null +++ b/src/workers/email.worker.ts @@ -0,0 +1,10 @@ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +import { Worker } from "bullmq"; +import { emailQueue } from "../queues"; // Assuming you have already defined the emailQueue +import connection from "../queues/connection"; +import emailJob from "../jobs/email.job"; + +// Create a new BullMQ worker instance for emailQueue +const emailWorker = new Worker(emailQueue.name, emailJob, { connection }); + +export default emailWorker; diff --git a/src/workers/imageProcessing.worker.ts b/src/workers/imageProcessing.worker.ts new file mode 100644 index 0000000..f8fb016 --- /dev/null +++ b/src/workers/imageProcessing.worker.ts @@ -0,0 +1,14 @@ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +import { Worker } from "bullmq"; +import { imageProcessingQueue } from "../queues"; // Assuming you have already defined the imageProcessingQueue +import connection from "../queues/connection"; +import imageProcessingJob from "../jobs/imageProcessing.job"; + +// Create a new BullMQ worker instance for imageProcessingQueue +const imageProcessingWorker = new Worker( + imageProcessingQueue.name, + imageProcessingJob, + { connection } +); + +export default imageProcessingWorker; diff --git a/src/workers/index.ts b/src/workers/index.ts new file mode 100644 index 0000000..81d1d0e --- /dev/null +++ b/src/workers/index.ts @@ -0,0 +1,4 @@ +import emailWorker from "./email.worker"; +import imageProcessingWorker from "./imageProcessing.worker"; + +export { emailWorker, imageProcessingWorker };