Sync static image slots from the live website catalog.

Refresh now imports keys from the storefront and deletes unused slots plus their files.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-17 09:55:03 +03:30
co-authored by Cursor
parent 1c7678ce67
commit 5e5bfc4aa3
8 changed files with 474 additions and 2 deletions
+120
View File
@@ -184,6 +184,126 @@ export class MediaService {
return { message: 'Media deleted' };
}
/** Delete media that is no longer referenced by any business record. */
async deleteUnused(businessId: bigint, mediaIds: bigint[]): Promise<number> {
if (!mediaIds.length) return 0;
const uniqueIds = [...new Set(mediaIds.map((id) => id.toString()))].map(
(id) => BigInt(id),
);
const stillLinked = new Set<string>();
const addId = (id: bigint | null | undefined) => {
if (id != null) stillLinked.add(id.toString());
};
const attachments = await this.prisma.mediaAttachment.findMany({
where: { businessId, mediaId: { in: uniqueIds } },
select: { mediaId: true },
});
for (const row of attachments) addId(row.mediaId);
const [
portfolios,
products,
blogs,
videos,
instructions,
brands,
userProducts,
sliders,
staticItems,
business,
] = await Promise.all([
this.prisma.portfolios.findMany({
where: { business_id: businessId, featured_media_id: { in: uniqueIds } },
select: { featured_media_id: true },
}),
this.prisma.product.findMany({
where: { businessId, featuredMediaId: { in: uniqueIds } },
select: { featuredMediaId: true },
}),
this.prisma.blogs.findMany({
where: { business_id: businessId, featured_media_id: { in: uniqueIds } },
select: { featured_media_id: true },
}),
this.prisma.videos.findMany({
where: { business_id: businessId, featured_media_id: { in: uniqueIds } },
select: { featured_media_id: true },
}),
this.prisma.instructions.findMany({
where: { business_id: businessId, featured_media_id: { in: uniqueIds } },
select: { featured_media_id: true },
}),
this.prisma.brand.findMany({
where: { businessId, imageMediaId: { in: uniqueIds } },
select: { imageMediaId: true },
}),
this.prisma.userProduct.findMany({
where: { businessId, featuredMediaId: { in: uniqueIds } },
select: { featuredMediaId: true },
}),
this.prisma.website_slider_slides.findMany({
where: {
image_media_id: { in: uniqueIds },
website_sliders: { business_id: businessId },
},
select: { image_media_id: true },
}),
this.prisma.website_image_slot_items.findMany({
where: {
image_media_id: { in: uniqueIds },
website_image_slots: { business_id: businessId },
},
select: { image_media_id: true },
}),
this.prisma.business.findFirst({
where: {
id: businessId,
OR: [
{ logoMediaId: { in: uniqueIds } },
{ faviconMediaId: { in: uniqueIds } },
],
},
select: { logoMediaId: true, faviconMediaId: true },
}),
]);
for (const row of portfolios) addId(row.featured_media_id);
for (const row of products) addId(row.featuredMediaId);
for (const row of blogs) addId(row.featured_media_id);
for (const row of videos) addId(row.featured_media_id);
for (const row of instructions) addId(row.featured_media_id);
for (const row of brands) addId(row.imageMediaId);
for (const row of userProducts) addId(row.featuredMediaId);
for (const row of sliders) addId(row.image_media_id);
for (const row of staticItems) addId(row.image_media_id);
addId(business?.logoMediaId);
addId(business?.faviconMediaId);
const deletableIds = uniqueIds.filter((id) => !stillLinked.has(id.toString()));
if (!deletableIds.length) return 0;
const mediaRows = await this.prisma.media.findMany({
where: { businessId, id: { in: deletableIds } },
select: { id: true, storagePath: true, storageDisk: true },
});
await this.prisma.media.deleteMany({
where: { businessId, id: { in: mediaRows.map((row) => row.id) } },
});
for (const row of mediaRows) {
try {
await this.storage.delete(row.storagePath, row.storageDisk);
} catch {
// DB row removed; orphaned object can be cleaned later
}
}
return mediaRows.length;
}
private async uploadOne(
businessId: bigint,
file: Express.Multer.File,