This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { and, eq, inArray } from 'drizzle-orm';
|
import { and, eq, inArray } from 'drizzle-orm';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
imageDerivativeJobs,
|
||||||
inventoryImages,
|
inventoryImages,
|
||||||
inventoryItems,
|
inventoryItems,
|
||||||
listings,
|
listings,
|
||||||
@@ -42,16 +43,19 @@ export function createInventoryImageRepository(db) {
|
|||||||
sortOrder: inventoryImages.sortOrder,
|
sortOrder: inventoryImages.sortOrder,
|
||||||
isPrimary: inventoryImages.isPrimary,
|
isPrimary: inventoryImages.isPrimary,
|
||||||
processingStatus: inventoryImages.processingStatus,
|
processingStatus: inventoryImages.processingStatus,
|
||||||
|
derivatives: imageDerivativeJobs.derivatives,
|
||||||
})
|
})
|
||||||
.from(inventoryImages)
|
.from(inventoryImages)
|
||||||
.innerJoin(inventoryItems, eq(inventoryItems.id, inventoryImages.inventoryItemId))
|
.innerJoin(inventoryItems, eq(inventoryItems.id, inventoryImages.inventoryItemId))
|
||||||
.innerJoin(products, eq(products.id, inventoryItems.productId))
|
.innerJoin(products, eq(products.id, inventoryItems.productId))
|
||||||
.innerJoin(listings, eq(listings.inventoryItemId, inventoryItems.id))
|
.innerJoin(listings, eq(listings.inventoryItemId, inventoryItems.id))
|
||||||
|
.innerJoin(imageDerivativeJobs, eq(imageDerivativeJobs.inventoryImageId, inventoryImages.id))
|
||||||
.where(and(
|
.where(and(
|
||||||
eq(inventoryImages.inventoryItemId, inventoryItemId),
|
eq(inventoryImages.inventoryItemId, inventoryItemId),
|
||||||
eq(inventoryItems.status, 'ACTIVE'),
|
eq(inventoryItems.status, 'ACTIVE'),
|
||||||
inArray(listings.status, ['AVAILABLE', 'PENDING']),
|
inArray(listings.status, ['AVAILABLE', 'PENDING']),
|
||||||
eq(inventoryImages.processingStatus, 'COMPLETED'),
|
eq(inventoryImages.processingStatus, 'COMPLETED'),
|
||||||
|
eq(imageDerivativeJobs.status, 'COMPLETED'),
|
||||||
))
|
))
|
||||||
.orderBy(inventoryImages.sortOrder);
|
.orderBy(inventoryImages.sortOrder);
|
||||||
},
|
},
|
||||||
@@ -62,16 +66,19 @@ export function createInventoryImageRepository(db) {
|
|||||||
id: inventoryImages.id,
|
id: inventoryImages.id,
|
||||||
storageKey: inventoryImages.storageKey,
|
storageKey: inventoryImages.storageKey,
|
||||||
mimeType: inventoryImages.mimeType,
|
mimeType: inventoryImages.mimeType,
|
||||||
|
derivatives: imageDerivativeJobs.derivatives,
|
||||||
})
|
})
|
||||||
.from(inventoryImages)
|
.from(inventoryImages)
|
||||||
.innerJoin(inventoryItems, eq(inventoryItems.id, inventoryImages.inventoryItemId))
|
.innerJoin(inventoryItems, eq(inventoryItems.id, inventoryImages.inventoryItemId))
|
||||||
.innerJoin(products, eq(products.id, inventoryItems.productId))
|
.innerJoin(products, eq(products.id, inventoryItems.productId))
|
||||||
.innerJoin(listings, eq(listings.inventoryItemId, inventoryItems.id))
|
.innerJoin(listings, eq(listings.inventoryItemId, inventoryItems.id))
|
||||||
|
.innerJoin(imageDerivativeJobs, eq(imageDerivativeJobs.inventoryImageId, inventoryImages.id))
|
||||||
.where(and(
|
.where(and(
|
||||||
eq(inventoryImages.id, imageId),
|
eq(inventoryImages.id, imageId),
|
||||||
eq(inventoryItems.status, 'ACTIVE'),
|
eq(inventoryItems.status, 'ACTIVE'),
|
||||||
inArray(listings.status, ['AVAILABLE', 'PENDING']),
|
inArray(listings.status, ['AVAILABLE', 'PENDING']),
|
||||||
eq(inventoryImages.processingStatus, 'COMPLETED'),
|
eq(inventoryImages.processingStatus, 'COMPLETED'),
|
||||||
|
eq(imageDerivativeJobs.status, 'COMPLETED'),
|
||||||
));
|
));
|
||||||
|
|
||||||
return image ?? null;
|
return image ?? null;
|
||||||
|
|||||||
@@ -2,7 +2,12 @@ export async function registerPublicImageRoutes(app, options) {
|
|||||||
const { publicImageService } = options;
|
const { publicImageService } = options;
|
||||||
|
|
||||||
app.get('/public/images/:id', async (request, reply) => {
|
app.get('/public/images/:id', async (request, reply) => {
|
||||||
const image = await publicImageService.getContent(request.params.id);
|
const image = await publicImageService.getContent(request.params.id, 'original');
|
||||||
|
return reply.type(image.mimeType).send(image.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/public/images/:id/:variant', async (request, reply) => {
|
||||||
|
const image = await publicImageService.getContent(request.params.id, request.params.variant);
|
||||||
return reply.type(image.mimeType).send(image.data);
|
return reply.type(image.mimeType).send(image.data);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -6,18 +6,31 @@ export function createPublicImageService({ inventoryImageRepository, imageStorag
|
|||||||
const images = await inventoryImageRepository.listPublicByInventoryItemId(inventoryItemId);
|
const images = await inventoryImageRepository.listPublicByInventoryItemId(inventoryItemId);
|
||||||
return images.map((image) => ({
|
return images.map((image) => ({
|
||||||
...image,
|
...image,
|
||||||
url: `/api/v1/public/images/${image.id}`,
|
url: `/api/v1/public/images/${image.id}/large`,
|
||||||
|
variants: Object.fromEntries(
|
||||||
|
(Array.isArray(image.derivatives) ? image.derivatives : []).map(({ name }) => [
|
||||||
|
name,
|
||||||
|
`/api/v1/public/images/${image.id}/${name}`,
|
||||||
|
]),
|
||||||
|
),
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
async getContent(imageId) {
|
async getContent(imageId, variant = 'original') {
|
||||||
const image = await inventoryImageRepository.findPublicById(imageId);
|
const image = await inventoryImageRepository.findPublicById(imageId);
|
||||||
|
|
||||||
if (!image) {
|
if (!image) {
|
||||||
throw createApplicationError('IMAGE_NOT_FOUND', 'Image was not found.', 404);
|
throw createApplicationError('IMAGE_NOT_FOUND', 'Image was not found.', 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ...image, data: await imageStorage.get(image.storageKey) };
|
const derivative = (Array.isArray(image.derivatives) ? image.derivatives : [])
|
||||||
|
.find((candidate) => candidate.name === variant);
|
||||||
|
const storageKey = derivative?.key ?? image.storageKey;
|
||||||
|
return {
|
||||||
|
...image,
|
||||||
|
mimeType: derivative ? 'image/webp' : image.mimeType,
|
||||||
|
data: await imageStorage.get(storageKey),
|
||||||
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,8 @@ test('returns public image URLs without exposing storage keys', async () => {
|
|||||||
id: 'image-1',
|
id: 'image-1',
|
||||||
altText: 'Front of card',
|
altText: 'Front of card',
|
||||||
sortOrder: 0,
|
sortOrder: 0,
|
||||||
url: '/api/v1/public/images/image-1',
|
variants: {},
|
||||||
|
url: '/api/v1/public/images/image-1/large',
|
||||||
}]);
|
}]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ function markFailed(imageId) {
|
|||||||
<template>
|
<template>
|
||||||
<div class="image-gallery">
|
<div class="image-gallery">
|
||||||
<div class="image-gallery__stage">
|
<div class="image-gallery__stage">
|
||||||
<img v-if="props.images[selectedIndex]?.src && !failedImages.has(props.images[selectedIndex].id)" :src="props.images[selectedIndex].src" :alt="props.images[selectedIndex].alt" @error="markFailed(props.images[selectedIndex].id)" />
|
<img v-if="(props.images[selectedIndex]?.src ?? props.images[selectedIndex]?.url) && !failedImages.has(props.images[selectedIndex].id)" :src="props.images[selectedIndex].src ?? props.images[selectedIndex].url" :alt="props.images[selectedIndex].altText ?? props.images[selectedIndex].alt" @error="markFailed(props.images[selectedIndex].id)" />
|
||||||
<span v-else class="image-gallery__placeholder" role="img" :aria-label="props.images[selectedIndex]?.src ? 'Collectible image unavailable' : 'No collectible image available'">{{ props.images[selectedIndex]?.src ? 'IMAGE UNAVAILABLE' : (props.images[selectedIndex]?.label ?? 'IMAGE') }}</span>
|
<span v-else class="image-gallery__placeholder" role="img" :aria-label="props.images[selectedIndex]?.src ? 'Collectible image unavailable' : 'No collectible image available'">{{ props.images[selectedIndex]?.src ? 'IMAGE UNAVAILABLE' : (props.images[selectedIndex]?.label ?? 'IMAGE') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="props.images.length > 1" class="image-gallery__controls" aria-label="Gallery images">
|
<div v-if="props.images.length > 1" class="image-gallery__controls" aria-label="Gallery images">
|
||||||
|
|||||||
Reference in New Issue
Block a user