Files
Content-Hunter_BE/src/modules/content/content.service.ts
Harun CAN a229fc1e64
Some checks failed
Backend Deploy 🚀 / build-and-deploy (push) Has been cancelled
main
2026-03-28 17:16:56 +03:00

273 lines
8.9 KiB
TypeScript

// 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<string, { count: number; totalEngagement: number }>,
);
return {
totalPublished: contents.length,
byPlatform,
period,
};
}
}