adpa-enterprise-framework-automation
Version:
Modular, standards-compliant Node.js/TypeScript automation framework for enterprise requirements, project, and data management. Provides CLI and API for BABOK v3, PMBOK 7th Edition, and DMBOK 2.0 (in progress). Production-ready Express.js API with TypeSpe
386 lines • 13.9 kB
JavaScript
import { v4 as uuidv4 } from 'uuid';
import { DocumentProcessor } from '../services/DocumentProcessor.js';
import { JobManager } from '../services/JobManager.js';
import { InputFormat, OutputFormat } from '../types/api.js';
/**
* Document Processing API Controller
*
* Implements the TypeSpec-defined document processing endpoints
* with full CRUD operations for document conversion jobs.
*/
export class DocumentController {
static documentProcessor = new DocumentProcessor();
static jobManager = new JobManager();
/**
* Helper method to convert DocumentJob to DocumentConversionResponse
*/
static convertJobToResponse(job) {
return {
jobId: job.id,
status: job.status,
downloadUrl: job.downloadUrl,
fileSize: job.fileSize,
progress: job.progress,
estimatedCompletion: job.estimatedCompletion,
createdAt: job.createdAt,
completedAt: job.completedAt,
processingDuration: job.processingDuration,
outputFormat: job.outputFormat,
originalFilename: job.originalFilename,
generatedFilename: job.generatedFilename,
error: job.error,
logs: job.logs
};
}
/**
* Convert a single document
* POST /api/v1/documents/convert
*/
static async convertDocument(req, res, next) {
try {
const request = req.body;
const requestId = req.headers['x-request-id'] || uuidv4();
// Validate request
if (!request.content || !request.inputFormat || !request.outputFormat) {
return res.status(400).json({
success: false,
error: {
code: 'MISSING_REQUIRED_FIELD',
message: 'Missing required fields: content, inputFormat, or outputFormat',
timestamp: new Date().toISOString(),
errorId: uuidv4()
},
requestId
});
}
// Start document conversion
const job = await DocumentController.documentProcessor.startConversion(request);
const response = {
jobId: job.id,
status: job.status,
progress: job.progress,
createdAt: job.createdAt,
outputFormat: request.outputFormat,
estimatedCompletion: job.estimatedCompletion
};
res.status(202).json({
success: true,
data: response,
timestamp: new Date().toISOString(),
requestId
});
}
catch (error) {
next(error);
}
}
/**
* Convert multiple documents in batch
* POST /api/v1/documents/batch/convert
*/
static async batchConvert(req, res, next) {
try {
const request = req.body;
const requestId = req.headers['x-request-id'] || uuidv4();
// Validate batch request
if (!request.documents || request.documents.length === 0) {
return res.status(400).json({
success: false,
error: {
code: 'INVALID_REQUEST',
message: 'Batch request must contain at least one document',
timestamp: new Date().toISOString()
},
requestId
});
}
if (request.documents.length > 100) {
return res.status(400).json({
success: false,
error: {
code: 'INVALID_REQUEST',
message: 'Batch request cannot exceed 100 documents',
timestamp: new Date().toISOString()
},
requestId
});
}
// Start batch conversion
const batch = await DocumentController.documentProcessor.startBatchConversion(request);
const response = {
batchId: batch.id,
jobs: batch.jobs.map(job => DocumentController.convertJobToResponse(job)),
status: batch.status,
progress: batch.progress,
successCount: batch.successCount,
failureCount: batch.failureCount,
createdAt: batch.createdAt,
batchName: request.batchName
};
res.status(202).json({
success: true,
data: response,
timestamp: new Date().toISOString(),
requestId
});
}
catch (error) {
next(error);
}
}
/**
* Get job status
* GET /api/v1/documents/jobs/:jobId
*/
static async getJobStatus(req, res, next) {
try {
const { jobId } = req.params;
const requestId = req.headers['x-request-id'] || uuidv4();
const job = await DocumentController.jobManager.getJob(jobId);
if (!job) {
return res.status(404).json({
success: false,
error: {
code: 'RESOURCE_NOT_FOUND',
message: `Job with ID ${jobId} not found`,
timestamp: new Date().toISOString()
},
requestId
});
}
const response = DocumentController.convertJobToResponse(job);
res.json({
success: true,
data: response,
timestamp: new Date().toISOString(),
requestId
});
}
catch (error) {
next(error);
}
}
/**
* Download converted document
* GET /api/v1/documents/download/:jobId
*/
static async downloadDocument(req, res, next) {
try {
const { jobId } = req.params;
const job = await DocumentController.jobManager.getJob(jobId);
if (!job) {
return res.status(404).json({
success: false,
error: {
code: 'RESOURCE_NOT_FOUND',
message: `Job with ID ${jobId} not found`,
timestamp: new Date().toISOString()
}
});
}
if (job.status !== 'completed') {
return res.status(400).json({
success: false,
error: {
code: 'INVALID_REQUEST',
message: 'Document is not ready for download',
details: `Current status: ${job.status}`,
timestamp: new Date().toISOString()
}
});
}
// Get file content and metadata
const fileData = await DocumentController.jobManager.getJobFile(jobId);
// Set response headers
res.setHeader('Content-Type', fileData.contentType);
res.setHeader('Content-Disposition', `attachment; filename="${fileData.filename}"`);
res.setHeader('Content-Length', fileData.size);
// Stream file content
res.send(fileData.content);
}
catch (error) {
next(error);
}
}
/**
* Cancel conversion job
* DELETE /api/v1/documents/jobs/:jobId
*/
static async cancelJob(req, res, next) {
try {
const { jobId } = req.params;
const requestId = req.headers['x-request-id'] || uuidv4();
const result = await DocumentController.jobManager.cancelJob(jobId);
if (!result.success) {
return res.status(result.statusCode || 400).json({
success: false,
error: {
code: result.errorCode || 'OPERATION_FAILED',
message: result.message,
timestamp: new Date().toISOString()
},
requestId
});
}
res.json({
success: true,
data: null,
timestamp: new Date().toISOString(),
requestId
});
}
catch (error) {
next(error);
}
}
/**
* List conversion jobs
* GET /api/v1/documents/jobs
*/
static async listJobs(req, res, next) {
try {
const { page = 1, limit = 20, sort = 'createdAt', order = 'desc', status, fromDate, toDate, outputFormat, search } = req.query;
const requestId = req.headers['x-request-id'] || uuidv4();
const filters = {
status: status,
fromDate: fromDate ? new Date(fromDate) : undefined,
toDate: toDate ? new Date(toDate) : undefined,
outputFormat: outputFormat,
search: search
};
const result = await DocumentController.jobManager.listJobs({
page: parseInt(page),
limit: Math.min(parseInt(limit), 100),
sort: sort,
order: order,
filters
});
res.json({
success: true,
data: result,
timestamp: new Date().toISOString(),
requestId
});
}
catch (error) {
next(error);
}
}
/**
* Get conversion statistics
* GET /api/v1/documents/stats
*/
static async getStats(req, res, next) {
try {
const { fromDate, toDate, groupBy = 'day' } = req.query;
const requestId = req.headers['x-request-id'] || uuidv4();
const stats = await DocumentController.jobManager.getStatistics({
fromDate: fromDate ? new Date(fromDate) : undefined,
toDate: toDate ? new Date(toDate) : undefined,
groupBy: groupBy
});
res.json({
success: true,
data: stats,
timestamp: new Date().toISOString(),
requestId
});
}
catch (error) {
next(error);
}
}
/**
* Retry failed job
* POST /api/v1/documents/jobs/:jobId/retry
*/
static async retryJob(req, res, next) {
try {
const { jobId } = req.params;
const options = req.body;
const requestId = req.headers['x-request-id'] || uuidv4();
const result = await DocumentController.jobManager.retryJob(jobId, options);
if (!result.success) {
return res.status(result.statusCode || 400).json({
success: false,
error: {
code: result.errorCode || 'OPERATION_FAILED',
message: result.message,
timestamp: new Date().toISOString()
},
requestId
});
}
res.json({
success: true,
data: result.job,
timestamp: new Date().toISOString(),
requestId
});
}
catch (error) {
next(error);
}
}
/**
* Get supported format combinations
* GET /api/v1/documents/formats
*/
static async getSupportedFormats(req, res, next) {
try {
const requestId = req.headers['x-request-id'] || uuidv4();
const formats = {
inputFormats: Object.values(InputFormat),
outputFormats: Object.values(OutputFormat),
conversions: [
{ input: InputFormat.markdown, outputs: [OutputFormat.pdf, OutputFormat.docx, OutputFormat.html, OutputFormat.pptx] },
{ input: InputFormat.html, outputs: [OutputFormat.pdf, OutputFormat.docx] },
{ input: InputFormat.docx, outputs: [OutputFormat.pdf, OutputFormat.html] },
{ input: InputFormat.txt, outputs: [OutputFormat.pdf, OutputFormat.docx, OutputFormat.html] },
{ input: InputFormat.rtf, outputs: [OutputFormat.pdf, OutputFormat.docx, OutputFormat.html] }
]
};
res.json({
success: true,
data: formats,
timestamp: new Date().toISOString(),
requestId
});
}
catch (error) {
next(error);
}
}
/**
* Get batch status
* GET /api/v1/documents/batch/:batchId
*/
static async getBatchStatus(req, res, next) {
try {
const { batchId } = req.params;
const requestId = req.headers['x-request-id'] || uuidv4();
const batch = await DocumentController.jobManager.getBatch(batchId);
if (!batch) {
return res.status(404).json({
success: false,
error: {
code: 'RESOURCE_NOT_FOUND',
message: `Batch with ID ${batchId} not found`,
timestamp: new Date().toISOString()
},
requestId
});
}
res.json({
success: true,
data: batch,
timestamp: new Date().toISOString(),
requestId
});
}
catch (error) {
next(error);
}
}
}
//# sourceMappingURL=DocumentController.js.map