generated from fahricansecer/boilerplate-be
This commit is contained in:
225
src/modules/content/content.service.ts
Normal file
225
src/modules/content/content.service.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
// 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;
|
||||
},
|
||||
) {
|
||||
return this.prisma.content.findMany({
|
||||
where: {
|
||||
userId,
|
||||
...(options?.platform && { platform: options.platform }),
|
||||
...(options?.status && { status: options.status }),
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: options?.limit || 20,
|
||||
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 } },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 }) {
|
||||
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 content
|
||||
*/
|
||||
async delete(id: string) {
|
||||
return this.prisma.content.delete({
|
||||
where: { id },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user