feat: enforce per-item image count limits
Quality / test (push) Canceled after 0s

This commit is contained in:
2026-09-11 00:39:27 -04:00
parent e6510c0317
commit f54223966c
7 changed files with 62 additions and 6 deletions
+1
View File
@@ -198,6 +198,7 @@ async function startServer() {
imageStorage: createLocalImageStorage(imageStorageRoot),
imageDerivativeJobRepository,
inventoryImageRepository,
sellerSettingsRepository,
}),
listingService: createListingService({
listingRepository,
@@ -1,5 +1,6 @@
import { moveImageInOrder } from '../domain/image-order.js';
import { and, eq, inArray } from 'drizzle-orm';
import { createApplicationError } from '../../../shared/errors/application-error.js';
import {
imageDerivativeJobs,
@@ -24,6 +25,24 @@ export function createInventoryImageRepository(db) {
return createdImage;
},
async createForSellerWithLimit(image, sellerId, maxImages) {
return db.transaction(async (tx) => {
const [ownedItem] = await tx.select({ id: inventoryItems.id })
.from(inventoryItems)
.innerJoin(products, eq(products.id, inventoryItems.productId))
.where(and(eq(inventoryItems.id, image.inventoryItemId), eq(products.sellerId, sellerId)))
.for('update', { of: inventoryItems });
if (!ownedItem) return null;
const existing = await tx.select({ id: inventoryImages.id }).from(inventoryImages)
.where(eq(inventoryImages.inventoryItemId, image.inventoryItemId));
if (existing.length >= maxImages) {
throw createApplicationError('IMAGE_COUNT_LIMIT_REACHED', 'Maximum images per item reached.', 409);
}
const [createdImage] = await tx.insert(inventoryImages).values(image).returning();
return createdImage;
});
},
async countByInventoryItemId(inventoryItemId) {
const rows = await db
.select({ id: inventoryImages.id })
@@ -2,13 +2,20 @@ import { validateImageMove } from '../domain/image-order.js';
import { createApplicationError } from '../../../shared/errors/application-error.js';
import { normalizeImageAltText, validateImageSortOrder, validateImageSignature, validateInventoryImageMetadata } from '../domain/inventory-image.js';
export function createInventoryImageService({ inventoryImageRepository, imageDerivativeJobRepository, imageStorage, imageInspector }) {
export function createInventoryImageService({ inventoryImageRepository, imageDerivativeJobRepository, imageStorage, imageInspector, sellerSettingsRepository }) {
return {
async create(input) {
validateInventoryImageMetadata(input);
const image = await inventoryImageRepository.createForSeller(
const settingsRecord = await sellerSettingsRepository?.findBySellerId(input.sellerId);
const maxImages = settingsRecord?.settings?.inventory?.maxImagesPerItem ?? 10;
if (!Number.isInteger(maxImages) || maxImages < 1 || maxImages > 100) {
throw createApplicationError('IMAGE_COUNT_LIMIT_INVALID', 'Image count limit is invalid.', 500);
}
const createImage = inventoryImageRepository.createForSellerWithLimit ?? inventoryImageRepository.createForSeller;
const image = await createImage.call(inventoryImageRepository,
{ ...input, processingStatus: 'PENDING' },
input.sellerId,
maxImages,
);
if (!image) {
@@ -99,4 +106,4 @@ async function requireOwnedImage(imagePromise) {
const image = await imagePromise;
if (!image) throw imageNotFoundError();
return image;
}
}