import { BaseRepository } from './baseRepository'; import { prisma } from '../client'; import { Content, ContentTag, ContentSpace } from '@prisma/client'; /** * Repository for Content entities * Handles all database operations related to content */ export class ContentRepository extends BaseRepository { /** * The Prisma model to operate on */ protected model = prisma.content; /** * Find content by ID with related tags and spaces * @param id The content ID * @returns The content with its tags and spaces or null if not found */ async findByIdWithRelations(id: string): Promise { try { return await this.model.findUnique({ where: { id }, include: { contentTags: { include: { tag: true } }, spaces: { include: { space: true } } } }); } catch (error) { console.error(`Error in findByIdWithRelations:`, error); throw error; } } /** * Find content by URL * @param url The URL to search for * @returns The content with that URL or null if not found */ async findByUrl(url: string): Promise { try { return await this.model.findFirst({ where: { url }, include: { contentTags: { include: { tag: true } }, spaces: { include: { space: true } } } }); } catch (error) { console.error(`Error in findByUrl:`, error); throw error; } } /** * Get all content with optional pagination * @param skip Number of items to skip for pagination * @param take Maximum number of items to return * @param includeTags Whether to include tag relations * @param includeSpaces Whether to include space relations * @returns Array of content items and total count */ async getAllPaginated( skip: number = 0, take: number = 20, includeTags: boolean = true, includeSpaces: boolean = true ): Promise<{ contents: any[]; total: number }> { try { // Build include object based on parameters const include: any = {}; if (includeTags) { include.contentTags = { include: { tag: true } }; } if (includeSpaces) { include.spaces = { include: { space: true } }; } // Execute queries in parallel for better performance const [contents, total] = await Promise.all([ this.model.findMany({ skip, take, include, orderBy: { createdAt: 'desc' } }), this.model.count() ]); return { contents, total }; } catch (error) { console.error(`Error in getAllPaginated:`, error); throw error; } } /** * Search for content by title or URL * @param query The search query string * @param skip Number of items to skip for pagination * @param take Maximum number of items to return * @returns Array of matching content items and total count */ async search( query: string, skip: number = 0, take: number = 20 ): Promise<{ contents: any[]; total: number }> { try { // Prepare search conditions for title or URL const searchCondition = { OR: [ { title: { contains: query, mode: 'insensitive' } }, { url: { contains: query, mode: 'insensitive' } } ] }; // Execute queries in parallel for better performance const [contents, total] = await Promise.all([ this.model.findMany({ where: searchCondition, skip, take, include: { contentTags: { include: { tag: true } }, spaces: { include: { space: true } } }, orderBy: { createdAt: 'desc' } }), this.model.count({ where: searchCondition }) ]); return { contents, total }; } catch (error) { console.error(`Error in search:`, error); throw error; } } /** * Add a tag to content * @param contentId The content ID * @param tagId The tag ID * @returns The created ContentTag relation */ async addTag(contentId: string, tagId: string): Promise { try { return await prisma.contentTag.create({ data: { contentId, tagId } }); } catch (error: any) { // Ignore duplicate tag errors if (error.code === 'P2002') { console.log(`Tag ${tagId} already exists on content ${contentId}`); return null; } console.error(`Error in addTag:`, error); throw error; } } /** * Remove a tag from content * @param contentId The content ID * @param tagId The tag ID * @returns The deleted ContentTag relation */ async removeTag(contentId: string, tagId: string): Promise { try { return await prisma.contentTag.delete({ where: { contentId_tagId: { contentId, tagId } } }); } catch (error) { console.error(`Error in removeTag:`, error); throw error; } } /** * Add content to a space * @param contentId The content ID * @param spaceId The space ID * @returns The created ContentSpace relation */ async addToSpace(contentId: string, spaceId: string): Promise { try { return await prisma.contentSpace.create({ data: { contentId, spaceId } }); } catch (error: any) { // Ignore duplicate space errors if (error.code === 'P2002') { console.log(`Content ${contentId} already exists in space ${spaceId}`); return null; } console.error(`Error in addToSpace:`, error); throw error; } } /** * Remove content from a space * @param contentId The content ID * @param spaceId The space ID * @returns The deleted ContentSpace relation */ async removeFromSpace(contentId: string, spaceId: string): Promise { try { return await prisma.contentSpace.delete({ where: { contentId_spaceId: { contentId, spaceId } } }); } catch (error) { console.error(`Error in removeFromSpace:`, error); throw error; } } /** * Find content by space ID * @param spaceId The space ID to search for * @returns Array of content items in the specified space */ async findBySpace(spaceId: string): Promise { try { const contentSpaces = await prisma.contentSpace.findMany({ where: { spaceId }, include: { content: { include: { contentTags: { include: { tag: true } }, spaces: { include: { space: true } } } } } }); // Extract and return the content objects from the relations return contentSpaces.map((cs: any) => cs.content); } catch (error) { console.error(`Error in findBySpace:`, error); throw error; } } /** * Find content by multiple IDs * @param ids Array of content IDs to find * @returns Array of content items with the specified IDs */ async findByIds(ids: string[]): Promise { try { if (!ids || ids.length === 0) { return []; } return await this.model.findMany({ where: { id: { in: ids } }, include: { contentTags: { include: { tag: true } }, spaces: { include: { space: true } } } }); } catch (error) { console.error(`Error in findByIds:`, error); throw error; } } /** * Find content similar to the given content ID based on shared tags * @param contentId The content ID to find similar items for * @param limit Maximum number of similar items to return * @returns Array of similar content items */ async findSimilar(contentId: string, limit: number = 5): Promise { try { // First get the current content to extract its tags const content = await this.findByIdWithRelations(contentId); if (!content || !content.contentTags || content.contentTags.length === 0) { return []; } // Extract tag IDs const tagIds = content.contentTags.map((ct: any) => ct.tagId); // Find content with similar tags, excluding the original content const similarContent = await this.model.findMany({ where: { id: { not: contentId }, contentTags: { some: { tagId: { in: tagIds } } } }, include: { contentTags: { include: { tag: true } }, spaces: { include: { space: true } } }, take: limit, orderBy: { createdAt: 'desc' } }); return similarContent; } catch (error) { console.error(`Error in findSimilar:`, error); throw error; } } /** * Add multiple tags to content * @param contentId The content ID * @param tagIds Array of tag IDs to add * @returns Array of created ContentTag relations */ async addTags(contentId: string, tagIds: string[]): Promise { try { if (!tagIds || tagIds.length === 0) { return []; } const results = []; for (const tagId of tagIds) { try { const result = await this.addTag(contentId, tagId); if (result) { results.push(result); } } catch (error) { console.error(`Error adding tag ${tagId} to content ${contentId}:`, error); // Continue with other tags even if one fails } } return results; } catch (error) { console.error(`Error in addTags:`, error); throw error; } } /** * Remove multiple tags from content * @param contentId The content ID * @param tagIds Array of tag IDs to remove * @returns Array of deleted ContentTag relations */ async removeTags(contentId: string, tagIds: string[]): Promise { try { if (!tagIds || tagIds.length === 0) { return []; } const results = []; for (const tagId of tagIds) { try { const result = await this.removeTag(contentId, tagId); results.push(result); } catch (error) { console.error(`Error removing tag ${tagId} from content ${contentId}:`, error); // Continue with other tags even if one fails } } return results; } catch (error) { console.error(`Error in removeTags:`, error); throw error; } } }