@qontrol/express
Version:
Express.js middleware for BullMQ monitoring
514 lines • 18.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.JobsController = void 0;
const core_1 = require("@qontrol/core");
const node_stream_1 = require("node:stream");
class JobsController {
constructor(qontrol) {
this.qontrol = qontrol;
}
/**
* GET /api/queues/:queue/jobs
* Get jobs for a specific queue with pagination or all streamed
*/
async getJobs(req, res) {
const queueName = req.params.queue;
const params = req.validatedQuery;
try {
// Get the async generator from Qontrol
const jobsGenerator = this.qontrol.getJobs(queueName, params);
const toJsonTransform = new node_stream_1.Transform({
objectMode: true,
transform(chunk, encoding, callback) {
if (chunk.jobs.length > 0) {
const json = JSON.stringify(chunk) + '\n';
callback(null, json);
}
else {
callback();
}
},
flush(callback) {
callback();
}
});
// Set proper headers for streaming JSON
res.setHeader('Content-Type', 'application/x-ndjson'); // Newline-delimited JSON
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const stream = node_stream_1.Readable.from(jobsGenerator);
// Handle stream errors before starting
stream.on('error', (error) => {
core_1.Logger.getInstance().error('Error streaming jobs:', error);
if (!res.headersSent) {
res.status(500).json({
message: 'Failed to stream jobs',
code: 'JOBS_STREAM_ERROR',
});
}
else {
// If headers already sent, just end the response
res.end();
}
});
toJsonTransform.on('error', (error) => {
core_1.Logger.getInstance().error('Error in transform stream:', error);
if (!res.headersSent) {
res.status(500).json({
message: 'Failed to transform jobs',
code: 'JOBS_TRANSFORM_ERROR',
});
}
else {
res.end();
}
});
// Start streaming
stream
.pipe(toJsonTransform)
.pipe(res);
}
catch (error) {
core_1.Logger.getInstance().error('Error setting up jobs stream:', error);
if (!res.headersSent) {
res.status(500).json({
message: 'Failed to setup jobs stream',
code: 'JOBS_SETUP_ERROR',
});
}
}
}
/**
* GET /api/queues/:queue/job-by-id/:id
* Fast lookup for a specific job by ID
*/
async getJobById(req, res) {
try {
const { queue: queueName, id: jobId } = req.params;
const { jobs, total } = await this.qontrol.getJobById(queueName, jobId);
const response = {
jobs,
pagination: {
page: 1,
pageSize: 1,
total,
totalPages: 1,
},
filters: {},
timestamp: new Date(),
};
res.json(response);
}
catch (error) {
res.status(500).json({
message: 'Failed to fetch job by ID',
code: 'JOB_ID_FETCH_ERROR',
});
}
}
/**
* GET /api/queues/:queue/jobs/:id
* Get detailed information for a specific job
*/
async getJobDetail(req, res) {
try {
const { queue: queueName, id: jobId } = req.params;
const job = await this.qontrol.getJobDetail(queueName, jobId);
if (!job) {
return res.status(404).json({
message: 'Job not found',
code: 'JOB_NOT_FOUND',
});
}
const response = {
job,
timestamp: new Date(),
};
res.json(response);
}
catch (error) {
res.status(500).json({
message: 'Failed to fetch job details',
code: 'JOB_DETAIL_FETCH_ERROR',
});
}
}
/**
* GET /api/queues/:queue/jobs/:id/logs
* Get logs for a specific job
*/
async getJobLogs(req, res) {
try {
const { queue: queueName, id: jobId } = req.params;
const start = parseInt(req.query.start) || 0;
const end = parseInt(req.query.end) || -1;
const logsData = await this.qontrol.getJobLogs(queueName, jobId, start, end);
res.json({
logs: logsData.logs,
count: logsData.count,
timestamp: new Date(),
});
}
catch (error) {
res.status(500).json({
message: 'Failed to fetch job logs',
code: 'JOB_LOGS_FETCH_ERROR',
});
}
}
/**
* POST /api/queues/:queue/jobs
* Add a new job to the queue
*/
async addJob(req, res) {
try {
const { queue: queueName } = req.params;
const { name, data, options } = req.body;
// Validate required fields
if (!name || typeof name !== 'string') {
return res.status(400).json({
message: 'Job name is required and must be a string',
code: 'INVALID_JOB_NAME',
});
}
// Validate data - if provided as string, try to parse as JSON
let parsedData = {};
if (data !== undefined) {
if (typeof data === 'string') {
try {
parsedData = JSON.parse(data);
}
catch (e) {
return res.status(400).json({
message: 'Job data must be valid JSON',
code: 'INVALID_JOB_DATA_JSON',
details: 'Unable to parse data as JSON'
});
}
}
else if (typeof data === 'object' && data !== null) {
parsedData = data;
}
else {
return res.status(400).json({
message: 'Job data must be a valid JSON object or string',
code: 'INVALID_JOB_DATA',
});
}
}
// Validate options - if provided as string, try to parse as JSON
let parsedOptions = {};
if (options !== undefined) {
if (typeof options === 'string') {
try {
parsedOptions = JSON.parse(options);
}
catch (e) {
return res.status(400).json({
message: 'Job options must be valid JSON',
code: 'INVALID_JOB_OPTIONS_JSON',
details: 'Unable to parse options as JSON'
});
}
}
else if (typeof options === 'object' && options !== null) {
parsedOptions = options;
}
else {
return res.status(400).json({
message: 'Job options must be a valid JSON object or string',
code: 'INVALID_JOB_OPTIONS',
});
}
}
const result = await this.qontrol.addJob(queueName, {
name: name.trim(),
data: parsedData,
options: parsedOptions
});
res.status(201).json({
message: 'Job added successfully',
jobId: result.jobId,
queueName: result.queueName,
timestamp: new Date(),
});
}
catch (error) {
core_1.Logger.getInstance().error('Add job error:', error);
res.status(500).json({
message: 'Failed to add job',
code: 'JOB_ADD_ERROR',
details: error instanceof Error ? error.message : 'Unknown error'
});
}
}
/**
* DELETE /api/queues/:queue/jobs/:id
* Remove a specific job by ID with removeChildren flag
*/
async removeJob(req, res) {
try {
const { queue: queueName, id: jobId } = req.params;
const success = await this.qontrol.removeJob(queueName, jobId);
if (!success) {
return res.status(404).json({
message: 'Job not found or could not be removed',
code: 'JOB_REMOVE_FAILED',
});
}
res.json({
message: 'Job removed successfully',
jobId,
queueName,
timestamp: new Date(),
});
}
catch (error) {
res.status(500).json({
message: 'Failed to remove job',
code: 'JOB_REMOVE_ERROR',
});
}
}
/**
* POST /api/queues/:queue/jobs/:id/retry
* Retry a failed job
*/
async retryJob(req, res) {
try {
const { queue: queueName, id: jobId } = req.params;
const success = await this.qontrol.retryJob(queueName, jobId);
if (!success) {
return res.status(404).json({
message: 'Job not found or cannot be retried',
code: 'JOB_RETRY_FAILED',
});
}
res.json({
message: 'Job retry initiated successfully',
jobId,
queueName,
timestamp: new Date(),
});
}
catch (error) {
res.status(500).json({
message: 'Failed to retry job',
code: 'JOB_RETRY_ERROR',
});
}
}
/**
* POST /api/queues/:queue/jobs/:id/discard
* Discard an active job
*/
async discardJob(req, res) {
try {
const { queue: queueName, id: jobId } = req.params;
const success = await this.qontrol.discardJob(queueName, jobId);
if (!success) {
return res.status(404).json({
message: 'Job not found or cannot be discarded',
code: 'JOB_DISCARD_FAILED',
});
}
res.json({
message: 'Job discarded successfully',
jobId,
queueName,
timestamp: new Date(),
});
}
catch (error) {
res.status(500).json({
message: 'Failed to discard job',
code: 'JOB_DISCARD_ERROR',
});
}
}
/**
* POST /api/queues/:queue/jobs/:id/promote
* Promote a delayed job
*/
async promoteJob(req, res) {
try {
const { queue: queueName, id: jobId } = req.params;
const success = await this.qontrol.promoteJob(queueName, jobId);
if (!success) {
return res.status(404).json({
message: 'Job not found or cannot be promoted',
code: 'JOB_PROMOTE_FAILED',
});
}
res.json({
message: 'Job promoted successfully',
jobId,
queueName,
timestamp: new Date(),
});
}
catch (error) {
res.status(500).json({
message: 'Failed to promote job',
code: 'JOB_PROMOTE_ERROR',
});
}
}
/**
* POST /api/queues/:queue/jobs/bulk-remove
* Bulk remove jobs by their IDs
*/
async bulkRemoveJobs(req, res) {
try {
const { queue: queueName } = req.params;
const { jobIds } = req.body;
// Validate input
if (!Array.isArray(jobIds) || jobIds.length === 0) {
return res.status(400).json({
message: 'jobIds must be a non-empty array',
code: 'INVALID_JOB_IDS',
});
}
// Limit bulk operations to prevent overload
if (jobIds.length > 100) {
return res.status(400).json({
message: 'Cannot remove more than 100 jobs at once',
code: 'TOO_MANY_JOBS',
});
}
const result = await this.qontrol.bulkRemoveJobs(queueName, jobIds);
const response = {
success: result.success,
failed: result.failed,
errors: result.errors,
timestamp: new Date(),
};
res.json(response);
}
catch (error) {
res.status(500).json({
message: 'Failed to bulk remove jobs',
code: 'BULK_REMOVE_ERROR',
});
}
}
/**
* POST /api/queues/:queue/jobs/bulk-retry
* Bulk retry jobs by their IDs
*/
async bulkRetryJobs(req, res) {
try {
const { queue: queueName } = req.params;
const { jobIds } = req.body;
// Validate input
if (!Array.isArray(jobIds) || jobIds.length === 0) {
return res.status(400).json({
message: 'jobIds must be a non-empty array',
code: 'INVALID_JOB_IDS',
});
}
// Limit bulk operations to prevent overload
if (jobIds.length > 100) {
return res.status(400).json({
message: 'Cannot retry more than 100 jobs at once',
code: 'TOO_MANY_JOBS',
});
}
const result = await this.qontrol.bulkRetryJobs(queueName, jobIds);
const response = {
success: result.success,
failed: result.failed,
errors: result.errors,
timestamp: new Date(),
};
res.json(response);
}
catch (error) {
res.status(500).json({
message: 'Failed to bulk retry jobs',
code: 'BULK_RETRY_ERROR',
});
}
}
/**
* POST /api/queues/:queue/jobs/bulk-export
* Bulk export jobs by streaming job details in chunks
*/
async bulkExportJobs(req, res) {
try {
const { queue: queueName } = req.params;
const { jobIds } = req.body;
// Validate input
if (!Array.isArray(jobIds) || jobIds.length === 0) {
return res.status(400).json({
message: 'jobIds must be a non-empty array',
code: 'INVALID_JOB_IDS',
});
}
// Limit bulk operations to prevent overload
if (jobIds.length > 1000) {
return res.status(400).json({
message: 'Cannot export more than 1000 jobs at once',
code: 'TOO_MANY_JOBS',
});
}
// Get the async generator from Qontrol
const exportGenerator = this.qontrol.bulkExportJobs(queueName, jobIds);
const toJsonTransform = new node_stream_1.Transform({
objectMode: true,
transform(chunk, encoding, callback) {
const json = JSON.stringify(chunk) + '\n';
callback(null, json);
},
flush(callback) {
callback();
}
});
// Set proper headers for streaming JSON
res.setHeader('Content-Type', 'application/x-ndjson'); // Newline-delimited JSON
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const stream = node_stream_1.Readable.from(exportGenerator);
// Handle stream errors before starting
stream.on('error', (error) => {
core_1.Logger.getInstance().error('Error streaming bulk export:', error);
if (!res.headersSent) {
res.status(500).json({
message: 'Failed to stream bulk export',
code: 'BULK_EXPORT_STREAM_ERROR',
});
}
else {
res.end();
}
});
toJsonTransform.on('error', (error) => {
core_1.Logger.getInstance().error('Error in bulk export transform stream:', error);
if (!res.headersSent) {
res.status(500).json({
message: 'Failed to transform bulk export',
code: 'BULK_EXPORT_TRANSFORM_ERROR',
});
}
else {
res.end();
}
});
// Start streaming
stream
.pipe(toJsonTransform)
.pipe(res);
}
catch (error) {
core_1.Logger.getInstance().error('Error setting up bulk export stream:', error);
if (!res.headersSent) {
res.status(500).json({
message: 'Failed to setup bulk export stream',
code: 'BULK_EXPORT_SETUP_ERROR',
});
}
}
}
}
exports.JobsController = JobsController;
//# sourceMappingURL=jobs.controller.js.map