import { GraphQLError } from "graphql"; import type { Job } from "bullmq"; import { builder } from "../builder.js"; import { JobType, QueueInfoType } from "../types/index.js"; import { queues } from "../../queues/index.js"; import { requireAdmin } from "../../lib/acl.js"; const JOB_STATUSES = ["waiting", "active", "completed", "failed", "delayed"] as const; type JobStatus = (typeof JOB_STATUSES)[number]; async function toJobData(job: Job, queueName: string) { const status = await job.getState(); return { id: job.id ?? "", name: job.name, queue: queueName, status, data: job.data as unknown, result: job.returnvalue as unknown, failedReason: job.failedReason ?? null, attemptsMade: job.attemptsMade, createdAt: new Date(job.timestamp), processedAt: job.processedOn ? new Date(job.processedOn) : null, finishedAt: job.finishedOn ? new Date(job.finishedOn) : null, progress: typeof job.progress === "number" ? job.progress : null, }; } builder.queryField("adminQueues", (t) => t.field({ type: [QueueInfoType], resolve: async (_root, _args, ctx) => { requireAdmin(ctx); return Promise.all( Object.entries(queues).map(async ([name, queue]) => { const counts = await queue.getJobCounts( "waiting", "active", "completed", "failed", "delayed", "paused", ); const isPaused = await queue.isPaused(); return { name, counts: { waiting: counts.waiting ?? 0, active: counts.active ?? 0, completed: counts.completed ?? 0, failed: counts.failed ?? 0, delayed: counts.delayed ?? 0, paused: counts.paused ?? 0, }, isPaused, }; }), ); }, }), ); builder.queryField("adminQueueJobs", (t) => t.field({ type: [JobType], args: { queue: t.arg.string({ required: true }), status: t.arg.string(), limit: t.arg.int(), offset: t.arg.int(), }, resolve: async (_root, args, ctx) => { requireAdmin(ctx); const queue = queues[args.queue]; if (!queue) throw new GraphQLError(`Queue "${args.queue}" not found`); const limit = args.limit ?? 25; const offset = args.offset ?? 0; const statuses: JobStatus[] = args.status ? [args.status as JobStatus] : [...JOB_STATUSES]; const jobs = await queue.getJobs(statuses, offset, offset + limit - 1); return Promise.all(jobs.map((job) => toJobData(job, args.queue))); }, }), ); builder.mutationField("adminRetryJob", (t) => t.field({ type: "Boolean", args: { queue: t.arg.string({ required: true }), jobId: t.arg.string({ required: true }), }, resolve: async (_root, args, ctx) => { requireAdmin(ctx); const queue = queues[args.queue]; if (!queue) throw new GraphQLError(`Queue "${args.queue}" not found`); const job = await queue.getJob(args.jobId); if (!job) throw new GraphQLError(`Job "${args.jobId}" not found`); await job.retry(); return true; }, }), ); builder.mutationField("adminRemoveJob", (t) => t.field({ type: "Boolean", args: { queue: t.arg.string({ required: true }), jobId: t.arg.string({ required: true }), }, resolve: async (_root, args, ctx) => { requireAdmin(ctx); const queue = queues[args.queue]; if (!queue) throw new GraphQLError(`Queue "${args.queue}" not found`); const job = await queue.getJob(args.jobId); if (!job) throw new GraphQLError(`Job "${args.jobId}" not found`); await job.remove(); return true; }, }), ); builder.mutationField("adminPauseQueue", (t) => t.field({ type: "Boolean", args: { queue: t.arg.string({ required: true }) }, resolve: async (_root, args, ctx) => { requireAdmin(ctx); const queue = queues[args.queue]; if (!queue) throw new GraphQLError(`Queue "${args.queue}" not found`); await queue.pause(); return true; }, }), ); builder.mutationField("adminResumeQueue", (t) => t.field({ type: "Boolean", args: { queue: t.arg.string({ required: true }) }, resolve: async (_root, args, ctx) => { requireAdmin(ctx); const queue = queues[args.queue]; if (!queue) throw new GraphQLError(`Queue "${args.queue}" not found`); await queue.resume(); return true; }, }), );