This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
import { createApplicationError } from '../../../shared/errors/application-error.js';
|
||||
|
||||
export function validateImageMove(direction) {
|
||||
if (direction !== 'up' && direction !== 'down') {
|
||||
throw createApplicationError('IMAGE_MOVE_INVALID', 'Image move must be up or down.', 400);
|
||||
}
|
||||
}
|
||||
|
||||
// Input is the persisted display order. Return a reordered copy; boundary moves
|
||||
// are no-ops. Persistence assigns contiguous positions after an actual move.
|
||||
export function moveImageInOrder(images, imageId, direction) {
|
||||
validateImageMove(direction);
|
||||
const index = images.findIndex((image) => image.id === imageId);
|
||||
const neighbor = index + (direction === 'up' ? -1 : 1);
|
||||
if (index < 0 || neighbor < 0 || neighbor >= images.length) return images;
|
||||
const reordered = [...images];
|
||||
[reordered[index], reordered[neighbor]] = [reordered[neighbor], reordered[index]];
|
||||
return reordered;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { moveImageInOrder } from '../domain/image-order.js';
|
||||
import { and, eq, inArray } from 'drizzle-orm';
|
||||
|
||||
import {
|
||||
@@ -53,10 +54,42 @@ export function createInventoryImageRepository(db) {
|
||||
eq(inventoryImages.inventoryItemId, inventoryItemId),
|
||||
eq(products.sellerId, sellerId),
|
||||
))
|
||||
.orderBy(inventoryImages.sortOrder)
|
||||
.orderBy(inventoryImages.sortOrder, inventoryImages.createdAt, inventoryImages.id)
|
||||
.then((rows) => rows.map(({ image }) => image));
|
||||
},
|
||||
|
||||
async moveForSeller(imageId, direction, sellerId) {
|
||||
return db.transaction(async (tx) => {
|
||||
// Lock the owning physical item first, serializing moves within a gallery.
|
||||
const [owned] = await tx
|
||||
.select({ inventoryItemId: inventoryItems.id })
|
||||
.from(inventoryImages)
|
||||
.innerJoin(inventoryItems, eq(inventoryItems.id, inventoryImages.inventoryItemId))
|
||||
.innerJoin(products, eq(products.id, inventoryItems.productId))
|
||||
.where(and(eq(inventoryImages.id, imageId), eq(products.sellerId, sellerId)))
|
||||
.for('update', { of: inventoryItems });
|
||||
if (!owned) return null;
|
||||
|
||||
const images = await tx.select().from(inventoryImages)
|
||||
.where(eq(inventoryImages.inventoryItemId, owned.inventoryItemId))
|
||||
.orderBy(inventoryImages.sortOrder, inventoryImages.createdAt, inventoryImages.id)
|
||||
.for('update');
|
||||
const target = images.find((image) => image.id === imageId);
|
||||
if (!target) return null;
|
||||
const reordered = moveImageInOrder(images, imageId, direction);
|
||||
if (reordered === images) return target;
|
||||
const updatedAt = new Date();
|
||||
for (const [sortOrder, image] of reordered.entries()) {
|
||||
if (image.sortOrder !== sortOrder) {
|
||||
await tx.update(inventoryImages).set({ sortOrder, updatedAt })
|
||||
.where(eq(inventoryImages.id, image.id));
|
||||
}
|
||||
}
|
||||
const [moved] = await tx.select().from(inventoryImages).where(eq(inventoryImages.id, imageId));
|
||||
return moved;
|
||||
});
|
||||
},
|
||||
|
||||
async updateOrderForSeller(imageId, sortOrder, sellerId) {
|
||||
const owned = await this.findByIdForSeller(imageId, sellerId);
|
||||
if (!owned) return null;
|
||||
@@ -135,7 +168,7 @@ export function createInventoryImageRepository(db) {
|
||||
eq(inventoryImages.processingStatus, 'COMPLETED'),
|
||||
eq(imageDerivativeJobs.status, 'COMPLETED'),
|
||||
))
|
||||
.orderBy(inventoryImages.sortOrder);
|
||||
.orderBy(inventoryImages.sortOrder, inventoryImages.createdAt, inventoryImages.id);
|
||||
},
|
||||
|
||||
async findPublicById(imageId) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { toAdminInventoryImageDto } from '@pokemon-inventory/contracts/image';
|
||||
import { createInventoryImageSchema } from '@pokemon-inventory/validation/image';
|
||||
import { createInventoryImageSchema, moveInventoryImageSchema } from '@pokemon-inventory/validation/image';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { createApplicationError } from '../../../shared/errors/application-error.js';
|
||||
@@ -98,6 +98,18 @@ export async function registerAdminImageRoutes(app, options) {
|
||||
return admin;
|
||||
}
|
||||
|
||||
app.post('/images/:id/move', async (request, reply) => {
|
||||
const admin = await authorizeAdminMutation(request);
|
||||
const result = moveInventoryImageSchema.safeParse(request.body);
|
||||
if (!result.success) return validationError(reply, result);
|
||||
const image = await inventoryImageService.move({
|
||||
imageId: request.params.id,
|
||||
direction: result.data.direction,
|
||||
sellerId: admin.sellerId,
|
||||
});
|
||||
return { data: toAdminInventoryImageDto(image) };
|
||||
});
|
||||
|
||||
app.patch('/images/:id/order', async (request) => {
|
||||
const admin = await authorizeAdminMutation(request);
|
||||
const image = await inventoryImageService.updateOrder({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { validateImageMove } from '../domain/image-order.js';
|
||||
import { createApplicationError } from '../../../shared/errors/application-error.js';
|
||||
import { validateImageSignature, validateInventoryImageMetadata } from '../domain/inventory-image.js';
|
||||
|
||||
@@ -42,6 +43,11 @@ export function createInventoryImageService({ inventoryImageRepository, imageDer
|
||||
}
|
||||
},
|
||||
|
||||
async move({ imageId, direction, sellerId }) {
|
||||
validateImageMove(direction);
|
||||
return requireOwnedImage(inventoryImageRepository.moveForSeller(imageId, direction, sellerId));
|
||||
},
|
||||
|
||||
async updateOrder({ imageId, sortOrder, sellerId }) {
|
||||
return requireOwnedImage(inventoryImageRepository.updateOrderForSeller(imageId, sortOrder, sellerId));
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user