feat: add asynchronous inventory exports
Quality / test (push) Canceled after 0s

This commit is contained in:
2026-09-10 01:25:11 -04:00
parent abb0b64217
commit 7c4885cf65
16 changed files with 269 additions and 1 deletions
+2
View File
@@ -53,6 +53,7 @@ import { createPricingProviderRegistry } from "../modules/pricing/providers/pric
import { createFixturePricingProvider } from "../modules/pricing/providers/fixture-pricing-provider.js";
import { createHttpPricingProvider } from "../modules/pricing/providers/http-pricing-provider.js";
import { createAdminExportService } from "../modules/exports/services/admin-export-service.js";
import { createInventoryExportJobRepository } from "../modules/exports/repositories/inventory-export-job-repository.js";
import { createSellerSettingsRepository } from "../modules/settings/repositories/seller-settings-repository.js";
import { createSellerSettingsService } from "../modules/settings/services/seller-settings-service.js";
@@ -166,6 +167,7 @@ async function startServer() {
}),
inventoryItemService,
adminExportService: createAdminExportService({
inventoryExportJobRepository: createInventoryExportJobRepository(database.db),
inventoryItemService,
}),
sellerSettingsService,
@@ -0,0 +1,51 @@
import { and, asc, eq } from 'drizzle-orm';
import { inventoryExportJobs } from '../../../infrastructure/database/schema.js';
export function createInventoryExportJobRepository(db) {
return {
async create(job) {
const [created] = await db.insert(inventoryExportJobs).values(job).returning();
return created;
},
async findByIdForSeller(id, sellerId) {
const [job] = await db.select().from(inventoryExportJobs).where(and(
eq(inventoryExportJobs.id, id),
eq(inventoryExportJobs.sellerId, sellerId),
));
return job ?? null;
},
async claimNext() {
return db.transaction(async (transaction) => {
const [job] = await transaction.select().from(inventoryExportJobs)
.where(eq(inventoryExportJobs.status, 'PENDING'))
.orderBy(asc(inventoryExportJobs.createdAt))
.limit(1).for('update', { skipLocked: true });
if (!job) return null;
const [claimed] = await transaction.update(inventoryExportJobs)
.set({ status: 'PROCESSING', updatedAt: new Date(), error: null })
.where(and(eq(inventoryExportJobs.id, job.id), eq(inventoryExportJobs.status, 'PENDING')))
.returning();
return claimed ?? null;
});
},
async complete(id, output) {
const [completed] = await db.update(inventoryExportJobs)
.set({ status: 'COMPLETED', output, updatedAt: new Date(), error: null })
.where(and(eq(inventoryExportJobs.id, id), eq(inventoryExportJobs.status, 'PROCESSING')))
.returning();
return completed ?? null;
},
async fail(id, error) {
const [failed] = await db.update(inventoryExportJobs)
.set({ status: 'FAILED', error, updatedAt: new Date() })
.where(and(eq(inventoryExportJobs.id, id), eq(inventoryExportJobs.status, 'PROCESSING')))
.returning();
return failed ?? null;
},
};
}
@@ -48,4 +48,34 @@ export async function registerAdminExportRoutes(app, options) {
.header('content-disposition', 'attachment; filename="inventory.xls"')
.send(data);
});
app.post('/exports/inventory.json/jobs', async (request, reply) => {
const job = await adminExportService.enqueueInventoryJson({
sellerId: request.admin.sellerId,
});
return reply.code(202).send({
data: { id: job.id, status: job.status, createdAt: job.createdAt },
});
});
app.get('/exports/jobs/:id', async (request, reply) => {
const job = await adminExportService.getExportJob({
sellerId: request.admin.sellerId,
jobId: request.params.id,
});
if (!job) {
throw createApplicationError('EXPORT_JOB_NOT_FOUND', 'Export job was not found.', 404);
}
return reply.send({
data: {
id: job.id,
format: job.format,
status: job.status,
error: job.error ?? null,
output: job.status === 'COMPLETED' ? job.output : null,
createdAt: job.createdAt,
updatedAt: job.updatedAt,
},
});
});
}
@@ -1,12 +1,20 @@
import { toAdminInventoryItemDto } from '@pokemon-inventory/contracts/inventory';
export function createAdminExportService({ inventoryItemService }) {
export function createAdminExportService({ inventoryItemService, inventoryExportJobRepository }) {
async function inventoryRows(sellerId) {
const inventoryItems = await inventoryItemService.listBySellerId(sellerId);
return inventoryItems.map(toAdminInventoryItemDto);
}
return {
async enqueueInventoryJson({ sellerId }) {
return inventoryExportJobRepository.create({ sellerId, format: 'JSON' });
},
async getExportJob({ sellerId, jobId }) {
return inventoryExportJobRepository.findByIdForSeller(jobId, sellerId);
},
async inventoryJson({ sellerId }) {
return inventoryRows(sellerId);
},