Files
backend/src/website/website-sliders.service.ts
T
Ali Reza bb59d5e9ba Initial commit: Meshkee CMS API
NestJS backend with Prisma, Docker Compose for Postgres/Redis, and deploy docs for the production VM.
2026-07-21 17:52:36 +03:30

304 lines
8.2 KiB
TypeScript

import {
BadRequestException,
ForbiddenException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { AuthUser } from '../auth/auth.types';
import { PermissionsService } from '../auth/permissions.service';
import { PrismaService } from '../prisma/prisma.service';
import { TenantService } from '../tenant/tenant.service';
import {
CreateWebsiteSliderDto,
ListWebsiteSlidersDto,
UpdateWebsiteSliderDto,
WebsiteSliderSlideInputDto,
} from './dto/website-sliders.dto';
const sliderInclude = {
website_slider_slides: {
orderBy: [{ sort_order: 'asc' as const }, { id: 'asc' as const }],
include: {
media: true,
},
},
} satisfies Prisma.website_slidersInclude;
type SliderWithSlides = Prisma.website_slidersGetPayload<{
include: typeof sliderInclude;
}>;
@Injectable()
export class WebsiteSlidersService {
constructor(
private readonly prisma: PrismaService,
private readonly permissions: PermissionsService,
private readonly tenant: TenantService,
) {}
async listPublic(host: string) {
const business = await this.tenant.resolveBusinessByDomain(host);
const sliders = await this.prisma.website_sliders.findMany({
where: { business_id: business.id, is_active: true },
orderBy: [{ sort_order: 'asc' }, { created_at: 'asc' }],
include: sliderInclude,
});
return {
items: sliders.map((slider) => this.serializeSlider(slider, true)),
};
}
async list(
businessIdRaw: string,
query: ListWebsiteSlidersDto,
actor: AuthUser,
) {
const businessId = BigInt(businessIdRaw);
await this.assertPermission(businessId, actor.id, 'website.read');
const page = query.page ?? 1;
const pageSize = query.pageSize ?? 20;
const skip = (page - 1) * pageSize;
const where: Prisma.website_slidersWhereInput = {
business_id: businessId,
...(query.isActive !== undefined ? { is_active: query.isActive } : {}),
};
const [items, total] = await Promise.all([
this.prisma.website_sliders.findMany({
where,
orderBy: [{ sort_order: 'asc' }, { created_at: 'asc' }],
skip,
take: pageSize,
include: sliderInclude,
}),
this.prisma.website_sliders.count({ where }),
]);
return {
items: items.map((slider) => this.serializeSlider(slider, false)),
total,
page,
pageSize,
};
}
async getOne(
businessIdRaw: string,
sliderIdRaw: string,
actor: AuthUser,
) {
const businessId = BigInt(businessIdRaw);
const sliderId = BigInt(sliderIdRaw);
await this.assertPermission(businessId, actor.id, 'website.read');
const slider = await this.findSliderOrThrow(businessId, sliderId);
return { slider: this.serializeSlider(slider, false) };
}
async create(
businessIdRaw: string,
dto: CreateWebsiteSliderDto,
actor: AuthUser,
) {
const businessId = BigInt(businessIdRaw);
await this.assertPermission(businessId, actor.id, 'website.update');
const slides = dto.slides ?? [];
await this.assertSlideMedia(businessId, slides);
const created = await this.prisma.$transaction(async (tx) => {
const slider = await tx.website_sliders.create({
data: {
business_id: businessId,
title: dto.title.trim(),
sort_order: dto.sortOrder ?? 0,
is_active: dto.isActive ?? true,
},
});
await this.replaceSlides(tx, slider.id, slides);
return tx.website_sliders.findUniqueOrThrow({
where: { id: slider.id },
include: sliderInclude,
});
});
return {
message: 'Website slider created successfully',
slider: this.serializeSlider(created, false),
};
}
async update(
businessIdRaw: string,
sliderIdRaw: string,
dto: UpdateWebsiteSliderDto,
actor: AuthUser,
) {
const businessId = BigInt(businessIdRaw);
const sliderId = BigInt(sliderIdRaw);
await this.assertPermission(businessId, actor.id, 'website.update');
await this.findSliderOrThrow(businessId, sliderId);
let slides: WebsiteSliderSlideInputDto[] | undefined;
if (dto.slides !== undefined) {
slides = dto.slides;
await this.assertSlideMedia(businessId, slides);
}
const updated = await this.prisma.$transaction(async (tx) => {
await tx.website_sliders.update({
where: { id: sliderId },
data: {
...(dto.title !== undefined ? { title: dto.title.trim() } : {}),
...(dto.sortOrder !== undefined ? { sort_order: dto.sortOrder } : {}),
...(dto.isActive !== undefined ? { is_active: dto.isActive } : {}),
},
});
if (slides !== undefined) {
await this.replaceSlides(tx, sliderId, slides);
}
return tx.website_sliders.findUniqueOrThrow({
where: { id: sliderId },
include: sliderInclude,
});
});
return {
message: 'Website slider updated successfully',
slider: this.serializeSlider(updated, false),
};
}
async remove(
businessIdRaw: string,
sliderIdRaw: string,
actor: AuthUser,
) {
const businessId = BigInt(businessIdRaw);
const sliderId = BigInt(sliderIdRaw);
await this.assertPermission(businessId, actor.id, 'website.update');
await this.findSliderOrThrow(businessId, sliderId);
await this.prisma.website_sliders.delete({ where: { id: sliderId } });
return { message: 'Website slider deleted successfully' };
}
private async findSliderOrThrow(businessId: bigint, sliderId: bigint) {
const slider = await this.prisma.website_sliders.findFirst({
where: { id: sliderId, business_id: businessId },
include: sliderInclude,
});
if (!slider) {
throw new NotFoundException('Website slider not found');
}
return slider;
}
private async assertSlideMedia(
businessId: bigint,
slides: WebsiteSliderSlideInputDto[],
) {
if (slides.length === 0) {
return;
}
const mediaIds = slides.map((slide) => BigInt(slide.imageMediaId));
const uniqueMediaIds = [...new Set(mediaIds.map((id) => id.toString()))].map(
(id) => BigInt(id),
);
const found = await this.prisma.media.findMany({
where: {
businessId,
id: { in: uniqueMediaIds },
mimeType: { startsWith: 'image/' },
},
select: { id: true },
});
if (found.length !== uniqueMediaIds.length) {
throw new BadRequestException(
'One or more slide images were not found for this business',
);
}
}
private async replaceSlides(
tx: Prisma.TransactionClient,
sliderId: bigint,
slides: WebsiteSliderSlideInputDto[],
) {
await tx.website_slider_slides.deleteMany({ where: { slider_id: sliderId } });
if (slides.length === 0) {
return;
}
await tx.website_slider_slides.createMany({
data: slides.map((slide, index) => ({
slider_id: sliderId,
image_media_id: BigInt(slide.imageMediaId),
title: slide.title?.trim() || null,
link_url: slide.linkUrl?.trim() || null,
sort_order: index,
is_active: slide.isActive ?? true,
})),
});
}
private serializeSlider(slider: SliderWithSlides, publicView: boolean) {
const slides = slider.website_slider_slides
.filter((slide) => !publicView || slide.is_active)
.map((slide) => ({
id: slide.id.toString(),
imageMediaId: slide.image_media_id.toString(),
imageUrl: slide.media.publicUrl,
title: slide.title,
linkUrl: slide.link_url,
sortOrder: slide.sort_order,
isActive: slide.is_active,
createdAt: slide.created_at,
updatedAt: slide.updated_at,
}));
return {
id: slider.id.toString(),
title: slider.title,
sortOrder: slider.sort_order,
isActive: slider.is_active,
createdAt: slider.created_at,
updatedAt: slider.updated_at,
slides,
};
}
private async assertPermission(
businessId: bigint,
userId: bigint,
permission: string,
) {
const allowed = await this.permissions.hasBusinessPermission(
userId,
businessId,
permission,
);
if (!allowed) {
throw new ForbiddenException(
`Missing permission: ${permission} for this business`,
);
}
}
}