-
Notifications
You must be signed in to change notification settings - Fork 6
feat: enrich connectors response with social profile data #406
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: test
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,184 @@ | ||
| import { describe, it, expect, vi, beforeEach } from "vitest"; | ||
| import { enrichConnectorsWithSocials } from "../enrichConnectorsWithSocials"; | ||
| import type { ConnectorInfo } from "../getConnectors"; | ||
| import { selectAccountSocials } from "@/lib/supabase/account_socials/selectAccountSocials"; | ||
|
|
||
| vi.mock("@/lib/supabase/account_socials/selectAccountSocials", () => ({ | ||
| selectAccountSocials: vi.fn(), | ||
| })); | ||
|
|
||
| describe("enrichConnectorsWithSocials", () => { | ||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| }); | ||
|
|
||
| it("should add avatar and username from matching social profiles", async () => { | ||
| const connectors: ConnectorInfo[] = [ | ||
| { slug: "instagram", name: "Instagram", isConnected: true, connectedAccountId: "ca_1" }, | ||
| { slug: "tiktok", name: "TikTok", isConnected: true, connectedAccountId: "ca_2" }, | ||
| ]; | ||
|
|
||
| vi.mocked(selectAccountSocials).mockResolvedValue([ | ||
| { | ||
| id: "as_1", | ||
| account_id: "artist-123", | ||
| social_id: "s_1", | ||
| social: { | ||
| id: "s_1", | ||
| username: "artist_ig", | ||
| profile_url: "https://www.instagram.com/artist_ig", | ||
| avatar: "https://cdn.example.com/ig-avatar.jpg", | ||
| bio: null, | ||
| followerCount: 1000, | ||
| followingCount: 50, | ||
| region: null, | ||
| updated_at: "2026-01-01T00:00:00Z", | ||
| }, | ||
| }, | ||
| { | ||
| id: "as_2", | ||
| account_id: "artist-123", | ||
| social_id: "s_2", | ||
| social: { | ||
| id: "s_2", | ||
| username: "artist_tt", | ||
| profile_url: "https://tiktok.com/@artist_tt", | ||
| avatar: "https://cdn.example.com/tt-avatar.jpg", | ||
| bio: null, | ||
| followerCount: 5000, | ||
| followingCount: 100, | ||
| region: null, | ||
| updated_at: "2026-01-01T00:00:00Z", | ||
| }, | ||
| }, | ||
| ] as any); | ||
|
|
||
| const result = await enrichConnectorsWithSocials(connectors, "artist-123"); | ||
|
|
||
| expect(result[0]).toMatchObject({ | ||
| slug: "instagram", | ||
| avatar: "https://cdn.example.com/ig-avatar.jpg", | ||
| username: "artist_ig", | ||
| }); | ||
| expect(result[1]).toMatchObject({ | ||
| slug: "tiktok", | ||
| avatar: "https://cdn.example.com/tt-avatar.jpg", | ||
| username: "artist_tt", | ||
| }); | ||
| }); | ||
|
|
||
| it("should not add social data for unconnected connectors", async () => { | ||
| const connectors: ConnectorInfo[] = [ | ||
| { slug: "instagram", name: "Instagram", isConnected: false }, | ||
| ]; | ||
|
|
||
| vi.mocked(selectAccountSocials).mockResolvedValue([ | ||
| { | ||
| id: "as_1", | ||
| account_id: "artist-123", | ||
| social_id: "s_1", | ||
| social: { | ||
| id: "s_1", | ||
| username: "artist_ig", | ||
| profile_url: "https://www.instagram.com/artist_ig", | ||
| avatar: "https://cdn.example.com/ig-avatar.jpg", | ||
| bio: null, | ||
| followerCount: 1000, | ||
| followingCount: 50, | ||
| region: null, | ||
| updated_at: "2026-01-01T00:00:00Z", | ||
| }, | ||
| }, | ||
| ] as any); | ||
|
|
||
| const result = await enrichConnectorsWithSocials(connectors, "artist-123"); | ||
|
|
||
| expect(result[0].avatar).toBeUndefined(); | ||
| expect(result[0].username).toBeUndefined(); | ||
| }); | ||
|
|
||
| it("should handle no matching social profiles", async () => { | ||
| const connectors: ConnectorInfo[] = [ | ||
| { slug: "instagram", name: "Instagram", isConnected: true, connectedAccountId: "ca_1" }, | ||
| ]; | ||
|
|
||
| vi.mocked(selectAccountSocials).mockResolvedValue([ | ||
| { | ||
| id: "as_1", | ||
| account_id: "artist-123", | ||
| social_id: "s_1", | ||
| social: { | ||
| id: "s_1", | ||
| username: "artist_tt", | ||
| profile_url: "https://tiktok.com/@artist_tt", | ||
| avatar: "https://cdn.example.com/tt-avatar.jpg", | ||
| bio: null, | ||
| followerCount: 1000, | ||
| followingCount: 50, | ||
| region: null, | ||
| updated_at: "2026-01-01T00:00:00Z", | ||
| }, | ||
| }, | ||
| ] as any); | ||
|
|
||
| const result = await enrichConnectorsWithSocials(connectors, "artist-123"); | ||
|
|
||
| expect(result[0].avatar).toBeUndefined(); | ||
| expect(result[0].username).toBeUndefined(); | ||
| }); | ||
|
|
||
| it("should handle null socials result", async () => { | ||
| const connectors: ConnectorInfo[] = [ | ||
| { slug: "instagram", name: "Instagram", isConnected: true, connectedAccountId: "ca_1" }, | ||
| ]; | ||
|
|
||
| vi.mocked(selectAccountSocials).mockResolvedValue(null); | ||
|
|
||
| const result = await enrichConnectorsWithSocials(connectors, "artist-123"); | ||
|
|
||
| expect(result[0].avatar).toBeUndefined(); | ||
| }); | ||
|
|
||
| it("should match by hostname and ignore subdomains", async () => { | ||
| const connectors: ConnectorInfo[] = [ | ||
| { slug: "instagram", name: "Instagram", isConnected: true, connectedAccountId: "ca_1" }, | ||
| ]; | ||
|
|
||
| vi.mocked(selectAccountSocials).mockResolvedValue([ | ||
| { | ||
| id: "as_1", | ||
| account_id: "artist-123", | ||
| social_id: "s_1", | ||
| social: { | ||
| id: "s_1", | ||
| username: "artist_ig", | ||
| profile_url: "https://scontent.cdninstagram.com/something", | ||
| avatar: "https://cdn.example.com/wrong.jpg", | ||
| bio: null, | ||
| followerCount: 1000, | ||
| followingCount: 50, | ||
| region: null, | ||
| updated_at: "2026-01-01T00:00:00Z", | ||
| }, | ||
| }, | ||
| ] as any); | ||
|
|
||
| const result = await enrichConnectorsWithSocials(connectors, "artist-123"); | ||
|
|
||
| // cdninstagram.com is NOT instagram.com — should not match | ||
| expect(result[0].avatar).toBeUndefined(); | ||
| }); | ||
|
|
||
| it("should not enrich non-social connectors like googlesheets", async () => { | ||
| const connectors: ConnectorInfo[] = [ | ||
| { slug: "googlesheets", name: "Google Sheets", isConnected: true, connectedAccountId: "ca_1" }, | ||
| ]; | ||
|
|
||
| vi.mocked(selectAccountSocials).mockResolvedValue([]); | ||
|
|
||
| const result = await enrichConnectorsWithSocials(connectors, "artist-123"); | ||
|
|
||
| expect(selectAccountSocials).toHaveBeenCalled(); | ||
| expect(result[0].avatar).toBeUndefined(); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| import type { ConnectorInfo } from "./getConnectors"; | ||
| import { selectAccountSocials } from "@/lib/supabase/account_socials/selectAccountSocials"; | ||
| import type { Tables } from "@/types/database.types"; | ||
|
|
||
| type SocialRow = Tables<"socials">; | ||
|
|
||
| /** | ||
| * Connector slug → expected social profile hostname. | ||
| */ | ||
| const CONNECTOR_SOCIAL_HOSTNAMES: Record<string, string> = { | ||
| instagram: "instagram.com", | ||
| tiktok: "tiktok.com", | ||
| }; | ||
|
|
||
| /** | ||
| * Match a social profile_url hostname to a connector slug. | ||
| */ | ||
| function matchesPlatform( | ||
| profileUrl: string, | ||
| expectedHostname: string, | ||
| ): boolean { | ||
| try { | ||
| const hostname = new URL(profileUrl).hostname.toLowerCase(); | ||
| return ( | ||
| hostname === expectedHostname || hostname.endsWith(`.${expectedHostname}`) | ||
| ); | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Enrich connectors with social profile data (avatar, username) from Supabase. | ||
| * | ||
| * For each connected connector that has a social platform mapping, | ||
| * looks up the artist's social profiles and adds avatar/username. | ||
| * | ||
| * @param connectors - The connectors from Composio | ||
| * @param accountId - The artist account ID to look up socials for | ||
| * @returns Enriched connectors with optional avatar and username | ||
| */ | ||
| export async function enrichConnectorsWithSocials( | ||
| connectors: ConnectorInfo[], | ||
| accountId: string, | ||
| ): Promise<ConnectorInfo[]> { | ||
| const accountSocials = await selectAccountSocials(accountId); | ||
| const socials: SocialRow[] = (accountSocials || []) | ||
| .map((item) => (item as any).social as SocialRow | null) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: Custom agent: Enforce Clear Code Style and Maintainability Practices Rule 1 (strict TypeScript typing) is violated by using Prompt for AI agents |
||
| .filter((s): s is SocialRow => s !== null); | ||
|
|
||
| return connectors.map((connector) => { | ||
| if (!connector.isConnected) return connector; | ||
|
|
||
| const expectedHostname = CONNECTOR_SOCIAL_HOSTNAMES[connector.slug]; | ||
| if (!expectedHostname) return connector; | ||
|
|
||
| const social = socials.find((s) => | ||
| matchesPlatform(s.profile_url, expectedHostname), | ||
| ); | ||
|
|
||
| if (!social) return connector; | ||
|
|
||
| return { | ||
| ...connector, | ||
| avatar: social.avatar, | ||
| username: social.username, | ||
| }; | ||
| }); | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,6 +3,7 @@ import { NextResponse } from "next/server"; | |||||||||
| import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; | ||||||||||
| import { validateGetConnectorsRequest } from "./validateGetConnectorsRequest"; | ||||||||||
| import { getConnectors } from "./getConnectors"; | ||||||||||
| import { enrichConnectorsWithSocials } from "./enrichConnectorsWithSocials"; | ||||||||||
|
|
||||||||||
| /** | ||||||||||
| * Display names for connectors. | ||||||||||
|
|
@@ -19,11 +20,15 @@ const CONNECTOR_DISPLAY_NAMES: Record<string, string> = { | |||||||||
| * | ||||||||||
| * Lists all available connectors and their connection status. | ||||||||||
| * Use account_id query param to get connectors for a specific entity. | ||||||||||
| * When an account_id is provided, connectors are enriched with social | ||||||||||
| * profile data (avatar, username) from the artist's linked socials. | ||||||||||
| * | ||||||||||
| * @param request - The incoming request | ||||||||||
| * @returns List of connectors with connection status | ||||||||||
| */ | ||||||||||
| export async function getConnectorsHandler(request: NextRequest): Promise<NextResponse> { | ||||||||||
| export async function getConnectorsHandler( | ||||||||||
| request: NextRequest, | ||||||||||
| ): Promise<NextResponse> { | ||||||||||
| const headers = getCorsHeaders(); | ||||||||||
|
|
||||||||||
| try { | ||||||||||
|
|
@@ -40,15 +45,19 @@ export async function getConnectorsHandler(request: NextRequest): Promise<NextRe | |||||||||
| displayNames: CONNECTOR_DISPLAY_NAMES, | ||||||||||
| }); | ||||||||||
|
|
||||||||||
| // Enrich with social profile data (avatar, username) | ||||||||||
| const enriched = await enrichConnectorsWithSocials(connectors, accountId); | ||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Handle enrichment failures gracefully so social-profile lookup errors don’t turn Prompt for AI agents
Suggested change
|
||||||||||
|
|
||||||||||
| return NextResponse.json( | ||||||||||
| { | ||||||||||
| success: true, | ||||||||||
| connectors, | ||||||||||
| connectors: enriched, | ||||||||||
| }, | ||||||||||
| { status: 200, headers }, | ||||||||||
| ); | ||||||||||
| } catch (error) { | ||||||||||
| const message = error instanceof Error ? error.message : "Failed to fetch connectors"; | ||||||||||
| const message = | ||||||||||
| error instanceof Error ? error.message : "Failed to fetch connectors"; | ||||||||||
| return NextResponse.json({ error: message }, { status: 500, headers }); | ||||||||||
| } | ||||||||||
| } | ||||||||||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P1:
new URL(profileUrl)fails for normalized profile URLs stored without protocol, causing social matching to miss valid Instagram/TikTok records.Prompt for AI agents