From 59d8ea49626e55d132ab203a83ac6ae39b8b9773 Mon Sep 17 00:00:00 2001 From: Olivier Louvignes Date: Tue, 29 Oct 2024 14:46:39 +0100 Subject: [PATCH] feat(eslint): update eslint to 9.x and fix issues --- .eslintrc | 5 -- eslint.config.js | 32 ++++++++++ package.json | 3 +- src/PrismaJob.ts | 2 +- src/PrismaQueue.spec.ts | 138 +++++++++++++++++++++------------------- src/PrismaQueue.ts | 21 +++--- src/types.ts | 1 - src/utils/error.ts | 2 +- src/utils/prisma.ts | 2 +- src/utils/stringify.ts | 8 ++- test/utils/queue.ts | 1 + 11 files changed, 127 insertions(+), 88 deletions(-) delete mode 100644 .eslintrc create mode 100644 eslint.config.js diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index e8e5521..0000000 --- a/.eslintrc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "root": true, - "extends": ["@mgcrea/node"], - "rules": {} -} diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..8023800 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,32 @@ +// @ts-check + +import baseConfig from "@mgcrea/eslint-config-node"; + +const config = [ + ...baseConfig, + { + rules: { + // "@typescript-eslint/no-unnecessary-type-parameters": "off", + }, + }, + { + files: ["**/*.{mock,spec,test}.{js,ts,tsx}", "**/__{mocks,tests}__/**/*.{js,ts,tsx}"], + rules: { + // "@typescript-eslint/no-unsafe-member-access": "off", + // "@typescript-eslint/no-unsafe-call": "off", + // "@typescript-eslint/no-unsafe-assignment": "off", + // "@typescript-eslint/no-unsafe-argument": "off", + // "@typescript-eslint/no-non-null-assertion": "off", + }, + }, + { + languageOptions: { + parserOptions: { + project: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + }, +]; + +export default config; diff --git a/package.json b/package.json index 6bf11ae..6403647 100644 --- a/package.json +++ b/package.json @@ -68,5 +68,6 @@ "typescript": "^5.6.3", "vite-tsconfig-paths": "^5.0.1", "vitest": "^2.1.4" - } + }, + "packageManager": "pnpm@9.12.3+sha512.cce0f9de9c5a7c95bef944169cc5dfe8741abfb145078c0d508b868056848a87c81e626246cb60967cbd7fd29a6c062ef73ff840d96b3c86c40ac92cf4a813ee" } diff --git a/src/PrismaJob.ts b/src/PrismaJob.ts index 16b0c44..470f4a8 100644 --- a/src/PrismaJob.ts +++ b/src/PrismaJob.ts @@ -28,7 +28,7 @@ export class PrismaJob { this.#model = model; this.#client = client; this.#record = record; - this.id = record["id"]; + this.id = record.id; } /** diff --git a/src/PrismaQueue.spec.ts b/src/PrismaQueue.spec.ts index 2da0238..5555f51 100644 --- a/src/PrismaQueue.spec.ts +++ b/src/PrismaQueue.spec.ts @@ -35,15 +35,15 @@ describe("PrismaQueue", () => { }); describe("enqueue", () => { let queue: PrismaQueue; - beforeAll(async () => { + beforeAll(() => { queue = createEmailQueue(); }); beforeEach(async () => { await prisma.queueJob.deleteMany(); - queue.start(); + void queue.start(); }); - afterEach(async () => { - queue.stop(); + afterEach(() => { + void queue.stop(); }); it("should properly enqueue a job", async () => { const job = await queue.enqueue({ email: "foo@bar.com" }); @@ -55,8 +55,8 @@ describe("PrismaQueue", () => { `); const record = await job.fetch(); expect(record.key).toBeNull(); - expect(record?.payload).toEqual({ email: "foo@bar.com" }); - expect(record?.runAt).toBeInstanceOf(Date); + expect(record.payload).toEqual({ email: "foo@bar.com" }); + expect(record.runAt).toBeInstanceOf(Date); }); it("should properly enqueue a job with a custom key", async () => { @@ -68,23 +68,23 @@ describe("PrismaQueue", () => { ] `); const record = await job.fetch(); - expect(record?.payload).toEqual({ email: "foo@bar.com" }); - expect(record?.runAt).toBeInstanceOf(Date); + expect(record.payload).toEqual({ email: "foo@bar.com" }); + expect(record.runAt).toBeInstanceOf(Date); expect(record.key).toBe("custom-key"); }); }); describe("schedule", () => { let queue: PrismaQueue; - beforeAll(async () => { + beforeAll(() => { queue = createEmailQueue(); }); beforeEach(async () => { await prisma.queueJob.deleteMany(); - queue.start(); + void queue.start(); }); - afterEach(async () => { - queue.stop(); + afterEach(() => { + void queue.stop(); }); it("should properly schedule a recurring job", async () => { const job = await queue.schedule( @@ -94,15 +94,15 @@ describe("PrismaQueue", () => { expect(job).toBeInstanceOf(PrismaJob); const record = await job.fetch(); expect(record).toBeDefined(); - expect(record?.runAt.getHours()).toBe(5); - expect(record?.runAt.getMinutes()).toBe(5); + expect(record.runAt.getHours()).toBe(5); + expect(record.runAt.getMinutes()).toBe(5); }); it("should properly re-enqueue a recurring job", async () => { await queue.schedule( { key: "email-schedule", cron: "5 5 * * *", runAt: new Date() }, { email: "foo@bar.com" }, ); - queue.start(); + void queue.start(); await waitForNextEvent(queue, "enqueue"); const jobs = await prisma.queueJob.findMany({ where: { key: "email-schedule" } }); expect(jobs.length).toBe(2); @@ -129,15 +129,15 @@ describe("PrismaQueue", () => { describe("dequeue", () => { let queue: PrismaQueue; - beforeAll(async () => { + beforeAll(() => { queue = createEmailQueue(); }); beforeEach(async () => { await prisma.queueJob.deleteMany(); - queue.start(); + void queue.start(); }); - afterEach(async () => { - queue.stop(); + afterEach(() => { + void queue.stop(); }); it("should properly dequeue a successful job", async () => { queue.worker = vi.fn(async (_job) => { @@ -149,10 +149,11 @@ describe("PrismaQueue", () => { expect(queue.worker).toHaveBeenCalledTimes(1); expect(queue.worker).toHaveBeenNthCalledWith(1, expect.any(PrismaJob), expect.any(PrismaClient)); const record = await job.fetch(); - expect(record?.finishedAt).toBeInstanceOf(Date); + expect(record.finishedAt).toBeInstanceOf(Date); }); it("should properly dequeue a failed job", async () => { let error: Error | null = null; + // eslint-disable-next-line @typescript-eslint/require-await queue.worker = vi.fn(async (_job) => { error = new Error("failed"); throw error; @@ -162,8 +163,8 @@ describe("PrismaQueue", () => { expect(queue.worker).toHaveBeenCalledTimes(1); expect(queue.worker).toHaveBeenNthCalledWith(1, expect.any(PrismaJob), expect.any(PrismaClient)); const record = await job.fetch(); - expect(record?.finishedAt).toBeNull(); - expect(record?.error).toEqual(serializeError(error)); + expect(record.finishedAt).toBeNull(); + expect(record.error).toEqual(serializeError(error)); }); it("should properly dequeue multiple jobs in a row", async () => { const JOB_WAIT = 50; @@ -181,7 +182,7 @@ describe("PrismaQueue", () => { }); it("should properly handle multiple restarts", async () => { const JOB_WAIT = 50; - await queue.stop(); + void queue.stop(); queue.worker = vi.fn(async (_job) => { await waitFor(JOB_WAIT); return { code: "200" }; @@ -190,34 +191,35 @@ describe("PrismaQueue", () => { queue.enqueue({ email: "foo1@bar1.com" }), queue.enqueue({ email: "foo2@bar2.com" }), ]); - queue.start(); + void queue.start(); expect(queue.worker).toHaveBeenCalledTimes(0); - await queue.stop(); - queue.start(); + void queue.stop(); + void queue.start(); await waitFor(10); expect(queue.worker).toHaveBeenCalledTimes(1); await waitFor(JOB_WAIT + 10); expect(queue.worker).toHaveBeenCalledTimes(1); }); afterAll(() => { - queue.stop(); + void queue.stop(); }); }); describe("deleteOn", () => { let queue: PrismaQueue; describe("success", () => { - beforeAll(async () => { + beforeAll(() => { queue = createEmailQueue({ deleteOn: "success" }); }); beforeEach(async () => { await prisma.queueJob.deleteMany(); - queue.start(); + void queue.start(); }); - afterEach(async () => { - queue.stop(); + afterEach(() => { + void queue.stop(); }); it("should properly dequeue a successful job", async () => { + // eslint-disable-next-line @typescript-eslint/require-await queue.worker = vi.fn(async (_job) => { return { code: "200" }; }); @@ -228,22 +230,23 @@ describe("PrismaQueue", () => { expect(record).toBeNull(); }); afterAll(() => { - queue.stop(); + void queue.stop(); }); }); describe("failure", () => { - beforeAll(async () => { + beforeAll(() => { queue = createEmailQueue({ deleteOn: "failure" }); }); beforeEach(async () => { await prisma.queueJob.deleteMany(); - queue.start(); + void queue.start(); }); - afterEach(async () => { - queue.stop(); + afterEach(() => { + void queue.stop(); }); it("should properly dequeue a failed job", async () => { let error: Error | null = null; + // eslint-disable-next-line @typescript-eslint/require-await queue.worker = vi.fn(async (_job) => { error = new Error("failed"); throw error; @@ -255,21 +258,22 @@ describe("PrismaQueue", () => { expect(record).toBeNull(); }); afterAll(() => { - queue.stop(); + void queue.stop(); }); }); describe("always", () => { - beforeAll(async () => { + beforeAll(() => { queue = createEmailQueue({ deleteOn: "always" }); }); beforeEach(async () => { await prisma.queueJob.deleteMany(); - queue.start(); + void queue.start(); }); - afterEach(async () => { - queue.stop(); + afterEach(() => { + void queue.stop(); }); it("should properly dequeue a successful job", async () => { + // eslint-disable-next-line @typescript-eslint/require-await queue.worker = vi.fn(async (_job) => { return { code: "200" }; }); @@ -281,6 +285,7 @@ describe("PrismaQueue", () => { }); it("should properly dequeue a failed job", async () => { let error: Error | null = null; + // eslint-disable-next-line @typescript-eslint/require-await queue.worker = vi.fn(async (_job) => { error = new Error("failed"); throw error; @@ -292,22 +297,22 @@ describe("PrismaQueue", () => { expect(record).toBeNull(); }); afterAll(() => { - queue.stop(); + void queue.stop(); }); }); }); describe("maxConcurrency", () => { let queue: PrismaQueue; - beforeAll(async () => { + beforeAll(() => { queue = createEmailQueue({ maxConcurrency: 2 }); }); beforeEach(async () => { await prisma.queueJob.deleteMany(); - queue.start(); + void queue.start(); }); - afterEach(async () => { - queue.stop(); + afterEach(() => { + void queue.stop(); }); it("should properly dequeue multiple jobs in a row according to maxConcurrency", async () => { const JOB_WAIT = 100; @@ -324,29 +329,30 @@ describe("PrismaQueue", () => { expect(queue.worker).toHaveBeenNthCalledWith(2, expect.any(PrismaJob), expect.any(PrismaClient)); }); afterAll(() => { - queue.stop(); + void queue.stop(); }); }); describe("priority", () => { let queue: PrismaQueue; - beforeAll(async () => { + beforeAll(() => { queue = createEmailQueue(); }); beforeEach(async () => { await prisma.queueJob.deleteMany(); - // queue.start(); + // void queue.start(); }); - afterEach(async () => { - queue.stop(); + afterEach(() => { + void queue.stop(); }); it("should properly prioritize a job with a lower priority", async () => { + // eslint-disable-next-line @typescript-eslint/require-await queue.worker = vi.fn(async (_job) => { return { code: "200" }; }); await queue.enqueue({ email: "foo@bar.com" }); await queue.enqueue({ email: "baz@bar.com" }, { priority: -1 }); - queue.start(); + void queue.start(); await waitForNthJob(queue, 2); expect(queue.worker).toHaveBeenCalledTimes(2); expect(queue.worker).toHaveBeenNthCalledWith( @@ -365,50 +371,50 @@ describe("PrismaQueue", () => { ); }); afterAll(() => { - queue.stop(); + void queue.stop(); }); }); describe("Job.progress()", () => { let queue: PrismaQueue; - beforeAll(async () => { + beforeAll(() => { queue = createEmailQueue(); }); beforeEach(async () => { await prisma.queueJob.deleteMany(); - queue.start(); + void queue.start(); }); - afterEach(async () => { - queue.stop(); + afterEach(() => { + void queue.stop(); }); it("should properly update job progress", async () => { - queue.worker = vi.fn(async (job) => { + queue.worker = vi.fn(async (job: PrismaJob) => { debug("working...", job.id, job.payload); await job.progress(50); throw new Error("failed"); }); const job = await queue.enqueue({ email: "foo@bar.com" }); - queue.start(); + void queue.start(); await waitForNextJob(queue); const record = await job.fetch(); - expect(record?.progress).toBe(50); + expect(record.progress).toBe(50); }); afterAll(() => { - queue.stop(); + void queue.stop(); }); }); describe("Job.isLocked()", () => { let queue: PrismaQueue; - beforeAll(async () => { + beforeAll(() => { queue = createEmailQueue({ pollInterval: 200 }); }); beforeEach(async () => { await prisma.queueJob.deleteMany(); - queue.start(); + void queue.start(); }); - afterEach(async () => { - queue.stop(); + afterEach(() => { + void queue.stop(); }); it("should be toggled", async () => { queue.worker = vi.fn(async (_job) => { @@ -422,7 +428,7 @@ describe("PrismaQueue", () => { expect(await job.isLocked()).toBe(false); }); afterAll(() => { - queue.stop(); + void queue.stop(); }); }); }); diff --git a/src/PrismaQueue.ts b/src/PrismaQueue.ts index d9d31a4..31a6ce3 100644 --- a/src/PrismaQueue.ts +++ b/src/PrismaQueue.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unnecessary-condition */ import { Prisma, PrismaClient } from "@prisma/client"; import { Cron } from "croner"; import { EventEmitter } from "events"; @@ -48,6 +49,7 @@ export type PrismaQueueEvents) => void; }; +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions export interface PrismaQueue { on>(event: E, listener: PrismaQueueEvents[E]): this; once>(event: E, listener: PrismaQueueEvents[E]): this; @@ -116,14 +118,14 @@ export class PrismaQueue< }; // Default error handler - this.on("error", (error, job) => + this.on("error", (error, job) => { debug( job ? `Job with id=${job.id} failed for queue named="${this.name}" with error` : `Queue named="${this.name}" encountered an unexpected error`, error, - ), - ); + ); + }); } /** @@ -163,6 +165,7 @@ export class PrismaQueue< * @param payloadOrFunction - The job payload or a function that returns a job payload. * @param options - Options for the job, such as scheduling and attempts. */ + // eslint-disable-next-line @typescript-eslint/unbound-method public add = this.enqueue; /** @@ -220,7 +223,7 @@ export class PrismaQueue< ): Promise> { debug(`schedule`, this.name, options, payloadOrFunction); const { key, cron, runAt: firstRunAt, ...otherOptions } = options; - const runAt = firstRunAt || new Cron(cron).nextRun(); + const runAt = firstRunAt ?? new Cron(cron).nextRun(); assert(runAt, `Failed to find a future occurence for given cron`); return this.enqueue(payloadOrFunction, { key, cron, runAt, ...otherOptions }); } @@ -254,7 +257,7 @@ export class PrismaQueue< // debug(`concurrency=${this.concurrency}, maxConcurrency=${maxConcurrency}`); debug(`processing job from queue named="${this.name}"...`); this.concurrency++; - setImmediate(() => + setImmediate(() => { this.dequeue() .then((job) => { if (job) { @@ -265,13 +268,13 @@ export class PrismaQueue< estimatedQueueSize = 0; } }) - .catch((error) => { + .catch((error: unknown) => { this.emit("error", error); }) .finally(() => { this.concurrency--; - }), - ); + }); + }); await waitFor(jobInterval); } await waitFor(jobInterval * 2); @@ -341,7 +344,7 @@ export class PrismaQueue< } catch (error) { const date = new Date(); debug( - `failed finishing job({id: ${id}, payload: ${JSON.stringify(payload)}}) with error="${error}"`, + `failed finishing job({id: ${id}, payload: ${JSON.stringify(payload)}}) with error="${String(error)}"`, ); const isFinished = maxAttempts && attempts >= maxAttempts; const notBefore = new Date(date.getTime() + calculateDelay(attempts)); diff --git a/src/types.ts b/src/types.ts index c28377a..eb26651 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,7 +1,6 @@ import type { Prisma, PrismaClient, QueueJob as PrismaQueueJob } from "@prisma/client"; import type { PrismaJob } from "./PrismaJob"; -// eslint-disable-next-line @typescript-eslint/ban-types export type Simplify = { [KeyType in keyof T]: T[KeyType] } & {}; export type JobPayload = Prisma.InputJsonValue; diff --git a/src/utils/error.ts b/src/utils/error.ts index 0b94982..d8d76d9 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -10,7 +10,7 @@ export const serializeError = (err: unknown) => { } return { name: "UnknownError", - message: `${err}`, + message: String(err), stack: undefined, }; }; diff --git a/src/utils/prisma.ts b/src/utils/prisma.ts index fa3b3ad..79aaf51 100644 --- a/src/utils/prisma.ts +++ b/src/utils/prisma.ts @@ -3,6 +3,6 @@ import assert from "assert"; export const getTableName = (modelName: string): string => { const model = Prisma.dmmf.datamodel.models.find((model) => model.name === modelName); - assert(model && model.dbName, `Did not foudn model=${modelName} in Prisma.dmmf!`); + assert(model?.dbName, `Did not foudn model=${modelName} in Prisma.dmmf!`); return model.dbName; }; diff --git a/src/utils/stringify.ts b/src/utils/stringify.ts index 053f49b..6f4cc6f 100644 --- a/src/utils/stringify.ts +++ b/src/utils/stringify.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unnecessary-condition */ import type { Prisma } from "@prisma/client"; type InputJsonValue = Prisma.InputJsonValue; @@ -49,9 +50,9 @@ export function restoreFromJson(preparedValue: InputJsonValue): T { } else if (preparedValue["$type"] === "bigint") { return BigInt(preparedValue["$value"] as string) as T; } else if (preparedValue["$type"] === "Map") { - return new Map(preparedValue["$value"] as Array<[unknown, unknown]>) as T; + return new Map(preparedValue["$value"] as [unknown, unknown][]) as T; } else if (preparedValue["$type"] === "Set") { - return new Set(preparedValue["$value"] as Array) as T; + return new Set(preparedValue["$value"] as unknown[]) as T; } else if (preparedValue["$type"] === "Date") { return new Date(preparedValue["$value"] as number) as T; } @@ -61,7 +62,8 @@ export function restoreFromJson(preparedValue: InputJsonValue): T { } else { const copy: Record = {}; for (const key in preparedValue) { - copy[key] = restoreFromJson((preparedValue as InputJsonObject)[key] as InputJsonValue) as T; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + copy[key] = restoreFromJson((preparedValue as InputJsonObject)[key]!); } return copy as T; } diff --git a/test/utils/queue.ts b/test/utils/queue.ts index ad886df..45448d4 100644 --- a/test/utils/queue.ts +++ b/test/utils/queue.ts @@ -10,6 +10,7 @@ let globalQueueIndex = 0; export const createEmailQueue = ( options: PrismaQueueOptions = {}, + // eslint-disable-next-line @typescript-eslint/require-await worker: JobWorker = async (_job) => { return { code: "200" }; },