diff --git a/src/db.js b/src/db.js index a630dda..900fce4 100644 --- a/src/db.js +++ b/src/db.js @@ -1,28 +1,23 @@ -import mariadb from 'mariadb'; +import { PrismaClient } from '@prisma/client'; -let pool; +let prisma; export function buildDatabaseUrl(config) { const { host, user, password, database } = config.db; return `mysql://${encodeURIComponent(user)}:${encodeURIComponent(password)}@${host}:3306/${database}?connection_limit=10`; } -export function getPool(config) { - if (!pool) { - pool = mariadb.createPool({ - host: config.db.host, - user: config.db.user, - password: config.db.password, - database: config.db.database, - connectionLimit: 10, - }); +export function getPrismaClient(config) { + if (!prisma) { + process.env.DATABASE_URL = buildDatabaseUrl(config); + prisma = new PrismaClient(); } - return pool; + return prisma; } -export async function closePool() { - if (pool) { - await pool.end(); - pool = undefined; +export async function closePrismaClient() { + if (prisma) { + await prisma.$disconnect(); + prisma = undefined; } } diff --git a/src/jobs/jobRepository.js b/src/jobs/jobRepository.js index e880532..65f4f5d 100644 --- a/src/jobs/jobRepository.js +++ b/src/jobs/jobRepository.js @@ -1,109 +1,86 @@ -function toCamelJob(row) { - if (!row) return null; - return { - id: row.id, - uuid: row.uuid, - status: row.status, - family: row.family, - sourceFormat: row.source_format, - targetFormat: row.target_format, - originalFilename: row.original_filename, - inputPath: row.input_path, - outputPath: row.output_path, - inputMimeType: row.input_mime_type, - outputMimeType: row.output_mime_type, - inputSizeBytes: row.input_size_bytes, - outputSizeBytes: row.output_size_bytes, - quality: row.quality, - conversionDurationSeconds: row.conversion_duration_seconds, - errorMessage: row.error_message, - createdAt: row.created_at, - updatedAt: row.updated_at, - expiresAt: row.expires_at, - cleanedAt: row.cleaned_at, - }; +const jobSelect = { + id: true, + uuid: true, + status: true, + family: true, + sourceFormat: true, + targetFormat: true, + originalFilename: true, + inputPath: true, + outputPath: true, + inputMimeType: true, + outputMimeType: true, + inputSizeBytes: true, + outputSizeBytes: true, + quality: true, + conversionDurationSeconds: true, + errorMessage: true, + createdAt: true, + updatedAt: true, + expiresAt: true, + cleanedAt: true, +}; + +export async function createJob(prisma, job) { + await prisma.conversionJob.create({ + data: { + uuid: job.uuid, + family: job.family, + sourceFormat: job.sourceFormat, + targetFormat: job.targetFormat, + originalFilename: job.originalFilename, + inputPath: job.inputPath, + inputMimeType: job.inputMimeType, + inputSizeBytes: job.inputSizeBytes, + expiresAt: job.expiresAt, + quality: job.quality ?? null, + }, + }); } -export async function createJob(pool, job) { - await pool.query( - `INSERT INTO conversion_jobs - (uuid, status, family, source_format, target_format, original_filename, input_path, input_mime_type, input_size_bytes, expires_at, quality) - VALUES (?, 'pending', ?, ?, ?, ?, ?, ?, ?, ?, ?)`, - [ - job.uuid, - job.family, - job.sourceFormat, - job.targetFormat, - job.originalFilename, - job.inputPath, - job.inputMimeType, - job.inputSizeBytes, - job.expiresAt, - job.quality ?? null, - ] - ); +export async function getJobByUuid(prisma, uuid) { + return prisma.conversionJob.findUnique({ where: { uuid }, select: jobSelect }); } -export async function getJobByUuid(pool, uuid) { - const rows = await pool.query( - `SELECT id, uuid, status, family, source_format, target_format, original_filename, - input_path, output_path, input_mime_type, output_mime_type, - input_size_bytes, output_size_bytes, quality, conversion_duration_seconds, - error_message, created_at, updated_at, expires_at, cleaned_at - FROM conversion_jobs WHERE uuid = ?`, - [uuid] - ); - return toCamelJob(rows[0]); +export async function getJobErrorLog(prisma, id) { + const job = await prisma.conversionJob.findUnique({ where: { id }, select: { errorLog: true } }); + return job?.errorLog ?? null; } -export async function getJobErrorLog(pool, id) { - const rows = await pool.query('SELECT error_log FROM conversion_jobs WHERE id = ?', [id]); - return rows[0]?.error_log ?? null; +export async function markProcessing(prisma, id) { + await prisma.conversionJob.update({ where: { id }, data: { status: 'processing' } }); } -export async function markProcessing(pool, id) { - await pool.query("UPDATE conversion_jobs SET status = 'processing' WHERE id = ?", [id]); +export async function markDone(prisma, id, { outputPath, outputMimeType, outputSizeBytes, conversionDurationSeconds }) { + await prisma.conversionJob.update({ + where: { id }, + data: { status: 'done', outputPath, outputMimeType, outputSizeBytes, conversionDurationSeconds }, + }); } -export async function markDone(pool, id, { outputPath, outputMimeType, outputSizeBytes, conversionDurationSeconds }) { - await pool.query( - `UPDATE conversion_jobs - SET status = 'done', output_path = ?, output_mime_type = ?, output_size_bytes = ?, conversion_duration_seconds = ? - WHERE id = ?`, - [outputPath, outputMimeType, outputSizeBytes, conversionDurationSeconds, id] - ); +export async function markFailed(prisma, id, { errorMessage, errorLog }) { + await prisma.conversionJob.update({ + where: { id }, + data: { status: 'failed', errorMessage, errorLog }, + }); } -export async function markFailed(pool, id, { errorMessage, errorLog }) { - await pool.query( - "UPDATE conversion_jobs SET status = 'failed', error_message = ?, error_log = ? WHERE id = ?", - [errorMessage, errorLog, id] - ); +export async function findPendingJobs(prisma, limit) { + return prisma.conversionJob.findMany({ + where: { status: 'pending' }, + orderBy: { createdAt: 'asc' }, + take: limit, + select: jobSelect, + }); } -export async function findPendingJobs(pool, limit) { - const rows = await pool.query( - `SELECT id, uuid, status, family, source_format, target_format, original_filename, - input_path, output_path, input_mime_type, output_mime_type, - input_size_bytes, output_size_bytes, quality, conversion_duration_seconds, - error_message, created_at, updated_at, expires_at, cleaned_at - FROM conversion_jobs WHERE status = 'pending' ORDER BY created_at ASC LIMIT ?`, - [limit] - ); - return rows.map(toCamelJob); +export async function findExpiredJobs(prisma) { + return prisma.conversionJob.findMany({ + where: { expiresAt: { lt: new Date() }, cleanedAt: null }, + select: jobSelect, + }); } -export async function findExpiredJobs(pool) { - const rows = await pool.query( - `SELECT id, uuid, status, family, source_format, target_format, original_filename, - input_path, output_path, input_mime_type, output_mime_type, - input_size_bytes, output_size_bytes, quality, conversion_duration_seconds, - error_message, created_at, updated_at, expires_at, cleaned_at - FROM conversion_jobs WHERE expires_at < NOW() AND cleaned_at IS NULL` - ); - return rows.map(toCamelJob); -} - -export async function markCleaned(pool, id) { - await pool.query('UPDATE conversion_jobs SET cleaned_at = NOW() WHERE id = ?', [id]); +export async function markCleaned(prisma, id) { + await prisma.conversionJob.update({ where: { id }, data: { cleanedAt: new Date() } }); } diff --git a/test/db.test.js b/test/db.test.js index 11e0147..76fc3cd 100644 --- a/test/db.test.js +++ b/test/db.test.js @@ -1,5 +1,5 @@ import { describe, it, expect, afterAll } from 'vitest'; -import { getPool, closePool, buildDatabaseUrl } from '../src/db.js'; +import { getPrismaClient, closePrismaClient, buildDatabaseUrl } from '../src/db.js'; import { loadConfig } from '../src/config.js'; describe('buildDatabaseUrl', () => { @@ -20,25 +20,25 @@ describe('buildDatabaseUrl', () => { }); }); -describe('getPool', () => { +describe('getPrismaClient', () => { afterAll(async () => { - await closePool(); + await closePrismaClient(); }); - it('returns a working pool that can run a query', async () => { + it('returns a working client that can run a query', async () => { const config = loadConfig(); - const pool = getPool(config); + const prisma = getPrismaClient(config); - const rows = await pool.query('SELECT 1 AS value'); + const rows = await prisma.$queryRaw`SELECT 1 AS value`; expect(Number(rows[0].value)).toBe(1); }); - it('returns the same pool instance on repeated calls', () => { + it('returns the same client instance on repeated calls', () => { const config = loadConfig(); - const poolA = getPool(config); - const poolB = getPool(config); + const clientA = getPrismaClient(config); + const clientB = getPrismaClient(config); - expect(poolA).toBe(poolB); + expect(clientA).toBe(clientB); }); }); diff --git a/test/jobs/jobRepository.test.js b/test/jobs/jobRepository.test.js index 8affa55..636f384 100644 --- a/test/jobs/jobRepository.test.js +++ b/test/jobs/jobRepository.test.js @@ -1,5 +1,5 @@ import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'; -import { getPool, closePool } from '../../src/db.js'; +import { getPrismaClient, closePrismaClient } from '../../src/db.js'; import { loadConfig } from '../../src/config.js'; import { createJob, @@ -13,18 +13,18 @@ import { markCleaned, } from '../../src/jobs/jobRepository.js'; -let pool; +let prisma; beforeAll(() => { - pool = getPool(loadConfig()); + prisma = getPrismaClient(loadConfig()); }); afterAll(async () => { - await closePool(); + await closePrismaClient(); }); beforeEach(async () => { - await pool.query('DELETE FROM conversion_jobs'); + await prisma.conversionJob.deleteMany(); }); function baseJob(overrides = {}) { @@ -44,9 +44,9 @@ function baseJob(overrides = {}) { describe('jobRepository', () => { it('creates and retrieves a pending job', async () => { - await createJob(pool, baseJob()); + await createJob(prisma, baseJob()); - const job = await getJobByUuid(pool, '11111111-1111-4111-8111-111111111111'); + const job = await getJobByUuid(prisma, '11111111-1111-4111-8111-111111111111'); expect(job.id).toEqual(expect.any(Number)); expect(job.uuid).toBe('11111111-1111-4111-8111-111111111111'); @@ -66,31 +66,31 @@ describe('jobRepository', () => { }); it('stores and retrieves a numeric quality value', async () => { - await createJob(pool, baseJob({ uuid: '66666666-6666-4666-8666-666666666666', quality: 82 })); + await createJob(prisma, baseJob({ uuid: '66666666-6666-4666-8666-666666666666', quality: 82 })); - const job = await getJobByUuid(pool, '66666666-6666-4666-8666-666666666666'); + const job = await getJobByUuid(prisma, '66666666-6666-4666-8666-666666666666'); expect(job.quality).toBe(82); }); it('returns null for an unknown uuid', async () => { - const job = await getJobByUuid(pool, '22222222-2222-4222-8222-222222222222'); + const job = await getJobByUuid(prisma, '22222222-2222-4222-8222-222222222222'); expect(job).toBeNull(); }); it('transitions a job through processing to done', async () => { - await createJob(pool, baseJob()); + await createJob(prisma, baseJob()); - await markProcessing(pool, (await getJobByUuid(pool, '11111111-1111-4111-8111-111111111111')).id); - const processing = await getJobByUuid(pool, '11111111-1111-4111-8111-111111111111'); + await markProcessing(prisma, (await getJobByUuid(prisma, '11111111-1111-4111-8111-111111111111')).id); + const processing = await getJobByUuid(prisma, '11111111-1111-4111-8111-111111111111'); expect(processing.status).toBe('processing'); - await markDone(pool, processing.id, { + await markDone(prisma, processing.id, { outputPath: '11111111-1111-4111-8111-111111111111.webp', outputMimeType: 'image/webp', outputSizeBytes: 2048, conversionDurationSeconds: 1.5, }); - const done = await getJobByUuid(pool, '11111111-1111-4111-8111-111111111111'); + const done = await getJobByUuid(prisma, '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'); @@ -99,29 +99,29 @@ describe('jobRepository', () => { }); 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 createJob(prisma, baseJob()); + const created = await getJobByUuid(prisma, '11111111-1111-4111-8111-111111111111'); - await markFailed(pool, created.id, { + await markFailed(prisma, 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 getJobByUuid(pool, '11111111-1111-4111-8111-111111111111'); + const job = await getJobByUuid(prisma, '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, created.id); + const errorLog = await getJobErrorLog(prisma, 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({ 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' })); + await createJob(prisma, baseJob({ uuid: '33333333-3333-4333-8333-333333333333' })); + await createJob(prisma, baseJob({ uuid: '44444444-4444-4444-8444-444444444444' })); + await createJob(prisma, baseJob({ uuid: '55555555-5555-4555-8555-555555555555' })); - const jobs = await findPendingJobs(pool, 2); + const jobs = await findPendingJobs(prisma, 2); expect(jobs).toHaveLength(2); expect(jobs[0].uuid).toBe('33333333-3333-4333-8333-333333333333'); @@ -129,19 +129,19 @@ describe('jobRepository', () => { }); it('finds expired jobs and allows marking them cleaned without deleting the row', async () => { - await createJob(pool, baseJob({ expiresAt: new Date(Date.now() - 1000) })); + await createJob(prisma, baseJob({ expiresAt: new Date(Date.now() - 1000) })); - const expired = await findExpiredJobs(pool); + const expired = await findExpiredJobs(prisma); expect(expired).toHaveLength(1); expect(expired[0].uuid).toBe('11111111-1111-4111-8111-111111111111'); - await markCleaned(pool, expired[0].id); + await markCleaned(prisma, expired[0].id); - const stillPresent = await getJobByUuid(pool, '11111111-1111-4111-8111-111111111111'); + const stillPresent = await getJobByUuid(prisma, '11111111-1111-4111-8111-111111111111'); expect(stillPresent).not.toBeNull(); expect(stillPresent.cleanedAt).not.toBeNull(); - const afterCleaning = await findExpiredJobs(pool); + const afterCleaning = await findExpiredJobs(prisma); expect(afterCleaning).toHaveLength(0); }); });