feat: add auto-increment id, size/duration tracking, and soft cleanup to conversion_jobs

Rename the old CHAR(36) id to uuid (still used for public URLs and file
naming) and add a real auto-increment id as the primary key. Track
input/output file size and conversion duration per job. Cleanup no
longer deletes rows; it marks cleaned_at and skips already-cleaned
expired jobs on later runs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 22:06:32 +02:00
co-authored by Claude Sonnet 5
parent 24f7ab2625
commit ff5a257a0e
10 changed files with 208 additions and 117 deletions
+41 -24
View File
@@ -3,14 +3,14 @@ import { getPool, closePool } from '../../src/db.js';
import { loadConfig } from '../../src/config.js';
import {
createJob,
getJobById,
getJobByUuid,
getJobErrorLog,
markProcessing,
markDone,
markFailed,
findPendingJobs,
findExpiredJobs,
deleteJob,
markCleaned,
} from '../../src/jobs/jobRepository.js';
let pool;
@@ -29,13 +29,14 @@ beforeEach(async () => {
function baseJob(overrides = {}) {
return {
id: overrides.id ?? '11111111-1111-4111-8111-111111111111',
uuid: overrides.uuid ?? '11111111-1111-4111-8111-111111111111',
family: 'image',
sourceFormat: 'png',
targetFormat: 'webp',
originalFilename: 'photo.png',
inputPath: `${overrides.id ?? '11111111-1111-4111-8111-111111111111'}.png`,
inputPath: `${overrides.uuid ?? '11111111-1111-4111-8111-111111111111'}.png`,
inputMimeType: 'image/png',
inputSizeBytes: 1024,
expiresAt: new Date(Date.now() + 3600 * 1000),
...overrides,
};
@@ -45,78 +46,94 @@ describe('jobRepository', () => {
it('creates and retrieves a pending job', async () => {
await createJob(pool, baseJob());
const job = await getJobById(pool, '11111111-1111-4111-8111-111111111111');
const job = await getJobByUuid(pool, '11111111-1111-4111-8111-111111111111');
expect(job.id).toEqual(expect.any(Number));
expect(job.uuid).toBe('11111111-1111-4111-8111-111111111111');
expect(job.status).toBe('pending');
expect(job.family).toBe('image');
expect(job.sourceFormat).toBe('png');
expect(job.targetFormat).toBe('webp');
expect(job.originalFilename).toBe('photo.png');
expect(job.inputMimeType).toBe('image/png');
expect(job.inputSizeBytes).toBe(1024);
expect(job.outputPath).toBeNull();
expect(job.outputSizeBytes).toBeNull();
expect(job.conversionDurationSeconds).toBeNull();
expect(job.errorMessage).toBeNull();
expect(job.cleanedAt).toBeNull();
});
it('returns null for an unknown id', async () => {
const job = await getJobById(pool, '22222222-2222-4222-8222-222222222222');
it('returns null for an unknown uuid', async () => {
const job = await getJobByUuid(pool, '22222222-2222-4222-8222-222222222222');
expect(job).toBeNull();
});
it('transitions a job through processing to done', async () => {
await createJob(pool, baseJob());
await markProcessing(pool, '11111111-1111-4111-8111-111111111111');
const processing = await getJobById(pool, '11111111-1111-4111-8111-111111111111');
await markProcessing(pool, (await getJobByUuid(pool, '11111111-1111-4111-8111-111111111111')).id);
const processing = await getJobByUuid(pool, '11111111-1111-4111-8111-111111111111');
expect(processing.status).toBe('processing');
await markDone(pool, '11111111-1111-4111-8111-111111111111', {
await markDone(pool, processing.id, {
outputPath: '11111111-1111-4111-8111-111111111111.webp',
outputMimeType: 'image/webp',
outputSizeBytes: 2048,
conversionDurationSeconds: 1.5,
});
const done = await getJobById(pool, '11111111-1111-4111-8111-111111111111');
const done = await getJobByUuid(pool, '11111111-1111-4111-8111-111111111111');
expect(done.status).toBe('done');
expect(done.outputPath).toBe('11111111-1111-4111-8111-111111111111.webp');
expect(done.outputMimeType).toBe('image/webp');
expect(done.outputSizeBytes).toBe(2048);
expect(Number(done.conversionDurationSeconds)).toBe(1.5);
});
it('marks a job failed with a short message and a separate detailed log', async () => {
await createJob(pool, baseJob());
const created = await getJobByUuid(pool, '11111111-1111-4111-8111-111111111111');
await markFailed(pool, '11111111-1111-4111-8111-111111111111', {
await markFailed(pool, created.id, {
errorMessage: 'Conversion failed, please try again',
errorLog: 'Error: sharp threw at line 42\n at convert (image.js:10:5)',
});
const job = await getJobById(pool, '11111111-1111-4111-8111-111111111111');
const job = await getJobByUuid(pool, '11111111-1111-4111-8111-111111111111');
expect(job.status).toBe('failed');
expect(job.errorMessage).toBe('Conversion failed, please try again');
expect(job.errorLog).toBeUndefined();
const errorLog = await getJobErrorLog(pool, '11111111-1111-4111-8111-111111111111');
const errorLog = await getJobErrorLog(pool, created.id);
expect(errorLog).toBe('Error: sharp threw at line 42\n at convert (image.js:10:5)');
});
it('finds pending jobs oldest first, up to a limit', async () => {
await createJob(pool, baseJob({ id: '33333333-3333-4333-8333-333333333333' }));
await createJob(pool, baseJob({ id: '44444444-4444-4444-8444-444444444444' }));
await createJob(pool, baseJob({ id: '55555555-5555-4555-8555-555555555555' }));
await createJob(pool, baseJob({ uuid: '33333333-3333-4333-8333-333333333333' }));
await createJob(pool, baseJob({ uuid: '44444444-4444-4444-8444-444444444444' }));
await createJob(pool, baseJob({ uuid: '55555555-5555-4555-8555-555555555555' }));
const jobs = await findPendingJobs(pool, 2);
expect(jobs).toHaveLength(2);
expect(jobs[0].id).toBe('33333333-3333-4333-8333-333333333333');
expect(jobs[1].id).toBe('44444444-4444-4444-8444-444444444444');
expect(jobs[0].uuid).toBe('33333333-3333-4333-8333-333333333333');
expect(jobs[1].uuid).toBe('44444444-4444-4444-8444-444444444444');
});
it('finds expired jobs and allows deleting them', async () => {
it('finds expired jobs and allows marking them cleaned without deleting the row', async () => {
await createJob(pool, baseJob({ expiresAt: new Date(Date.now() - 1000) }));
const expired = await findExpiredJobs(pool);
expect(expired).toHaveLength(1);
expect(expired[0].id).toBe('11111111-1111-4111-8111-111111111111');
expect(expired[0].uuid).toBe('11111111-1111-4111-8111-111111111111');
await deleteJob(pool, '11111111-1111-4111-8111-111111111111');
const afterDelete = await getJobById(pool, '11111111-1111-4111-8111-111111111111');
expect(afterDelete).toBeNull();
await markCleaned(pool, expired[0].id);
const stillPresent = await getJobByUuid(pool, '11111111-1111-4111-8111-111111111111');
expect(stillPresent).not.toBeNull();
expect(stillPresent.cleanedAt).not.toBeNull();
const afterCleaning = await findExpiredJobs(pool);
expect(afterCleaning).toHaveLength(0);
});
});