// Content Service - Main orchestrator for content operations // Path: src/modules/content/content.service.ts import { Injectable, Logger } from '@nestjs/common'; import { PrismaService } from '../../database/prisma.service'; import { MasterContentService } from './services/master-content.service'; import { BuildingBlocksService } from './services/building-blocks.service'; import { WritingStylesService } from './services/writing-styles.service'; import { ContentVariationsService } from './services/content-variations.service'; import { PlatformAdaptersService } from './services/platform-adapters.service'; import { SocialPlatform, ContentStatus } from '@prisma/client'; export interface CreateContentDto { masterContentId: string; platform: SocialPlatform; body?: string; scheduledAt?: Date; } @Injectable() export class ContentService { private readonly logger = new Logger(ContentService.name); constructor( private readonly prisma: PrismaService, private readonly masterContent: MasterContentService, private readonly buildingBlocks: BuildingBlocksService, private readonly writingStyles: WritingStylesService, private readonly variations: ContentVariationsService, private readonly platforms: PlatformAdaptersService, ) { } /** * Create platform-specific content from master content */ async createFromMaster(userId: string, dto: CreateContentDto) { const master = await this.masterContent.getById(dto.masterContentId); if (!master) { throw new Error('Master content not found'); } // Get platform config const platformConfig = this.platforms.getConfig(dto.platform); // Adapt content for platform let body = dto.body || master.body || ''; body = this.platforms.format(body, dto.platform); // Validate content const validation = this.platforms.validate(body, dto.platform); if (!validation.valid) { this.logger.warn(`Content validation issues: ${validation.issues.join(', ')}`); } // Create content record const content = await this.prisma.content.create({ data: { userId, masterContentId: dto.masterContentId, type: dto.platform as any, body, status: dto.scheduledAt ? ContentStatus.SCHEDULED : ContentStatus.DRAFT, scheduledAt: dto.scheduledAt, }, }); return { content, platformConfig, validation, }; } /** * Get user's content with filters */ async getByUser( userId?: string, options?: { platform?: SocialPlatform; status?: ContentStatus; limit?: number; offset?: number; sortBy?: string; sortOrder?: 'asc' | 'desc'; }, ) { // Build dynamic orderBy const sortField = options?.sortBy || 'createdAt'; const sortDir = options?.sortOrder || 'desc'; const validSortFields = ['createdAt', 'updatedAt', 'type', 'status', 'publishedAt']; const orderBy = validSortFields.includes(sortField) ? { [sortField]: sortDir } : { createdAt: 'desc' as const }; const where = { ...(userId && { userId }), ...(options?.platform && { type: options.platform as any }), ...(options?.status && { status: options.status }), }; const [items, total] = await Promise.all([ this.prisma.content.findMany({ where, orderBy, take: options?.limit || 100, skip: options?.offset || 0, include: { masterContent: { select: { id: true, title: true, type: true } }, variants: { select: { id: true, name: true, isWinner: true } }, _count: { select: { variants: true } }, }, }), this.prisma.content.count({ where }), ]); return { items, total }; } /** * Get content by ID */ async getById(id: string) { return this.prisma.content.findUnique({ where: { id }, include: { masterContent: true, variants: true, approvals: true, }, }); } /** * Update content */ async update(id: string, data: { body?: string; status?: ContentStatus; scheduledAt?: Date; imageUrl?: string }) { return this.prisma.content.update({ where: { id }, data, }); } /** * Publish content (mark as published) */ async publish(id: string, publishedUrl?: string) { return this.prisma.content.update({ where: { id }, data: { status: ContentStatus.PUBLISHED, publishedAt: new Date(), publishedUrl, }, }); } /** * Delete all related records for given content IDs, then delete the content. * Uses a transaction to ensure atomicity. */ private async cascadeDeleteContents(ids: string[]) { return this.prisma.$transaction(async (tx) => { // Delete all dependent relations first await tx.citation.deleteMany({ where: { contentId: { in: ids } } }); await tx.contentVariant.deleteMany({ where: { contentId: { in: ids } } }); await tx.media.deleteMany({ where: { contentId: { in: ids } } }); await tx.scheduledPost.deleteMany({ where: { contentId: { in: ids } } }); await tx.contentApproval.deleteMany({ where: { contentId: { in: ids } } }); await tx.contentAnalytics.deleteMany({ where: { contentId: { in: ids } } }); await tx.contentSeo.deleteMany({ where: { contentId: { in: ids } } }); await tx.contentPsychology.deleteMany({ where: { contentId: { in: ids } } }); await tx.contentSource.deleteMany({ where: { contentId: { in: ids } } }); // Now delete the content itself const result = await tx.content.deleteMany({ where: { id: { in: ids } } }); return result.count; }); } /** * Delete content (with cascade) */ async delete(id: string) { const deleted = await this.cascadeDeleteContents([id]); return { deleted }; } /** * Bulk delete content items (with cascade) */ async bulkDelete(ids: string[]) { const deleted = await this.cascadeDeleteContents(ids); return { deleted }; } /** * Get content calendar for user */ async getCalendar(userId: string, startDate: Date, endDate: Date) { return this.prisma.content.findMany({ where: { userId, OR: [ { scheduledAt: { gte: startDate, lte: endDate } }, { publishedAt: { gte: startDate, lte: endDate } }, ], }, orderBy: [{ scheduledAt: 'asc' }, { publishedAt: 'asc' }], select: { id: true, type: true, status: true, scheduledAt: true, publishedAt: true, masterContent: { select: { title: true } }, }, }); } /** * Get content analytics */ async getAnalytics(userId: string, period: 'week' | 'month' | 'year') { const now = new Date(); let startDate: Date; switch (period) { case 'week': startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); break; case 'month': startDate = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); break; case 'year': startDate = new Date(now.getTime() - 365 * 24 * 60 * 60 * 1000); break; } const contents = await this.prisma.content.findMany({ where: { userId, publishedAt: { gte: startDate }, }, include: { variants: true, }, }); // Aggregate by platform const byPlatform = contents.reduce( (acc, content) => { if (!acc[content.type]) { acc[content.type] = { count: 0, totalEngagement: 0 }; } acc[content.type].count++; acc[content.type].totalEngagement += content.variants.reduce( (sum, v) => sum + (v.engagements || 0), 0, ); return acc; }, {} as Record, ); return { totalPublished: contents.length, byPlatform, period, }; } }