platform-marketing-content-.../src/modules/projects/services/content-piece.service.ts
2026-01-04 07:19:31 -06:00

189 lines
5.7 KiB
TypeScript

import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, IsNull } from 'typeorm';
import { ContentPiece, ContentStatus, ContentType } from '../entities/content-piece.entity';
import { CreateContentPieceDto, UpdateContentPieceDto } from '../dto';
import { TenantAwareService } from '@/shared/services/tenant-aware.service';
import { PaginationParams, PaginatedResult } from '@/shared/dto/pagination.dto';
@Injectable()
export class ContentPieceService extends TenantAwareService<ContentPiece> {
constructor(
@InjectRepository(ContentPiece)
private readonly contentPieceRepository: Repository<ContentPiece>,
) {
super(contentPieceRepository);
}
async findAllPaginated(
tenantId: string,
pagination: PaginationParams & {
projectId?: string;
type?: ContentType;
status?: ContentStatus;
search?: string;
},
): Promise<PaginatedResult<ContentPiece>> {
const {
page = 1,
limit = 20,
sortBy = 'created_at',
sortOrder = 'DESC',
projectId,
type,
status,
search,
} = pagination;
const queryBuilder = this.contentPieceRepository
.createQueryBuilder('content')
.leftJoinAndSelect('content.project', 'project')
.leftJoinAndSelect('content.assignee', 'assignee')
.where('content.tenant_id = :tenantId', { tenantId })
.andWhere('content.deleted_at IS NULL');
if (projectId) {
queryBuilder.andWhere('content.project_id = :projectId', { projectId });
}
if (type) {
queryBuilder.andWhere('content.type = :type', { type });
}
if (status) {
queryBuilder.andWhere('content.status = :status', { status });
}
if (search) {
queryBuilder.andWhere(
'(content.title ILIKE :search OR content.content ILIKE :search)',
{ search: `%${search}%` },
);
}
queryBuilder.orderBy(`content.${sortBy}`, sortOrder);
const total = await queryBuilder.getCount();
const totalPages = Math.ceil(total / limit);
queryBuilder.skip((page - 1) * limit).take(limit);
const data = await queryBuilder.getMany();
return {
data,
meta: {
total,
page,
limit,
totalPages,
hasNextPage: page < totalPages,
hasPreviousPage: page > 1,
},
};
}
async findById(tenantId: string, id: string): Promise<ContentPiece> {
const contentPiece = await this.contentPieceRepository.findOne({
where: { id, tenant_id: tenantId, deleted_at: IsNull() },
relations: ['project', 'assignee'],
});
if (!contentPiece) {
throw new NotFoundException(`Content piece with ID ${id} not found`);
}
return contentPiece;
}
async findByProject(tenantId: string, projectId: string): Promise<ContentPiece[]> {
return this.contentPieceRepository.find({
where: { tenant_id: tenantId, project_id: projectId, deleted_at: IsNull() },
relations: ['assignee'],
order: { created_at: 'DESC' },
});
}
async create(tenantId: string, dto: CreateContentPieceDto): Promise<ContentPiece> {
const contentPiece = this.contentPieceRepository.create({
...dto,
tenant_id: tenantId,
version: 1,
});
return this.contentPieceRepository.save(contentPiece);
}
async update(tenantId: string, id: string, dto: UpdateContentPieceDto): Promise<ContentPiece> {
const contentPiece = await this.findById(tenantId, id);
// Increment version on content changes
if (dto.content && dto.content !== contentPiece.content) {
contentPiece.version += 1;
}
Object.assign(contentPiece, dto);
return this.contentPieceRepository.save(contentPiece);
}
async updateStatus(tenantId: string, id: string, status: ContentStatus): Promise<ContentPiece> {
const contentPiece = await this.findById(tenantId, id);
contentPiece.status = status;
if (status === ContentStatus.PUBLISHED) {
contentPiece.published_at = new Date();
}
return this.contentPieceRepository.save(contentPiece);
}
async assignTo(tenantId: string, id: string, userId: string | null): Promise<ContentPiece> {
const contentPiece = await this.findById(tenantId, id);
contentPiece.assigned_to = userId;
return this.contentPieceRepository.save(contentPiece);
}
async schedule(tenantId: string, id: string, scheduledAt: Date): Promise<ContentPiece> {
const contentPiece = await this.findById(tenantId, id);
contentPiece.scheduled_at = scheduledAt;
contentPiece.status = ContentStatus.SCHEDULED;
return this.contentPieceRepository.save(contentPiece);
}
async publish(tenantId: string, id: string, publishedUrl?: string): Promise<ContentPiece> {
const contentPiece = await this.findById(tenantId, id);
contentPiece.status = ContentStatus.PUBLISHED;
contentPiece.published_at = new Date();
if (publishedUrl) {
contentPiece.published_url = publishedUrl;
}
return this.contentPieceRepository.save(contentPiece);
}
async softDelete(tenantId: string, id: string): Promise<void> {
const contentPiece = await this.findById(tenantId, id);
contentPiece.deleted_at = new Date();
await this.contentPieceRepository.save(contentPiece);
}
async duplicate(tenantId: string, id: string): Promise<ContentPiece> {
const original = await this.findById(tenantId, id);
const duplicate = this.contentPieceRepository.create({
...original,
id: undefined,
title: `${original.title} (Copy)`,
status: ContentStatus.IDEA,
published_at: null,
published_url: null,
scheduled_at: null,
version: 1,
created_at: undefined,
updated_at: undefined,
});
return this.contentPieceRepository.save(duplicate);
}
}