feat: claim durable image derivative jobs
Quality / test (push) Canceled after 0s

This commit is contained in:
2026-09-08 22:04:02 -04:00
parent 7be79f4840
commit abd574293b
4 changed files with 134 additions and 3 deletions
@@ -1,6 +1,9 @@
import { asc, eq } from 'drizzle-orm';
import { and, asc, eq } from 'drizzle-orm';
import { imageDerivativeJobs } from '../../../infrastructure/database/schema.js';
import {
imageDerivativeJobs,
inventoryImages,
} from '../../../infrastructure/database/schema.js';
export function createImageDerivativeJobRepository(db) {
return {
@@ -16,5 +19,71 @@ export function createImageDerivativeJobRepository(db) {
.where(eq(imageDerivativeJobs.inventoryImageId, inventoryImageId))
.orderBy(asc(imageDerivativeJobs.createdAt));
},
async claimNext() {
return db.transaction(async (tx) => {
const [job] = await tx
.select()
.from(imageDerivativeJobs)
.where(eq(imageDerivativeJobs.status, 'PENDING'))
.orderBy(asc(imageDerivativeJobs.createdAt))
.limit(1)
.for('update', { skipLocked: true });
if (!job) return null;
const [claimed] = await tx
.update(imageDerivativeJobs)
.set({ status: 'PROCESSING', updatedAt: new Date(), error: null })
.where(and(
eq(imageDerivativeJobs.id, job.id),
eq(imageDerivativeJobs.status, 'PENDING'),
))
.returning();
if (claimed) {
await tx
.update(inventoryImages)
.set({ processingStatus: 'PROCESSING', updatedAt: new Date() })
.where(eq(inventoryImages.id, claimed.inventoryImageId));
}
return claimed ?? null;
});
},
async complete(id, derivatives) {
const [completed] = await db
.update(imageDerivativeJobs)
.set({ status: 'COMPLETED', derivatives, updatedAt: new Date(), error: null })
.where(and(eq(imageDerivativeJobs.id, id), eq(imageDerivativeJobs.status, 'PROCESSING')))
.returning();
if (completed) {
await db
.update(inventoryImages)
.set({ processingStatus: 'COMPLETED', updatedAt: new Date() })
.where(eq(inventoryImages.id, completed.inventoryImageId));
}
return completed ?? null;
},
async fail(id, message) {
const [failed] = await db
.update(imageDerivativeJobs)
.set({ status: 'FAILED', error: message, updatedAt: new Date() })
.where(and(eq(imageDerivativeJobs.id, id), eq(imageDerivativeJobs.status, 'PROCESSING')))
.returning();
if (failed) {
await db
.update(inventoryImages)
.set({ processingStatus: 'FAILED', updatedAt: new Date() })
.where(eq(inventoryImages.id, failed.inventoryImageId));
}
return failed ?? null;
},
};
}
@@ -9,6 +9,8 @@ import {
conditionProfiles,
conditions,
inventoryItems,
inventoryImages,
imageDerivativeJobs,
listings,
mfaMethods,
passwordResetTokens,
@@ -25,6 +27,7 @@ import { createPasswordResetRepository } from '../../src/modules/auth/repositori
import { createMfaMethodRepository } from '../../src/modules/auth/repositories/mfa-method-repository.js';
import { createInventoryItemRepository } from '../../src/modules/inventory/repositories/inventory-item-repository.js';
import { createProductRepository } from '../../src/modules/products/repositories/product-repository.js';
import { createImageDerivativeJobRepository } from '../../src/modules/inventory/repositories/image-derivative-job-repository.js';
import { createSellerRepository } from '../../src/modules/sellers/repositories/seller-repository.js';
const connectionString = process.env.TEST_DATABASE_URL;
@@ -39,6 +42,7 @@ if (!new URL(connectionString).pathname.endsWith('_test')) {
const database = createDatabase(connectionString);
const productRepository = createProductRepository(database.db);
const imageDerivativeJobRepository = createImageDerivativeJobRepository(database.db);
const sellerRepository = createSellerRepository(database.db);
const inventoryItemRepository = createInventoryItemRepository(database.db);
const sessionRepository = createSessionRepository(database.db);
@@ -79,7 +83,7 @@ async function createCatalogPrerequisites() {
beforeEach(async () => {
await database.db.execute(
sql`TRUNCATE security_audit_events, password_reset_tokens, mfa_methods, sessions, inventory_items, products, conditions, condition_profiles, categories, sellers, users CASCADE`,
sql`TRUNCATE image_derivative_jobs, inventory_images, security_audit_events, password_reset_tokens, mfa_methods, sessions, inventory_items, products, conditions, condition_profiles, categories, sellers, users CASCADE`,
);
});
@@ -221,6 +225,44 @@ test('lists only sellers with publicly visible inventory', async () => {
assert.deepEqual(await sellerRepository.listPublic(), []);
});
test('claims and completes image derivative jobs while updating image processing status', async () => {
const { seller, category, condition } = await createCatalogPrerequisites();
const [product] = await database.db.insert(products).values({
sellerId: seller.id,
categoryId: category.id,
name: 'Derivative Job Product',
slug: 'derivative-job-product',
}).returning();
const [inventoryItem] = await database.db.insert(inventoryItems).values({
productId: product.id,
conditionId: condition.id,
}).returning();
const [image] = await database.db.insert(inventoryImages).values({
inventoryItemId: inventoryItem.id,
storageKey: 'inventory/derivative-source.jpg',
originalFilename: 'source.jpg',
mimeType: 'image/jpeg',
fileSize: 4,
width: 100,
height: 100,
}).returning();
const [job] = await database.db.insert(imageDerivativeJobs).values({
inventoryImageId: image.id,
sourceKey: image.storageKey,
}).returning();
const claimed = await imageDerivativeJobRepository.claimNext();
assert.equal(claimed.id, job.id);
assert.equal(claimed.status, 'PROCESSING');
const [processingImage] = await database.db.select().from(inventoryImages).where(sql`id = ${image.id}`);
assert.equal(processingImage.processingStatus, 'PROCESSING');
const completed = await imageDerivativeJobRepository.complete(job.id, [{ name: 'thumbnail', key: 'thumb.webp' }]);
assert.equal(completed.status, 'COMPLETED');
const [completedImage] = await database.db.select().from(inventoryImages).where(sql`id = ${image.id}`);
assert.equal(completedImage.processingStatus, 'COMPLETED');
});
test('retrieves only non-revoked, non-expired sessions', async () => {
const { user, seller } = await createCatalogPrerequisites();
const tokenHash = 'a hashed session token';