From 4bef0aea6b13dcc310c88a8777cedb428de4563f Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Sat, 13 Dec 2025 15:58:05 -0500 Subject: [PATCH] MCP - create_segments --- lib/ai/generateArray.ts | 35 ++++++ lib/artist/getArtistSegments.ts | 4 +- lib/artist/mapArtistSegments.ts | 2 +- lib/mcp/tools/index.ts | 2 + lib/mcp/tools/registerCreateSegmentsTool.ts | 55 ++++++++ lib/segments/consts.ts | 16 +++ lib/segments/createSegmentResponses.ts | 30 +++++ lib/segments/createSegments.ts | 117 ++++++++++++++++++ lib/segments/generateSegments.ts | 33 +++++ lib/segments/getAnalysisPrompt.ts | 36 ++++++ lib/segments/getFanSegmentsToInsert.ts | 27 ++++ .../artist_segments/insertArtistSegments.ts | 22 ++++ .../artist_segments/selectArtistSegments.ts | 59 ++------- .../selectArtistSegmentsWithDetails.ts | 44 +++++++ .../fan_segments/insertFanSegments.ts | 22 ++++ lib/supabase/segments/deleteSegments.ts | 35 ++++++ lib/supabase/segments/insertSegments.ts | 22 ++++ lib/supabase/social_fans/selectSocialFans.ts | 89 +++++++++++++ 18 files changed, 601 insertions(+), 49 deletions(-) create mode 100644 lib/ai/generateArray.ts create mode 100644 lib/mcp/tools/registerCreateSegmentsTool.ts create mode 100644 lib/segments/consts.ts create mode 100644 lib/segments/createSegmentResponses.ts create mode 100644 lib/segments/createSegments.ts create mode 100644 lib/segments/generateSegments.ts create mode 100644 lib/segments/getAnalysisPrompt.ts create mode 100644 lib/segments/getFanSegmentsToInsert.ts create mode 100644 lib/supabase/artist_segments/insertArtistSegments.ts create mode 100644 lib/supabase/artist_segments/selectArtistSegmentsWithDetails.ts create mode 100644 lib/supabase/fan_segments/insertFanSegments.ts create mode 100644 lib/supabase/segments/deleteSegments.ts create mode 100644 lib/supabase/segments/insertSegments.ts create mode 100644 lib/supabase/social_fans/selectSocialFans.ts diff --git a/lib/ai/generateArray.ts b/lib/ai/generateArray.ts new file mode 100644 index 00000000..c33c459f --- /dev/null +++ b/lib/ai/generateArray.ts @@ -0,0 +1,35 @@ +import { generateObject } from "ai"; +import { DEFAULT_MODEL } from "@/lib/const"; +import { z } from "zod"; + +export interface GenerateArrayResult { + segmentName: string; + fans: string[]; +} + +const generateArray = async ({ + system, + prompt, +}: { + system?: string; + prompt: string; +}): Promise => { + const result = await generateObject({ + model: DEFAULT_MODEL, + system, + prompt, + output: "array", + schema: z.object({ + segmentName: z.string().describe("Segment name."), + fans: z + .array(z.string()) + .describe( + "For each Segment Name return an array of fan_social_id included in the segment. Do not make these up. Only use the actual fan_social_id provided in the fan data prompt input.", + ), + }), + }); + + return result.object; +}; + +export default generateArray; diff --git a/lib/artist/getArtistSegments.ts b/lib/artist/getArtistSegments.ts index dbd0e0cb..f4267ae2 100644 --- a/lib/artist/getArtistSegments.ts +++ b/lib/artist/getArtistSegments.ts @@ -1,5 +1,5 @@ import { selectArtistSegmentsCount } from "@/lib/supabase/artist_segments/selectArtistSegmentsCount"; -import { selectArtistSegments } from "@/lib/supabase/artist_segments/selectArtistSegments"; +import { selectArtistSegmentsWithDetails } from "@/lib/supabase/artist_segments/selectArtistSegmentsWithDetails"; import type { ArtistSegmentsQuery } from "@/lib/artist/validateArtistSegmentsQuery"; import { mapArtistSegments, type MappedArtistSegment } from "@/lib/artist/mapArtistSegments"; @@ -38,7 +38,7 @@ export const getArtistSegments = async ({ }; } - const data = await selectArtistSegments(artist_account_id, offset, limit); + const data = await selectArtistSegmentsWithDetails(artist_account_id, offset, limit); if (!data) { return { diff --git a/lib/artist/mapArtistSegments.ts b/lib/artist/mapArtistSegments.ts index c68a569c..b6e40af8 100644 --- a/lib/artist/mapArtistSegments.ts +++ b/lib/artist/mapArtistSegments.ts @@ -1,4 +1,4 @@ -import type { SegmentQueryResult } from "@/lib/supabase/artist_segments/selectArtistSegments"; +import type { SegmentQueryResult } from "@/lib/supabase/artist_segments/selectArtistSegmentsWithDetails"; export interface MappedArtistSegment { id: string; diff --git a/lib/mcp/tools/index.ts b/lib/mcp/tools/index.ts index a56ac2a6..4ab4b16f 100644 --- a/lib/mcp/tools/index.ts +++ b/lib/mcp/tools/index.ts @@ -10,6 +10,7 @@ import { registerUpdateAccountInfoTool } from "./registerUpdateAccountInfoTool"; import { registerAllArtistSocialsTools } from "./artistSocials"; import { registerSearchWebTool } from "./registerSearchWebTool"; import { registerAllFileTools } from "./files"; +import { registerCreateSegmentsTool } from "./registerCreateSegmentsTool"; /** * Registers all MCP tools on the server. @@ -29,4 +30,5 @@ export const registerAllTools = (server: McpServer): void => { registerGetLocalTimeTool(server); registerSearchWebTool(server); registerUpdateAccountInfoTool(server); + registerCreateSegmentsTool(server); }; diff --git a/lib/mcp/tools/registerCreateSegmentsTool.ts b/lib/mcp/tools/registerCreateSegmentsTool.ts new file mode 100644 index 00000000..bece16a7 --- /dev/null +++ b/lib/mcp/tools/registerCreateSegmentsTool.ts @@ -0,0 +1,55 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { createSegments } from "@/lib/segments/createSegments"; +import { getToolResultSuccess } from "@/lib/mcp/getToolResultSuccess"; +import { getToolResultError } from "@/lib/mcp/getToolResultError"; + +const createSegmentsSchema = z.object({ + artist_account_id: z + .string() + .min(1, "Artist account ID is required") + .describe( + "The artist_account_id to create segments for. If not provided, check system prompt for the active artist_account_id.", + ), + prompt: z + .string() + .min(1, "Prompt is required") + .describe( + "The prompt to use for generating segment names. This should be generated by the system if not provided.", + ), +}); + +export type CreateSegmentsArgs = z.infer; + +/** + * Registers the "create_segments" tool on the MCP server. + * Creates segments by analyzing fan data and generating segment names. + * + * @param server - The MCP server instance to register the tool on. + */ +export function registerCreateSegmentsTool(server: McpServer): void { + server.registerTool( + "create_segments", + { + description: + "Create segments by analyzing fan data and generating segment names. This tool fetches all fans for an artist, generates segment names based on the provided prompt, and saves the segments to the database. If required information is missing (social accounts or fan data), the tool will provide step-by-step instructions to gather the missing prerequisites using other available tools.", + inputSchema: createSegmentsSchema, + }, + async (args: CreateSegmentsArgs) => { + try { + const result = await createSegments(args); + + if (result.success) { + return getToolResultSuccess(result); + } + + return getToolResultError(result.message || "Failed to create segments"); + } catch (error) { + console.error("Error creating segments:", error); + return getToolResultError( + error instanceof Error ? error.message : "Failed to create segments", + ); + } + }, + ); +} diff --git a/lib/segments/consts.ts b/lib/segments/consts.ts new file mode 100644 index 00000000..341222ce --- /dev/null +++ b/lib/segments/consts.ts @@ -0,0 +1,16 @@ +export const SEGMENT_FAN_SOCIAL_ID_PROMPT = `For each Segment Name return an array of fan_social_id included in the segment. Do not make these up. Only use the actual fan_social_id provided in the fan data prompt input.`; + +export const SEGMENT_SYSTEM_PROMPT = `You are an expert music industry analyst specializing in fan segmentation. + Your task is to analyze fan data and generate meaningful segment names that would be useful for marketing and engagement strategies. + + Guidelines for segment names: + - Keep names concise and descriptive (2-4 words) + - Focus on engagement patterns, demographics, or behavioral characteristics + - Use clear, actionable language that marketers can understand + - Avoid generic terms like "fans" or "followers" + - Consider factors like engagement frequency, recency, and intensity + - Generate 5-10 segment names that cover different aspects of the fan base + + The segment names should help artists and managers understand their audience better for targeted marketing campaigns. + + ${SEGMENT_FAN_SOCIAL_ID_PROMPT}`; diff --git a/lib/segments/createSegmentResponses.ts b/lib/segments/createSegmentResponses.ts new file mode 100644 index 00000000..c9269ab0 --- /dev/null +++ b/lib/segments/createSegmentResponses.ts @@ -0,0 +1,30 @@ +import { Tables } from "@/types/database.types"; +import type { GenerateArrayResult } from "./generateSegments"; + +interface CreateArtistSegmentsSuccessData { + supabase_segments: Tables<"segments">[]; + supabase_artist_segments: Tables<"artist_segments">[]; + segments: GenerateArrayResult[]; + supabase_fan_segments: Tables<"fan_segments">[]; +} + +export const successResponse = ( + message: string, + data: CreateArtistSegmentsSuccessData, + count: number, +) => ({ + success: true, + status: "success", + message, + data, + count, +}); + +export const errorResponse = (message: string) => ({ + success: false, + status: "error", + message, + data: [], + count: 0, +}); + diff --git a/lib/segments/createSegments.ts b/lib/segments/createSegments.ts new file mode 100644 index 00000000..df358664 --- /dev/null +++ b/lib/segments/createSegments.ts @@ -0,0 +1,117 @@ +import { selectAccountSocials } from "@/lib/supabase/account_socials/selectAccountSocials"; +import { selectSocialFans } from "@/lib/supabase/social_fans/selectSocialFans"; +import { generateSegments } from "./generateSegments"; +import { insertSegments } from "@/lib/supabase/segments/insertSegments"; +import { deleteSegments } from "@/lib/supabase/segments/deleteSegments"; +import { insertArtistSegments } from "@/lib/supabase/artist_segments/insertArtistSegments"; +import { insertFanSegments } from "@/lib/supabase/fan_segments/insertFanSegments"; +import { Tables } from "@/types/database.types"; +import { successResponse, errorResponse } from "./createSegmentResponses"; +import type { GenerateArrayResult } from "./generateSegments"; +import { getFanSegmentsToInsert } from "./getFanSegmentsToInsert"; +import { selectAccounts } from "@/lib/supabase/accounts/selectAccounts"; +import { CreateSegmentsArgs } from "../mcp/tools/registerCreateSegmentsTool"; + +export const createSegments = async ({ artist_account_id, prompt }: CreateSegmentsArgs) => { + try { + // Get artist info for better error messages + const accounts = await selectAccounts(artist_account_id); + const artistInfo = accounts[0]; + const artistName = artistInfo?.name || "this artist"; + + // Step 1: Get all social IDs for the artist + const accountSocials = await selectAccountSocials(artist_account_id, 0, 10000); + const socialIds = (accountSocials || []).map((as: { social_id: string }) => as.social_id); + + if (socialIds.length === 0) { + return { + ...errorResponse("No social account found for this artist"), + feedback: + `No Instagram accounts found for ${artistName}. To automatically set up Instagram accounts, please follow these steps:\n` + + `1. Call 'search_web' to search for "${artistName} Instagram handle"\n` + + "2. Call 'update_artist_socials' with the discovered Instagram profile URL\n" + + "3. Call 'create_segments' again to retry segment creation\n" + + "Instagram is required for fan segmentation as it's the primary social platform configured for segments.", + }; + } + + // Step 2: Get all fans for the artist + const fans = await selectSocialFans({ + social_ids: socialIds, + orderBy: "latest_engagement", + orderDirection: "desc", + }); + + if (fans.length === 0) { + return { + ...errorResponse("No fans found for this artist"), + feedback: + `No social_fans records found for ${artistName}. Before creating segments, you need social_fans data. Follow these steps:\n` + + "1. Call 'scrape_instagram_profile' with the artist's Instagram handles to get posts\n" + + "2. Call 'scrape_instagram_comments' with Instagram post URLs to scrape comment data\n" + + "3. Wait for the scraping jobs to complete and process into social_fans records\n" + + "4. Call 'create_segments' again once social_fans records are populated\n" + + "Note: Scraping jobs may take several minutes to complete.", + }; + } + + // Step 3: Generate segment names using AI + const segments = await generateSegments({ fans, prompt }); + + if (segments.length === 0) { + return errorResponse("Failed to generate segment names"); + } + + // Step 4: Delete existing segments for the artist + await deleteSegments(artist_account_id); + + // Step 5: Insert segments into the database + const segmentsToInsert = segments.map((segment: GenerateArrayResult) => ({ + name: segment.segmentName, + updated_at: new Date().toISOString(), + })); + + const insertedSegments = await insertSegments(segmentsToInsert); + + // Step 6: Associate segments with the artist + const artistSegmentsToInsert = insertedSegments.map((segment: Tables<"segments">) => ({ + artist_account_id, + segment_id: segment.id, + updated_at: new Date().toISOString(), + })); + + const insertedArtistSegments = await insertArtistSegments(artistSegmentsToInsert); + + // Step 7: Associate fans with the new segments + // Build a set of valid IDs from the fetched fan list + const validFanIds = new Set(fans.map(f => f.fan_social_id)); + + const fanSegmentsToInsert = getFanSegmentsToInsert(segments, insertedSegments).filter(fs => { + const ok = validFanIds.has(fs.fan_social_id); + if (!ok) console.warn(`Skipping unknown fan_social_id: ${fs.fan_social_id}`); + return ok; + }); + + if (fanSegmentsToInsert.length === 0) { + return errorResponse("No valid fan IDs matched any segment."); + } + + const insertedFanSegments = await insertFanSegments(fanSegmentsToInsert); + + return successResponse( + `Successfully created ${segments.length} segments for artist`, + { + supabase_segments: insertedSegments, + supabase_artist_segments: insertedArtistSegments, + supabase_fan_segments: insertedFanSegments, + segments, + }, + segments.length, + ); + } catch (error) { + console.error("Error creating artist segments:", error); + return errorResponse( + error instanceof Error ? error.message : "Failed to create artist segments", + ); + } +}; diff --git a/lib/segments/generateSegments.ts b/lib/segments/generateSegments.ts new file mode 100644 index 00000000..40beeb09 --- /dev/null +++ b/lib/segments/generateSegments.ts @@ -0,0 +1,33 @@ +import generateArray from "@/lib/ai/generateArray"; +import { SEGMENT_SYSTEM_PROMPT } from "./consts"; +import getAnalysisPrompt from "./getAnalysisPrompt"; +import type { SocialFanWithDetails } from "@/lib/supabase/social_fans/selectSocialFans"; + +export interface GenerateSegmentsParams { + fans: SocialFanWithDetails[]; + prompt: string; +} + +export interface GenerateArrayResult { + segmentName: string; + fans: string[]; +} + +export const generateSegments = async ({ + fans, + prompt, +}: GenerateSegmentsParams): Promise => { + try { + const analysisPrompt = getAnalysisPrompt({ fans, prompt }); + + const result = await generateArray({ + system: SEGMENT_SYSTEM_PROMPT, + prompt: analysisPrompt, + }); + + return result; + } catch (error) { + console.error("Error generating segments:", error); + throw new Error("Failed to generate segments from fan data"); + } +}; diff --git a/lib/segments/getAnalysisPrompt.ts b/lib/segments/getAnalysisPrompt.ts new file mode 100644 index 00000000..d6a05ffd --- /dev/null +++ b/lib/segments/getAnalysisPrompt.ts @@ -0,0 +1,36 @@ +import { SEGMENT_FAN_SOCIAL_ID_PROMPT } from "./consts"; +import type { GenerateSegmentsParams } from "./generateSegments"; + +const getAnalysisPrompt = ({ fans, prompt }: GenerateSegmentsParams) => { + const fanCount = fans.length; + const fanData = fans.map(fan => { + const obj = { + fan_social_id: fan.fan_social_id, + username: fan.fan_social.username, + bio: fan.fan_social.bio, + followerCount: fan.fan_social.followerCount, + followingCount: fan.fan_social.followingCount, + comment: fan.latest_engagement_comment?.comment || null, + }; + // Remove keys with null values + return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== null)); + }); + + const maxFans = 111; + const slicedFanData = fanData.slice(0, maxFans); + + const fanDataString = JSON.stringify(slicedFanData, null, 2); + const analysisPrompt = `Analyze the following fan data and generate segment names based on the provided prompt. + + Fan Data Summary: + - Total fans: ${fanCount} + - Fan data: ${fanDataString} + + Artist's specific prompt: ${prompt} + + Generate segment names that align with the artist's requirements and the fan data characteristics. + ${SEGMENT_FAN_SOCIAL_ID_PROMPT}`; + return analysisPrompt; +}; + +export default getAnalysisPrompt; diff --git a/lib/segments/getFanSegmentsToInsert.ts b/lib/segments/getFanSegmentsToInsert.ts new file mode 100644 index 00000000..a6f7097d --- /dev/null +++ b/lib/segments/getFanSegmentsToInsert.ts @@ -0,0 +1,27 @@ +import type { GenerateArrayResult } from "./generateSegments"; +import { Tables } from "@/types/database.types"; + +/** + * Returns an array of fan-segment associations to insert, based on the AI-generated segments and the inserted segment records. + * Each fan is only associated with the segment(s) they are assigned to in the segments array. + * + * @param segments - The AI-generated segments. + * @param insertedSegments - The inserted segment records. + * @returns An array of fan-segment associations to insert. + */ +export function getFanSegmentsToInsert( + segments: GenerateArrayResult[], + insertedSegments: Tables<"segments">[], +) { + const segmentNameToId = new Map(insertedSegments.map(seg => [seg.name, seg.id])); + + return segments.flatMap((segment: GenerateArrayResult) => { + const segmentId = segmentNameToId.get(segment.segmentName); + if (!segmentId || !segment.fans) return []; + return segment.fans.map((fan_social_id: string) => ({ + fan_social_id, + segment_id: segmentId, + updated_at: new Date().toISOString(), + })); + }); +} diff --git a/lib/supabase/artist_segments/insertArtistSegments.ts b/lib/supabase/artist_segments/insertArtistSegments.ts new file mode 100644 index 00000000..1e442eec --- /dev/null +++ b/lib/supabase/artist_segments/insertArtistSegments.ts @@ -0,0 +1,22 @@ +import supabase from "../serverClient"; +import { Tables, TablesInsert } from "@/types/database.types"; + +/** + * Inserts artist_segments associations into the database. + * + * @param artistSegments - Array of artist_segment objects to insert + * @returns Array of inserted artist_segments + * @throws Error if the insertion fails + */ +export const insertArtistSegments = async ( + artistSegments: TablesInsert<"artist_segments">[], +): Promise[]> => { + const { data, error } = await supabase.from("artist_segments").insert(artistSegments).select(); + + if (error) { + console.error("Error inserting artist segments:", error); + throw error; + } + + return data || []; +}; diff --git a/lib/supabase/artist_segments/selectArtistSegments.ts b/lib/supabase/artist_segments/selectArtistSegments.ts index 6f24e1e0..370991b3 100644 --- a/lib/supabase/artist_segments/selectArtistSegments.ts +++ b/lib/supabase/artist_segments/selectArtistSegments.ts @@ -1,58 +1,25 @@ import supabase from "../serverClient"; /** - * Type for the Supabase query result with joined segments and accounts - */ -export interface SegmentQueryResult { - id: string; - artist_account_id: string; - segment_id: string; - updated_at: string | null; - segments: { - name: string | null; - } | null; - accounts: { - name: string | null; - } | null; -} - -/** - * Selects artist segments with joined segment and account data, filtered by artist account ID. + * Selects artist_segments records for an artist account. * - * @param artist_account_id - The unique identifier of the artist account - * @param offset - The number of records to skip - * @param limit - The maximum number of records to return - * @returns The query results with joined segment and account data - * @throws Error if the query fails + * @param artist_account_id - The artist account ID + * @returns Array of artist_segments records with segment_id, or empty array if none found or on error */ -export async function selectArtistSegments( - artist_account_id: string, - offset: number, - limit: number, -): Promise { - const queryText = ` - id, - artist_account_id, - segment_id, - updated_at, - segments ( - name - ), - accounts:artist_account_id ( - name - ) - `; - +export async function selectArtistSegments(artist_account_id: string) { const { data, error } = await supabase .from("artist_segments") - .select(queryText) - .eq("artist_account_id", artist_account_id) - .order("updated_at", { ascending: false }) - .range(offset, offset + limit - 1); + .select("segment_id") + .eq("artist_account_id", artist_account_id); if (error) { - throw new Error(`Failed to fetch artist segments: ${error.message}`); + console.error("Error fetching artist segments:", error); + return []; + } + + if (!data || data.length === 0) { + return []; } - return data as unknown as SegmentQueryResult[] | null; + return data; } diff --git a/lib/supabase/artist_segments/selectArtistSegmentsWithDetails.ts b/lib/supabase/artist_segments/selectArtistSegmentsWithDetails.ts new file mode 100644 index 00000000..56dce627 --- /dev/null +++ b/lib/supabase/artist_segments/selectArtistSegmentsWithDetails.ts @@ -0,0 +1,44 @@ +import supabase from "../serverClient"; +import type { Tables } from "@/types/database.types"; + +/** + * Type for the Supabase query result with joined segments and accounts + */ +export type SegmentQueryResult = Tables<"artist_segments"> & { + segments: Tables<"segments"> | null; + accounts: Tables<"accounts"> | null; +}; + +/** + * Selects artist segments with joined segment and account data, filtered by artist account ID. + * + * @param artist_account_id - The unique identifier of the artist account + * @param offset - The number of records to skip + * @param limit - The maximum number of records to return + * @returns The query results with joined segment and account data + * @throws Error if the query fails + */ +export async function selectArtistSegmentsWithDetails( + artist_account_id: string, + offset: number, + limit: number, +): Promise { + const queryText = ` + *, + segments(*), + accounts:artist_account_id(*) + `; + + const { data, error } = await supabase + .from("artist_segments") + .select(queryText) + .eq("artist_account_id", artist_account_id) + .order("updated_at", { ascending: false }) + .range(offset, offset + limit - 1); + + if (error) { + throw new Error(`Failed to fetch artist segments: ${error.message}`); + } + + return data as unknown as SegmentQueryResult[] | null; +} diff --git a/lib/supabase/fan_segments/insertFanSegments.ts b/lib/supabase/fan_segments/insertFanSegments.ts new file mode 100644 index 00000000..c1951886 --- /dev/null +++ b/lib/supabase/fan_segments/insertFanSegments.ts @@ -0,0 +1,22 @@ +import supabase from "../serverClient"; +import { Tables, TablesInsert } from "@/types/database.types"; + +/** + * Inserts fan_segments associations into the database. + * + * @param fanSegments - Array of fan_segment objects to insert + * @returns Array of inserted fan_segments + * @throws Error if the insertion fails + */ +export const insertFanSegments = async ( + fanSegments: TablesInsert<"fan_segments">[], +): Promise[]> => { + const { data, error } = await supabase.from("fan_segments").insert(fanSegments).select(); + + if (error) { + console.error("Error inserting fan segments:", error); + throw error; + } + + return data || []; +}; diff --git a/lib/supabase/segments/deleteSegments.ts b/lib/supabase/segments/deleteSegments.ts new file mode 100644 index 00000000..c4fa7a2b --- /dev/null +++ b/lib/supabase/segments/deleteSegments.ts @@ -0,0 +1,35 @@ +import supabase from "../serverClient"; +import { Tables } from "@/types/database.types"; +import { selectArtistSegments } from "../artist_segments/selectArtistSegments"; + +/** + * Deletes all segments associated with an artist account. + * + * @param artist_account_id - The artist account ID + * @returns Array of deleted segments + */ +export const deleteSegments = async (artist_account_id: string): Promise[]> => { + // Get all segment_ids associated with the artist + const artistSegments = await selectArtistSegments(artist_account_id); + const segmentIds = artistSegments.map((item: { segment_id: string }) => item.segment_id); + + if (segmentIds.length === 0) { + // No segments to delete + return []; + } + + // Delete the segments from the segments table + const { data, error } = await supabase.from("segments").delete().in("id", segmentIds).select(); + + if (error) { + console.error("Error deleting segments:", error); + return []; + } + + if (!data || data.length === 0) { + console.warn(`No segments found with ids: ${segmentIds.join(", ")}`); + return []; + } + + return data; +}; diff --git a/lib/supabase/segments/insertSegments.ts b/lib/supabase/segments/insertSegments.ts new file mode 100644 index 00000000..4c4c1111 --- /dev/null +++ b/lib/supabase/segments/insertSegments.ts @@ -0,0 +1,22 @@ +import supabase from "../serverClient"; +import { Tables, TablesInsert } from "@/types/database.types"; + +/** + * Inserts segments into the database. + * + * @param segments - Array of segment objects to insert + * @returns Array of inserted segments + * @throws Error if the insertion fails + */ +export const insertSegments = async ( + segments: TablesInsert<"segments">[], +): Promise[]> => { + const { data, error } = await supabase.from("segments").insert(segments).select(); + + if (error) { + console.error("Error inserting segments:", error); + throw error; + } + + return data || []; +}; diff --git a/lib/supabase/social_fans/selectSocialFans.ts b/lib/supabase/social_fans/selectSocialFans.ts new file mode 100644 index 00000000..af855376 --- /dev/null +++ b/lib/supabase/social_fans/selectSocialFans.ts @@ -0,0 +1,89 @@ +import supabase from "../serverClient"; +import { Tables } from "@/types/database.types"; + +type SocialFan = Tables<"social_fans">; +type Social = Tables<"socials">; +type PostComment = Tables<"post_comments">; + +// Extended type to include joined data +export interface SocialFanWithDetails extends SocialFan { + artist_social: Social; + fan_social: Social; + latest_engagement_comment: PostComment | null; +} + +// Allowed top-level columns for ordering +const SOCIAL_FANS_ORDERABLE_COLUMNS = [ + "id", + "artist_social_id", + "fan_social_id", + "created_at", + "updated_at", + "latest_engagement", + "latest_engagement_id", +] as const; +type SocialFansOrderableColumn = (typeof SOCIAL_FANS_ORDERABLE_COLUMNS)[number]; + +interface SelectSocialFansParams { + social_ids?: string[]; + orderBy?: SocialFansOrderableColumn; + orderDirection?: "asc" | "desc"; +} + +export const selectSocialFans = async ( + params?: SelectSocialFansParams, +): Promise => { + let query = supabase.from("social_fans").select(` + *, + artist_social:socials!social_fans_artist_social_id_fkey( + id, + username, + bio, + followerCount, + followingCount, + avatar, + profile_url, + region, + updated_at + ), + fan_social:socials!social_fans_fan_social_id_fkey( + id, + username, + bio, + followerCount, + followingCount, + avatar, + profile_url, + region, + updated_at + ), + latest_engagement_comment:post_comments!social_fans_latest_engagement_id_fkey( + id, + comment, + commented_at, + post_id, + social_id + ) + `); + + if (params?.social_ids && params.social_ids.length > 0) { + query = query.in("artist_social_id", params.social_ids); + } + + // Only allow ordering by top-level columns + if (params?.orderBy && SOCIAL_FANS_ORDERABLE_COLUMNS.includes(params.orderBy)) { + query = query.order(params.orderBy, { + ascending: params.orderDirection !== "desc", + nullsFirst: false, + }); + } + + const { data, error } = await query; + + if (error) { + console.error("Error selecting social fans:", error); + throw error; + } + + return (data || []) as SocialFanWithDetails[]; +};