-
Notifications
You must be signed in to change notification settings - Fork 10
feat(apify): persist scrape results for the remaining 5 platforms #740
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
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
46 changes: 46 additions & 0 deletions
46
lib/apify/facebook/__tests__/handleFacebookProfileScraperResults.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import { describe, it, expect, vi, beforeEach } from "vitest"; | ||
| import { handleFacebookProfileScraperResults } from "@/lib/apify/facebook/handleFacebookProfileScraperResults"; | ||
| const listItems = vi.fn(); | ||
| vi.mock("@/lib/apify/client", () => ({ default: { dataset: vi.fn(() => ({ listItems })) } })); | ||
| const upsertSocials = vi.fn(); | ||
| vi.mock("@/lib/supabase/socials/upsertSocials", () => ({ | ||
| upsertSocials: (...a: unknown[]) => upsertSocials(...a), | ||
| })); | ||
|
|
||
| const payload = { | ||
| eventData: { actorId: "4Hv5RhChiaDk6iwad" }, | ||
| resource: { defaultDatasetId: "ds-1" }, | ||
| } as never; | ||
| // Trimmed from real run ICZxdJBrtP1jkPITS (2026-07-01). | ||
| const REAL_ITEM = { | ||
| pageUrl: "https://www.facebook.com/facebook", | ||
| facebookUrl: "https://www.facebook.com/facebook", | ||
| pageName: "facebook", | ||
| title: "Facebook", | ||
| profilePictureUrl: "https://scontent.cdn/pic.jpg", | ||
| followers: 154000000, | ||
| }; | ||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| upsertSocials.mockResolvedValue([]); | ||
| }); | ||
|
|
||
| describe("handleFacebookProfileScraperResults", () => { | ||
| it("upserts the page from a real item (keyed on profile_url)", async () => { | ||
| listItems.mockResolvedValue({ items: [REAL_ITEM] }); | ||
| await handleFacebookProfileScraperResults(payload); | ||
| expect(upsertSocials).toHaveBeenCalledWith([ | ||
| { | ||
| profile_url: "facebook.com/facebook", | ||
| username: "facebook", | ||
| avatar: "https://scontent.cdn/pic.jpg", | ||
| followerCount: 154000000, | ||
| }, | ||
| ]); | ||
| }); | ||
| it("no-ops on an empty dataset", async () => { | ||
| listItems.mockResolvedValue({ items: [] }); | ||
| expect(await handleFacebookProfileScraperResults(payload)).toEqual({ social: null }); | ||
| expect(upsertSocials).not.toHaveBeenCalled(); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import apifyClient from "@/lib/apify/client"; | ||
| import { upsertSocials } from "@/lib/supabase/socials/upsertSocials"; | ||
| import { normalizeProfileUrl } from "@/lib/socials/normalizeProfileUrl"; | ||
| import type { ApifyWebhookPayload } from "@/lib/apify/validateApifyWebhookRequest"; | ||
|
|
||
| /** Page item from apify~facebook-pages-scraper (real shape, run ICZxdJBrtP1jkPITS). */ | ||
| type FacebookPageItem = { | ||
| pageUrl?: string; | ||
| facebookUrl?: string; | ||
| pageName?: string; | ||
| profilePictureUrl?: string; | ||
| followers?: number; | ||
| }; | ||
|
|
||
| /** Persists a Facebook page scrape back to `socials` (upsert on `profile_url`). */ | ||
| export async function handleFacebookProfileScraperResults(parsed: ApifyWebhookPayload) { | ||
| const { items } = await apifyClient.dataset(parsed.resource.defaultDatasetId).listItems(); | ||
| const first = items[0] as FacebookPageItem | undefined; | ||
| const url = first?.pageUrl ?? first?.facebookUrl; | ||
| if (!url) return { social: null }; | ||
|
|
||
| const social = { | ||
| profile_url: normalizeProfileUrl(url), | ||
| username: first?.pageName, | ||
| avatar: first?.profilePictureUrl ?? null, | ||
| followerCount: first?.followers ?? null, | ||
| }; | ||
| await upsertSocials([social]); | ||
| return { social }; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
46 changes: 46 additions & 0 deletions
46
lib/apify/threads/__tests__/handleThreadsProfileScraperResults.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import { describe, it, expect, vi, beforeEach } from "vitest"; | ||
| import { handleThreadsProfileScraperResults } from "@/lib/apify/threads/handleThreadsProfileScraperResults"; | ||
| const listItems = vi.fn(); | ||
| vi.mock("@/lib/apify/client", () => ({ default: { dataset: vi.fn(() => ({ listItems })) } })); | ||
| const upsertSocials = vi.fn(); | ||
| vi.mock("@/lib/supabase/socials/upsertSocials", () => ({ | ||
| upsertSocials: (...a: unknown[]) => upsertSocials(...a), | ||
| })); | ||
|
|
||
| const payload = { | ||
| eventData: { actorId: "kJdK90pa2hhYYrCK5" }, | ||
| resource: { defaultDatasetId: "ds-1" }, | ||
| } as never; | ||
| // Trimmed from real run 9iiG1sDkpaeWPCWHl (2026-07-01). | ||
| const REAL_ITEM = { | ||
| username: "zuck", | ||
| url: "https://www.threads.net/@zuck", | ||
| profile_pic_url: "https://instagram.cdn/pic.jpg", | ||
| biography: "Mostly superintelligence and MMA takes", | ||
| follower_count: 5642746, | ||
| }; | ||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| upsertSocials.mockResolvedValue([]); | ||
| }); | ||
|
|
||
| describe("handleThreadsProfileScraperResults", () => { | ||
| it("upserts the profile from a real item (keyed on profile_url)", async () => { | ||
| listItems.mockResolvedValue({ items: [REAL_ITEM] }); | ||
| await handleThreadsProfileScraperResults(payload); | ||
| expect(upsertSocials).toHaveBeenCalledWith([ | ||
| { | ||
| profile_url: "threads.net/@zuck", | ||
| username: "zuck", | ||
| avatar: "https://instagram.cdn/pic.jpg", | ||
| bio: "Mostly superintelligence and MMA takes", | ||
| followerCount: 5642746, | ||
| }, | ||
| ]); | ||
| }); | ||
| it("no-ops on an empty dataset", async () => { | ||
| listItems.mockResolvedValue({ items: [] }); | ||
| expect(await handleThreadsProfileScraperResults(payload)).toEqual({ social: null }); | ||
| expect(upsertSocials).not.toHaveBeenCalled(); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import apifyClient from "@/lib/apify/client"; | ||
| import { upsertSocials } from "@/lib/supabase/socials/upsertSocials"; | ||
| import { normalizeProfileUrl } from "@/lib/socials/normalizeProfileUrl"; | ||
| import type { ApifyWebhookPayload } from "@/lib/apify/validateApifyWebhookRequest"; | ||
|
|
||
| /** Profile item from apify~threads-profile-api-scraper (real shape, run 9iiG1sDkpaeWPCWHl). */ | ||
| type ThreadsProfileItem = { | ||
| username?: string; | ||
| url?: string; | ||
| profile_pic_url?: string; | ||
| biography?: string; | ||
| follower_count?: number; | ||
| }; | ||
|
|
||
| /** Persists a Threads profile scrape back to `socials` (upsert on `profile_url`). */ | ||
| export async function handleThreadsProfileScraperResults(parsed: ApifyWebhookPayload) { | ||
| const { items } = await apifyClient.dataset(parsed.resource.defaultDatasetId).listItems(); | ||
| const first = items[0] as ThreadsProfileItem | undefined; | ||
| if (!first?.url) return { social: null }; | ||
|
|
||
| const social = { | ||
| profile_url: normalizeProfileUrl(first.url), | ||
| username: first.username, | ||
| avatar: first.profile_pic_url ?? null, | ||
| bio: first.biography || null, | ||
| followerCount: first.follower_count ?? null, | ||
| }; | ||
| await upsertSocials([social]); | ||
| return { social }; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
51 changes: 51 additions & 0 deletions
51
lib/apify/tiktok/__tests__/handleTiktokProfileScraperResults.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| import { describe, it, expect, vi, beforeEach } from "vitest"; | ||
| import { handleTiktokProfileScraperResults } from "@/lib/apify/tiktok/handleTiktokProfileScraperResults"; | ||
| const listItems = vi.fn(); | ||
| vi.mock("@/lib/apify/client", () => ({ default: { dataset: vi.fn(() => ({ listItems })) } })); | ||
| const upsertSocials = vi.fn(); | ||
| vi.mock("@/lib/supabase/socials/upsertSocials", () => ({ | ||
| upsertSocials: (...a: unknown[]) => upsertSocials(...a), | ||
| })); | ||
|
|
||
| const payload = { | ||
| eventData: { actorId: "GdWCkxBtKWOsKjdch" }, | ||
| resource: { defaultDatasetId: "ds-1" }, | ||
| } as never; | ||
| // Trimmed from real run G4YRI0eUI0d5IidDN (2026-07-01). | ||
| const REAL_ITEM = { | ||
| text: "In welcher Stadt tanzen wir?", | ||
| authorMeta: { | ||
| name: "apache_207", | ||
| profileUrl: "https://www.tiktok.com/@apache_207", | ||
| avatar: "https://p16-common-sign.tiktokcdn-us.com/avatar.jpeg", | ||
| signature: "", | ||
| fans: 917500, | ||
| following: 0, | ||
| }, | ||
| }; | ||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| upsertSocials.mockResolvedValue([]); | ||
| }); | ||
|
|
||
| describe("handleTiktokProfileScraperResults", () => { | ||
| it("upserts author profile stats from a real post item (keyed on profile_url)", async () => { | ||
| listItems.mockResolvedValue({ items: [REAL_ITEM] }); | ||
| await handleTiktokProfileScraperResults(payload); | ||
| expect(upsertSocials).toHaveBeenCalledWith([ | ||
| { | ||
| profile_url: "tiktok.com/@apache_207", | ||
| username: "apache_207", | ||
| avatar: "https://p16-common-sign.tiktokcdn-us.com/avatar.jpeg", | ||
| bio: null, | ||
| followerCount: 917500, | ||
| followingCount: 0, | ||
| }, | ||
| ]); | ||
| }); | ||
| it("no-ops when the dataset is empty or has no authorMeta", async () => { | ||
| listItems.mockResolvedValue({ items: [] }); | ||
| expect(await handleTiktokProfileScraperResults(payload)).toEqual({ social: null }); | ||
| expect(upsertSocials).not.toHaveBeenCalled(); | ||
| }); | ||
| }); | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
P3: Test description claims to cover two scenarios (empty dataset OR no authorMeta) but only tests the empty-dataset path. The 'no authorMeta' case — where
items[0]exists but lacksauthorMeta— is untested. Since the handler guards on!author?.profileUrl, this path is exercised differently and should either be tested or removed from the description to avoid misleading future readers.Prompt for AI agents