From 3cd944a1f891cb3ea9732cb99c0d9e03c62a59a9 Mon Sep 17 00:00:00 2001 From: matt rothenberger Date: Wed, 8 Oct 2025 14:47:02 -0400 Subject: [PATCH 1/6] check in --- api/src/social-post-metrics/social-post-metrics.controller.ts | 0 api/src/social-post-metrics/social-post-metrics.module.ts | 0 api/src/social-post-metrics/social-post-metrics.service.ts | 0 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 api/src/social-post-metrics/social-post-metrics.controller.ts create mode 100644 api/src/social-post-metrics/social-post-metrics.module.ts create mode 100644 api/src/social-post-metrics/social-post-metrics.service.ts diff --git a/api/src/social-post-metrics/social-post-metrics.controller.ts b/api/src/social-post-metrics/social-post-metrics.controller.ts new file mode 100644 index 0000000..e69de29 diff --git a/api/src/social-post-metrics/social-post-metrics.module.ts b/api/src/social-post-metrics/social-post-metrics.module.ts new file mode 100644 index 0000000..e69de29 diff --git a/api/src/social-post-metrics/social-post-metrics.service.ts b/api/src/social-post-metrics/social-post-metrics.service.ts new file mode 100644 index 0000000..e69de29 From 46ed5818690df6694ee4e17904b6151d0ecb332f Mon Sep 17 00:00:00 2001 From: matt rothenberger Date: Wed, 8 Oct 2025 21:44:03 -0400 Subject: [PATCH 2/6] Init feed --- api/src/app.module.ts | 2 + api/src/main.ts | 2 + .../social-account-feeds-controller.md.ts | 7 + .../dto/platform-post-metrics.dto.ts | 151 ++++++++++++++++ .../dto/platform-post-query.dto.ts | 58 +++++++ .../dto/platform-post.dto.ts | 65 +++++++ .../social-account-feeds.controller.ts | 163 ++++++++++++++++++ .../social-account-feeds.module.ts | 12 ++ .../social-account-feeds.service.ts | 24 +++ .../social-post-metrics.controller.ts | 0 .../social-post-metrics.module.ts | 0 .../social-post-metrics.service.ts | 0 12 files changed, 484 insertions(+) create mode 100644 api/src/social-account-feeds/docs/social-account-feeds-controller.md.ts create mode 100644 api/src/social-account-feeds/dto/platform-post-metrics.dto.ts create mode 100644 api/src/social-account-feeds/dto/platform-post-query.dto.ts create mode 100644 api/src/social-account-feeds/dto/platform-post.dto.ts create mode 100644 api/src/social-account-feeds/social-account-feeds.controller.ts create mode 100644 api/src/social-account-feeds/social-account-feeds.module.ts create mode 100644 api/src/social-account-feeds/social-account-feeds.service.ts delete mode 100644 api/src/social-post-metrics/social-post-metrics.controller.ts delete mode 100644 api/src/social-post-metrics/social-post-metrics.module.ts delete mode 100644 api/src/social-post-metrics/social-post-metrics.service.ts diff --git a/api/src/app.module.ts b/api/src/app.module.ts index 42c2eee..258e8bb 100644 --- a/api/src/app.module.ts +++ b/api/src/app.module.ts @@ -12,6 +12,7 @@ import { AuthGuard } from './auth/auth.guard'; import { UnkeyModule } from './unkey/unkey.module'; import { SocialPostPreviewsModule } from './social-posts-previews/social-posts-previews.module'; import { WebhooksModule } from './webhooks/webhooks.module'; +import { SocialAccountFeedsModule } from './social-account-feeds/social-account-feeds.module'; @Module({ imports: [ @@ -26,6 +27,7 @@ import { WebhooksModule } from './webhooks/webhooks.module'; SocialAccountsModule, SocialPostPreviewsModule, WebhooksModule, + SocialAccountFeedsModule, ], controllers: [], providers: [AuthGuard], diff --git a/api/src/main.ts b/api/src/main.ts index 21ae4d7..afc6cb2 100644 --- a/api/src/main.ts +++ b/api/src/main.ts @@ -16,6 +16,7 @@ import type { NestExpressApplication } from '@nestjs/platform-express'; import type { NextFunction, Request, Response } from 'express'; import { webhookControllerDescription } from './webhooks/docs/webhooks-controller.md'; import { socialPostPreviewControllerDescription } from './social-posts-previews/docs/social-posts-preview-controller.md'; +import { socialAccountFeedsControllerDescription } from './social-account-feeds/docs/social-account-feeds-controller.md'; async function bootstrap() { const app: NestExpressApplication = await NestFactory.create(AppModule); @@ -32,6 +33,7 @@ async function bootstrap() { .addTag('Social Posts', postsControllerDescription) .addTag('Social Accounts', socialAccountsControllerDescription) .addTag('Social Post Results', postResultsControllerDescription) + .addTag('Social Account Feeds', socialAccountFeedsControllerDescription) .addTag('Webhooks', webhookControllerDescription) .addTag('Social Post Previews', socialPostPreviewControllerDescription) .setVersion('1.0') diff --git a/api/src/social-account-feeds/docs/social-account-feeds-controller.md.ts b/api/src/social-account-feeds/docs/social-account-feeds-controller.md.ts new file mode 100644 index 0000000..856172b --- /dev/null +++ b/api/src/social-account-feeds/docs/social-account-feeds-controller.md.ts @@ -0,0 +1,7 @@ +export const socialAccountFeedsControllerDescription = ` +The social account feed is every post made for the social account, including posts not made through our API. +Use this endpoint to get the platform details for any post made under the connected account. +Details will include: + - Metrics information including views, likes, follows, etc.. + - Post information including caption, url, media, etc.. +`; diff --git a/api/src/social-account-feeds/dto/platform-post-metrics.dto.ts b/api/src/social-account-feeds/dto/platform-post-metrics.dto.ts new file mode 100644 index 0000000..b9c5611 --- /dev/null +++ b/api/src/social-account-feeds/dto/platform-post-metrics.dto.ts @@ -0,0 +1,151 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class TikTokBusinessVideoMetricPercentageDto { + @ApiProperty({ description: 'Percentage value for the metric' }) + percentage: number; + + @ApiProperty({ description: 'Time in seconds for the metric' }) + second: string; +} + +export class TikTokBusinessPostImpressionSourceDto { + @ApiProperty({ description: 'Percentage of impressions from this source' }) + percentage: number; + + @ApiProperty({ description: 'Name of the impression source' }) + impression_source: string; +} + +export class TikTokBusinessPostAudienceTypeDto { + @ApiProperty({ description: 'Percentage of audience of this type' }) + percentage: number; + + @ApiProperty({ description: 'Type of audience' }) + type: string; +} + +export class TikTokBusinessPostAudienceCountryDto { + @ApiProperty({ description: 'Percentage of audience from this country' }) + percentage: number; + + @ApiProperty({ description: 'Country name' }) + country: string; +} + +export class TikTokBusinessPostAudienceCityDto { + @ApiProperty({ description: 'Percentage of audience from this city' }) + percentage: number; + + @ApiProperty({ description: 'City name' }) + city_name: string; +} + +export class TikTokBusinessPostAudienceGenderDto { + @ApiProperty({ description: 'Percentage of audience of this gender' }) + percentage: number; + + @ApiProperty({ description: 'Gender category' }) + gender: string; +} + +export class TikTokBusinessMetricsDto { + @ApiProperty({ description: 'Number of likes on the post' }) + likes: number; + + @ApiProperty({ description: 'Number of comments on the post' }) + comments: number; + + @ApiProperty({ description: 'Number of shares on the post' }) + shares: number; + + @ApiProperty({ description: 'Number of favorites on the post' }) + favorites: number; + + @ApiProperty({ description: 'Total reach of the post' }) + reach: number; + + @ApiProperty({ description: 'Total number of video views' }) + video_views: number; + + @ApiProperty({ description: 'Total time watched in seconds' }) + total_time_watched: number; + + @ApiProperty({ description: 'Average time watched in seconds' }) + average_time_watched: number; + + @ApiProperty({ description: 'Rate of full video watches as a percentage' }) + full_video_watched_rate: number; + + @ApiProperty({ description: 'Number of new followers gained from the post' }) + new_followers: number; + + @ApiProperty({ description: 'Number of profile views generated' }) + profile_views: number; + + @ApiProperty({ description: 'Number of website clicks' }) + website_clicks: number; + + @ApiProperty({ description: 'Number of phone number clicks' }) + phone_number_clicks: number; + + @ApiProperty({ description: 'Number of lead submissions' }) + lead_submissions: number; + + @ApiProperty({ description: 'Number of app download clicks' }) + app_download_clicks: number; + + @ApiProperty({ description: 'Number of email clicks' }) + email_clicks: number; + + @ApiProperty({ description: 'Number of address clicks' }) + address_clicks: number; + + @ApiProperty({ + description: 'Video view retention data by percentage and time', + type: TikTokBusinessVideoMetricPercentageDto, + isArray: true, + }) + video_view_retention: TikTokBusinessVideoMetricPercentageDto[]; + + @ApiProperty({ + description: 'Impression sources breakdown', + type: TikTokBusinessPostImpressionSourceDto, + isArray: true, + }) + impression_sources: TikTokBusinessPostImpressionSourceDto[]; + + @ApiProperty({ + description: 'Audience types breakdown', + type: TikTokBusinessPostAudienceTypeDto, + isArray: true, + }) + audience_types: TikTokBusinessPostAudienceTypeDto[]; + + @ApiProperty({ + description: 'Audience genders breakdown', + type: TikTokBusinessPostAudienceGenderDto, + isArray: true, + }) + audience_genders: TikTokBusinessPostAudienceGenderDto[]; + + @ApiProperty({ + description: 'Audience countries breakdown', + type: TikTokBusinessPostAudienceCountryDto, + isArray: true, + }) + audience_countries: TikTokBusinessPostAudienceCountryDto[]; + + @ApiProperty({ + description: 'Audience cities breakdown', + type: TikTokBusinessPostAudienceCityDto, + isArray: true, + }) + audience_cities: TikTokBusinessPostAudienceCityDto[]; + + @ApiProperty({ + description: 'Engagement likes data by percentage and time', + type: TikTokBusinessVideoMetricPercentageDto, + isArray: true, + }) + engagement_likes: TikTokBusinessVideoMetricPercentageDto[]; +} diff --git a/api/src/social-account-feeds/dto/platform-post-query.dto.ts b/api/src/social-account-feeds/dto/platform-post-query.dto.ts new file mode 100644 index 0000000..04cbde7 --- /dev/null +++ b/api/src/social-account-feeds/dto/platform-post-query.dto.ts @@ -0,0 +1,58 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsOptional, IsString } from 'class-validator'; +import { BasePaginatedQueryDto } from '../../pagination/base-paginated-query.dto'; + +export class PlatformPostQueryDto extends BasePaginatedQueryDto { + @ApiProperty({ + description: + 'Filter by Post for Me Social Postexternal ID. Multiple values imply OR logic (e.g., ?external_post_id=xxxxxx&external_post_id=yyyyyy).', + required: false, + type: 'array', + items: { type: 'string' }, + }) + @IsOptional() + external_post_id?: string[]; + + @ApiProperty({ + description: + 'Filter by Post for Me Social Post id(s). Multiple values imply OR logic (e.g., ?social_post_id=sp_xxxxxx&social_post_id=sp_yyyyyy).', + required: false, + type: 'array', + items: { type: 'string' }, + }) + @IsString({ each: true }) + @IsOptional() + social_post_id?: string[]; + + @ApiProperty({ + description: + 'Filter by Post for Me Social Post Result id(s). Multiple values imply OR logic (e.g., ?social_post_id=spr_xxxxxx&social_post_id=spr_yyyyyy).', + required: false, + type: 'array', + items: { type: 'string' }, + }) + @IsString({ each: true }) + @IsOptional() + social_post_result_id?: string[]; + + @ApiProperty({ + description: + 'Filter by Post for Me Social Post Account id(s). Multiple values imply OR logic (e.g., ?social_post_id=spc_xxxxxx&social_post_id=spc_yyyyyy).', + required: false, + type: 'array', + items: { type: 'string' }, + }) + @IsString({ each: true }) + @IsOptional() + social_account_id?: string[]; + + @ApiProperty({ + description: + 'Filter by Post for Me Social Postexternal ID. Multiple values imply OR logic (e.g., ?external_account_id=xxxxxx&external_account_id=yyyyyy).', + required: false, + type: 'array', + items: { type: 'string' }, + }) + @IsOptional() + external_account_id?: string[]; +} diff --git a/api/src/social-account-feeds/dto/platform-post.dto.ts b/api/src/social-account-feeds/dto/platform-post.dto.ts new file mode 100644 index 0000000..f06ced0 --- /dev/null +++ b/api/src/social-account-feeds/dto/platform-post.dto.ts @@ -0,0 +1,65 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { TikTokBusinessMetricsDto } from './platform-post-metrics.dto'; + +export class PlatformPostDto { + @ApiProperty({ description: 'Social media platform name' }) + platform: string; + + @ApiProperty({ + description: 'ID of the social post result', + required: false, + nullable: true, + }) + social_post_result_id?: string; + + @ApiProperty({ + description: 'ID of the social post', + required: false, + nullable: true, + }) + social_post_id?: string; + + @ApiProperty({ + description: 'External post ID from the platform', + required: false, + nullable: true, + }) + external_post_id?: string; + + @ApiProperty({ description: 'Platform-specific post ID' }) + platform_post_id: string; + + @ApiProperty({ + description: 'ID of the social account', + required: false, + nullable: true, + }) + social_account_id?: string; + + @ApiProperty({ + description: 'External account ID from the platform', + required: false, + nullable: true, + }) + external_account_id?: string; + + @ApiProperty({ description: 'Platform-specific account ID' }) + platform_account_id: string; + + @ApiProperty({ description: 'URL to the post on the platform' }) + platform_url: string; + + @ApiProperty({ description: 'Caption or text content of the post' }) + caption: string; + + @ApiProperty({ + description: 'Array of media items attached to the post', + isArray: true, + }) + media: any[]; + + @ApiProperty({ + description: 'Post metrics and analytics data', + }) + metrics: TikTokBusinessMetricsDto; +} diff --git a/api/src/social-account-feeds/social-account-feeds.controller.ts b/api/src/social-account-feeds/social-account-feeds.controller.ts new file mode 100644 index 0000000..699e05f --- /dev/null +++ b/api/src/social-account-feeds/social-account-feeds.controller.ts @@ -0,0 +1,163 @@ +import { + Controller, + Get, + HttpException, + HttpStatus, + Param, + Query, +} from '@nestjs/common'; +import { + ApiBearerAuth, + ApiOkResponse, + ApiOperation, + ApiParam, + ApiResponse, + ApiTags, + getSchemaPath, +} from '@nestjs/swagger'; + +import { PaginationService } from '../pagination/pagination.service'; +import type { PaginatedResponse } from '../pagination/pagination-response.interface'; + +import { User } from '../auth/user.decorator'; +import type { RequestUser } from '../auth/user.interface'; + +import { Protect } from '../auth/protect.decorator'; +import { PlatformPostDto } from './dto/platform-post.dto'; +import { PlatformPostQueryDto } from './dto/platform-post-query.dto'; +import { SocialAccountFeedsService } from './social-account-feeds.service'; + +@Controller('social-account-feeds') +@ApiTags('Social Account Feeds') +@ApiBearerAuth() +@Protect() +export class SocialAccountFeedsController { + constructor( + private readonly socialPostFeedService: SocialAccountFeedsService, + private readonly paginationService: PaginationService, + ) {} + + @Get(':social_account_id') + @ApiOperation({ + summary: `Get the social account feed`, + description: `Get a paginated result for the social account based on the applied filters`, + }) + @ApiOkResponse({ + description: `Paginated data set for the social account feed.`, + schema: { + properties: { + data: { + type: 'array', + items: { $ref: getSchemaPath(PlatformPostDto) }, + }, + meta: { + type: 'object', + properties: { + total: { + type: 'number', + description: 'Total number of items available.', + }, + offset: { + type: 'number', + description: 'Number of items skipped.', + }, + limit: { + type: 'number', + description: 'Maximum number of items returned.', + }, + next: { + type: 'string', + nullable: true, + description: 'URL to the next page of results, or null if none.', + example: 'https://api.postforme.dev/v1/items?offset=10&limit=10', + }, + }, + required: ['total', 'offset', 'limit', 'next'], + }, + }, + required: ['data', 'meta'], + }, + }) + @ApiResponse({ + status: 500, + description: `Internal server error when fetching social account feed.`, + }) + @ApiParam({ + name: 'social_account_id', + description: 'Social Account ID', + type: String, + required: true, + }) + getAccountFeed( + @Param() params: { social_account_id: string }, + @Query() query: PlatformPostQueryDto, + @User() user: RequestUser, + ): Promise> { + try { + return this.paginationService.createResponse( + this.socialPostFeedService.getPlatformPosts(query, user.projectId), + query, + ); + } catch (e) { + console.error(e); + throw new HttpException( + 'Internal server error', + HttpStatus.INTERNAL_SERVER_ERROR, + { + cause: e, + }, + ); + } + } + + @Get(':social_account_id/:platform_post_id') + @ApiResponse({ + status: 200, + description: 'Social account platform retrieved successfully.', + type: PlatformPostDto, + }) + @ApiResponse({ + status: 404, + description: 'Social account platform not found based on the given ID.', + }) + @ApiResponse({ + status: 500, + description: + 'Internal server error when fetching the social account platform.', + }) + @ApiOperation({ + summary: 'Get the social account platform post by platform ID', + }) + @ApiParam({ + name: 'social_account_id', + description: 'Social Account ID', + type: String, + required: true, + }) + @ApiParam({ + name: 'platform_post_id', + description: 'Platform Post ID', + type: String, + required: true, + }) + getAccountFeedPost( + @Param() params: { social_account_id: string; platform_post_id: string }, + @Query() query: PlatformPostQueryDto, + @User() user: RequestUser, + ): Promise { + try { + console.log(query, user); + + throw new HttpException('Not found', HttpStatus.NOT_FOUND); + } catch (e) { + console.error(e); + throw new HttpException( + 'Internal server error', + HttpStatus.INTERNAL_SERVER_ERROR, + { + cause: e, + }, + ); + } + } +} diff --git a/api/src/social-account-feeds/social-account-feeds.module.ts b/api/src/social-account-feeds/social-account-feeds.module.ts new file mode 100644 index 0000000..e7f825e --- /dev/null +++ b/api/src/social-account-feeds/social-account-feeds.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; + +import { PaginationModule } from '../pagination/pagination.module'; +import { SocialAccountFeedsController } from './social-account-feeds.controller'; +import { SocialAccountFeedsService } from './social-account-feeds.service'; + +@Module({ + imports: [PaginationModule], + controllers: [SocialAccountFeedsController], + providers: [SocialAccountFeedsService], +}) +export class SocialAccountFeedsModule {} diff --git a/api/src/social-account-feeds/social-account-feeds.service.ts b/api/src/social-account-feeds/social-account-feeds.service.ts new file mode 100644 index 0000000..e74262d --- /dev/null +++ b/api/src/social-account-feeds/social-account-feeds.service.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@nestjs/common'; +import { SupabaseService } from '../supabase/supabase.service'; + +import type { PaginatedRequestQuery } from '../pagination/pagination-request.interface'; + +import { PlatformPostQueryDto } from './dto/platform-post-query.dto'; +import { PlatformPostDto } from './dto/platform-post.dto'; + +@Injectable() +export class SocialAccountFeedsService { + constructor(private readonly supabaseService: SupabaseService) {} + + async getPlatformPosts( + queryParams: PlatformPostQueryDto, + projectId: string, + ): PaginatedRequestQuery { + console.log(queryParams, projectId); + await Promise.resolve(); + return { + data: [], + count: 0, + }; + } +} diff --git a/api/src/social-post-metrics/social-post-metrics.controller.ts b/api/src/social-post-metrics/social-post-metrics.controller.ts deleted file mode 100644 index e69de29..0000000 diff --git a/api/src/social-post-metrics/social-post-metrics.module.ts b/api/src/social-post-metrics/social-post-metrics.module.ts deleted file mode 100644 index e69de29..0000000 diff --git a/api/src/social-post-metrics/social-post-metrics.service.ts b/api/src/social-post-metrics/social-post-metrics.service.ts deleted file mode 100644 index e69de29..0000000 From fe614531dcca6e83dec8ce12bc369494dc95f876 Mon Sep 17 00:00:00 2001 From: matt rothenberger Date: Wed, 8 Oct 2025 21:51:04 -0400 Subject: [PATCH 3/6] vanity updates --- .../social-account-feeds/social-account-feeds.controller.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/social-account-feeds/social-account-feeds.controller.ts b/api/src/social-account-feeds/social-account-feeds.controller.ts index 699e05f..e05021d 100644 --- a/api/src/social-account-feeds/social-account-feeds.controller.ts +++ b/api/src/social-account-feeds/social-account-feeds.controller.ts @@ -39,7 +39,7 @@ export class SocialAccountFeedsController { @Get(':social_account_id') @ApiOperation({ - summary: `Get the social account feed`, + summary: `Get social account feed`, description: `Get a paginated result for the social account based on the applied filters`, }) @ApiOkResponse({ @@ -126,7 +126,7 @@ export class SocialAccountFeedsController { 'Internal server error when fetching the social account platform.', }) @ApiOperation({ - summary: 'Get the social account platform post by platform ID', + summary: 'Get post by platform ID', }) @ApiParam({ name: 'social_account_id', From 1bbd6fc4a1da4dc65f354c5b161700b3fda48a1f Mon Sep 17 00:00:00 2001 From: matt rothenberger Date: Wed, 8 Oct 2025 23:05:12 -0400 Subject: [PATCH 4/6] check in --- .../tiktok-business/tiktok-business.module.ts | 4 +++ .../tiktok-business.service.ts | 9 ++++++ ...al_provider_connection_pagination_data.sql | 15 +++++++++ supabase/supabase.types.ts | 32 +++++++++++++++++++ 4 files changed, 60 insertions(+) create mode 100644 api/src/tiktok-business/tiktok-business.module.ts create mode 100644 api/src/tiktok-business/tiktok-business.service.ts create mode 100644 supabase/migrations/20251009024528_social_provider_connection_pagination_data.sql diff --git a/api/src/tiktok-business/tiktok-business.module.ts b/api/src/tiktok-business/tiktok-business.module.ts new file mode 100644 index 0000000..ac39f85 --- /dev/null +++ b/api/src/tiktok-business/tiktok-business.module.ts @@ -0,0 +1,4 @@ +import { Module } from '@nestjs/common'; + +@Module({}) +export class TikTokBusinessModule {} diff --git a/api/src/tiktok-business/tiktok-business.service.ts b/api/src/tiktok-business/tiktok-business.service.ts new file mode 100644 index 0000000..3a2c53d --- /dev/null +++ b/api/src/tiktok-business/tiktok-business.service.ts @@ -0,0 +1,9 @@ +import { Injectable } from '@nestjs/common'; +import { PlatformPostDto } from 'src/social-account-feeds/dto/platform-post.dto'; + +@Injectable() +export class TikTokBusinessService { + async getAccountFeed(): Promise {} + + async getPost(): Promise {} +} diff --git a/supabase/migrations/20251009024528_social_provider_connection_pagination_data.sql b/supabase/migrations/20251009024528_social_provider_connection_pagination_data.sql new file mode 100644 index 0000000..d66b414 --- /dev/null +++ b/supabase/migrations/20251009024528_social_provider_connection_pagination_data.sql @@ -0,0 +1,15 @@ +-- Create oauth_data table +CREATE TABLE public.social_provider_connection_pagination_data( + id text DEFAULT nanoid('pgn') PRIMARY KEY, + provider_connection_id text NOT NULL REFERENCES public.social_provider_connections(id) ON DELETE CASCADE, + metadata jsonb NOT NULL, + created_at timestamp with time zone DEFAULT NOW(), + updated_at timestamp with time zone DEFAULT NOW() +); + +-- Create indexes for performance +CREATE INDEX idx_pagination_data_connection_id ON public.social_provider_connection_pagination_data(id, provider_connection_id); + +-- Enable RLS +ALTER TABLE public.social_provider_connection_pagination_data ENABLE ROW LEVEL SECURITY; + diff --git a/supabase/supabase.types.ts b/supabase/supabase.types.ts index 212b458..91fbd0f 100644 --- a/supabase/supabase.types.ts +++ b/supabase/supabase.types.ts @@ -491,6 +491,38 @@ export type Database = { }, ] } + social_provider_connection_pagination_data: { + Row: { + created_at: string | null + id: string + metadata: Json + provider_connection_id: string + updated_at: string | null + } + Insert: { + created_at?: string | null + id?: string + metadata: Json + provider_connection_id: string + updated_at?: string | null + } + Update: { + created_at?: string | null + id?: string + metadata?: Json + provider_connection_id?: string + updated_at?: string | null + } + Relationships: [ + { + foreignKeyName: "social_provider_connection_paginati_provider_connection_id_fkey" + columns: ["provider_connection_id"] + isOneToOne: false + referencedRelation: "social_provider_connections" + referencedColumns: ["id"] + }, + ] + } social_provider_connections: { Row: { access_token: string | null From 66f65e886da578729a65f8ba46e56c18d4b3dd8c Mon Sep 17 00:00:00 2001 From: matt rothenberger Date: Wed, 22 Oct 2025 02:24:29 +0900 Subject: [PATCH 5/6] check in --- .../pagination-platform-post-response.dto.ts | 12 ++ .../dto/platform-post-query.dto.ts | 44 +++--- .../social-account-feeds.controller.ts | 79 ++-------- .../social-account-feeds.service.ts | 142 ++++++++++++++++-- .../tiktok-business.service.ts | 5 +- 5 files changed, 183 insertions(+), 99 deletions(-) create mode 100644 api/src/social-account-feeds/dto/pagination-platform-post-response.dto.ts diff --git a/api/src/social-account-feeds/dto/pagination-platform-post-response.dto.ts b/api/src/social-account-feeds/dto/pagination-platform-post-response.dto.ts new file mode 100644 index 0000000..0c69ded --- /dev/null +++ b/api/src/social-account-feeds/dto/pagination-platform-post-response.dto.ts @@ -0,0 +1,12 @@ +import type { PlatformPostDto } from './platform-post.dto'; + +export interface PaginationPlatformPostMeta { + cursor: string; + limit: number; + next: string | null; +} + +export interface PaginatedPlatformPostResponse { + data: PlatformPostDto[]; + meta: PaginationPlatformPostMeta; +} diff --git a/api/src/social-account-feeds/dto/platform-post-query.dto.ts b/api/src/social-account-feeds/dto/platform-post-query.dto.ts index 04cbde7..fd5046f 100644 --- a/api/src/social-account-feeds/dto/platform-post-query.dto.ts +++ b/api/src/social-account-feeds/dto/platform-post-query.dto.ts @@ -1,58 +1,60 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsOptional, IsString } from 'class-validator'; -import { BasePaginatedQueryDto } from '../../pagination/base-paginated-query.dto'; +import { Type } from 'class-transformer'; +import { IsInt, IsOptional, IsString, Max, Min } from 'class-validator'; -export class PlatformPostQueryDto extends BasePaginatedQueryDto { +const DEFAULT_LIMIT = 50; +const MAX_LIMIT = 100; + +export class PlatformPostQueryDto { @ApiProperty({ - description: - 'Filter by Post for Me Social Postexternal ID. Multiple values imply OR logic (e.g., ?external_post_id=xxxxxx&external_post_id=yyyyyy).', + description: 'Number of items to return', + default: DEFAULT_LIMIT, required: false, - type: 'array', - items: { type: 'string' }, }) + @IsInt() + @Min(1) + @Max(MAX_LIMIT) @IsOptional() - external_post_id?: string[]; + @Type(() => Number) + limit: number = DEFAULT_LIMIT; @ApiProperty({ - description: - 'Filter by Post for Me Social Post id(s). Multiple values imply OR logic (e.g., ?social_post_id=sp_xxxxxx&social_post_id=sp_yyyyyy).', + description: 'Cursor identifying next page of results', required: false, - type: 'array', - items: { type: 'string' }, }) - @IsString({ each: true }) @IsOptional() - social_post_id?: string[]; + @Type(() => String) + cursor?: string | null; @ApiProperty({ description: - 'Filter by Post for Me Social Post Result id(s). Multiple values imply OR logic (e.g., ?social_post_id=spr_xxxxxx&social_post_id=spr_yyyyyy).', + 'Filter by Post for Me Social Postexternal ID. Multiple values imply OR logic (e.g., ?external_post_id=xxxxxx&external_post_id=yyyyyy).', required: false, type: 'array', items: { type: 'string' }, }) - @IsString({ each: true }) @IsOptional() - social_post_result_id?: string[]; + external_post_id?: string[]; @ApiProperty({ description: - 'Filter by Post for Me Social Post Account id(s). Multiple values imply OR logic (e.g., ?social_post_id=spc_xxxxxx&social_post_id=spc_yyyyyy).', + 'Filter by Post for Me Social Post id(s). Multiple values imply OR logic (e.g., ?social_post_id=sp_xxxxxx&social_post_id=sp_yyyyyy).', required: false, type: 'array', items: { type: 'string' }, }) @IsString({ each: true }) @IsOptional() - social_account_id?: string[]; + social_post_id?: string[]; @ApiProperty({ description: - 'Filter by Post for Me Social Postexternal ID. Multiple values imply OR logic (e.g., ?external_account_id=xxxxxx&external_account_id=yyyyyy).', + "Filter by the platform's id(s). Multiple values imply OR logic (e.g., ?social_post_id=spr_xxxxxx&social_post_id=spr_yyyyyy).", required: false, type: 'array', items: { type: 'string' }, }) + @IsString({ each: true }) @IsOptional() - external_account_id?: string[]; + platform_post_id?: string[]; } diff --git a/api/src/social-account-feeds/social-account-feeds.controller.ts b/api/src/social-account-feeds/social-account-feeds.controller.ts index e05021d..9a9b12c 100644 --- a/api/src/social-account-feeds/social-account-feeds.controller.ts +++ b/api/src/social-account-feeds/social-account-feeds.controller.ts @@ -17,7 +17,6 @@ import { } from '@nestjs/swagger'; import { PaginationService } from '../pagination/pagination.service'; -import type { PaginatedResponse } from '../pagination/pagination-response.interface'; import { User } from '../auth/user.decorator'; import type { RequestUser } from '../auth/user.interface'; @@ -26,6 +25,7 @@ import { Protect } from '../auth/protect.decorator'; import { PlatformPostDto } from './dto/platform-post.dto'; import { PlatformPostQueryDto } from './dto/platform-post-query.dto'; import { SocialAccountFeedsService } from './social-account-feeds.service'; +import { PaginatedPlatformPostResponse } from './dto/pagination-platform-post-response.dto'; @Controller('social-account-feeds') @ApiTags('Social Account Feeds') @@ -53,13 +53,9 @@ export class SocialAccountFeedsController { meta: { type: 'object', properties: { - total: { - type: 'number', - description: 'Total number of items available.', - }, - offset: { - type: 'number', - description: 'Number of items skipped.', + cursor: { + type: 'string', + description: 'Id representing the next page of items', }, limit: { type: 'number', @@ -69,10 +65,11 @@ export class SocialAccountFeedsController { type: 'string', nullable: true, description: 'URL to the next page of results, or null if none.', - example: 'https://api.postforme.dev/v1/items?offset=10&limit=10', + example: + 'https://api.postforme.dev/v1/items?cursor=pgn_xxxxx&limit=10', }, }, - required: ['total', 'offset', 'limit', 'next'], + required: ['cursor', 'limit', 'next'], }, }, required: ['data', 'meta'], @@ -92,12 +89,13 @@ export class SocialAccountFeedsController { @Param() params: { social_account_id: string }, @Query() query: PlatformPostQueryDto, @User() user: RequestUser, - ): Promise> { + ): Promise { try { - return this.paginationService.createResponse( - this.socialPostFeedService.getPlatformPosts(query, user.projectId), - query, - ); + return this.socialPostFeedService.getPlatformPosts({ + accountId: params.social_account_id, + queryParams: query, + projectId: user.projectId, + }); } catch (e) { console.error(e); throw new HttpException( @@ -110,54 +108,5 @@ export class SocialAccountFeedsController { } } - @Get(':social_account_id/:platform_post_id') - @ApiResponse({ - status: 200, - description: 'Social account platform retrieved successfully.', - type: PlatformPostDto, - }) - @ApiResponse({ - status: 404, - description: 'Social account platform not found based on the given ID.', - }) - @ApiResponse({ - status: 500, - description: - 'Internal server error when fetching the social account platform.', - }) - @ApiOperation({ - summary: 'Get post by platform ID', - }) - @ApiParam({ - name: 'social_account_id', - description: 'Social Account ID', - type: String, - required: true, - }) - @ApiParam({ - name: 'platform_post_id', - description: 'Platform Post ID', - type: String, - required: true, - }) - getAccountFeedPost( - @Param() params: { social_account_id: string; platform_post_id: string }, - @Query() query: PlatformPostQueryDto, - @User() user: RequestUser, - ): Promise { - try { - console.log(query, user); - - throw new HttpException('Not found', HttpStatus.NOT_FOUND); - } catch (e) { - console.error(e); - throw new HttpException( - 'Internal server error', - HttpStatus.INTERNAL_SERVER_ERROR, - { - cause: e, - }, - ); - } - } + // TODO: Discuss if endpoint to get single post is needed. Or if filter on paginated endpoint is enough. } diff --git a/api/src/social-account-feeds/social-account-feeds.service.ts b/api/src/social-account-feeds/social-account-feeds.service.ts index e74262d..a79e5d5 100644 --- a/api/src/social-account-feeds/social-account-feeds.service.ts +++ b/api/src/social-account-feeds/social-account-feeds.service.ts @@ -1,24 +1,146 @@ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { SupabaseService } from '../supabase/supabase.service'; -import type { PaginatedRequestQuery } from '../pagination/pagination-request.interface'; - import { PlatformPostQueryDto } from './dto/platform-post-query.dto'; -import { PlatformPostDto } from './dto/platform-post.dto'; +import { PaginatedPlatformPostResponse } from './dto/pagination-platform-post-response.dto'; +import { REQUEST } from '@nestjs/core'; +import { Request } from 'express'; @Injectable() export class SocialAccountFeedsService { - constructor(private readonly supabaseService: SupabaseService) {} + constructor( + private readonly supabaseService: SupabaseService, + @Inject(REQUEST) private request: Request, + ) {} + + generateNextUrl(queryParams: PlatformPostQueryDto): string { + const url = new URL( + `${this.request.protocol}://${this.request.get('host')}${this.request.path}`, + ); + + if (queryParams.cursor) { + url.searchParams.set('cursor', queryParams.cursor); + } + + url.searchParams.set('limit', String(queryParams.limit)); + + return url.toString(); + } + + async getPlatformPosts({ + accountId, + queryParams, + projectId, + }: { + accountId: string; + queryParams: PlatformPostQueryDto; + projectId: string; + }): Promise { + //Get Account + + const { data: account, error: accountError } = + await this.supabaseService.supabaseClient + .from('social_provider_connections') + .select() + .eq('id', accountId) + .eq('project_id', projectId) + .single(); + + if (accountError || !account) { + console.error(accountError); + throw new Error('Unable to fetch account'); + } + + const plaformPostIds: string[] = []; + const postResultsQuery = this.supabaseService.supabaseClient + .from('social_post_results') + .select( + ` + *, + social_posts!inner(external_id) + + `, + ) + .eq('provider_connection_id', accountId); + + if (queryParams.social_post_id) { + const values: string[] = []; + switch (true) { + case typeof queryParams.social_post_id === 'string': { + values.push(...(queryParams.social_post_id as string).split(',')); + break; + } + case Array.isArray(queryParams.social_post_id): + values.push(...queryParams.social_post_id); + break; + default: + values.push(queryParams.social_post_id); + break; + } + + postResultsQuery.in('social_post_id', values); + } + + if (queryParams.external_post_id) { + const values: string[] = []; + switch (true) { + case typeof queryParams.external_post_id === 'string': { + values.push(...(queryParams.external_post_id as string).split(',')); + break; + } + case Array.isArray(queryParams.external_post_id): + values.push(...queryParams.external_post_id); + break; + default: + values.push(queryParams.external_post_id); + break; + } + + postResultsQuery.in('social_posts.external_post_id', values); + } + + if (queryParams.social_post_id || queryParams.external_post_id) { + const { data: postResults } = await postResultsQuery; + + if (postResults && postResults.length > 0) { + const ids = postResults + ?.filter((pr) => pr.provider_post_id) + ?.map((pr) => pr.provider_post_id!); + plaformPostIds.push(...ids); + } + } + + if (queryParams.platform_post_id) { + const values: string[] = []; + switch (true) { + case typeof queryParams.platform_post_id === 'string': { + values.push(...(queryParams.platform_post_id as string).split(',')); + break; + } + case Array.isArray(queryParams.platform_post_id): + values.push(...queryParams.platform_post_id); + break; + default: + values.push(queryParams.platform_post_id); + break; + } + + plaformPostIds.push(...values); + } + + //Get App Credentials + + // Use service to get post data based on query params - async getPlatformPosts( - queryParams: PlatformPostQueryDto, - projectId: string, - ): PaginatedRequestQuery { console.log(queryParams, projectId); await Promise.resolve(); return { data: [], - count: 0, + meta: { + cursor: '', + limit: queryParams.limit, + next: this.generateNextUrl(queryParams), + }, }; } } diff --git a/api/src/tiktok-business/tiktok-business.service.ts b/api/src/tiktok-business/tiktok-business.service.ts index 3a2c53d..b71775b 100644 --- a/api/src/tiktok-business/tiktok-business.service.ts +++ b/api/src/tiktok-business/tiktok-business.service.ts @@ -1,9 +1,8 @@ import { Injectable } from '@nestjs/common'; -import { PlatformPostDto } from 'src/social-account-feeds/dto/platform-post.dto'; @Injectable() export class TikTokBusinessService { - async getAccountFeed(): Promise {} + async getAccountFeed() {} - async getPost(): Promise {} + async getPost() {} } From 4788b9851be0366f092995a4ce90b20803175d9f Mon Sep 17 00:00:00 2001 From: matt rothenberger Date: Fri, 7 Nov 2025 13:58:29 -0500 Subject: [PATCH 6/6] updating lock file --- bun.lockb | Bin 651792 -> 651792 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/bun.lockb b/bun.lockb index 41038eacb94f974840adbdaa01b2f6ecddff3cfc..44e417df27807d14e3ad35651ab8df37fa514be4 100755 GIT binary patch delta 83 zcmbR6M18^&^$m>2Sa#o^{AV-kF&jn*%k{WTCYZIo)qru2I+)q+8o>y}OhC*G#4JF} T3dC$c%nrmH+g&3#PtODZhNL6b delta 83 zcmbR6M18^&^$m>2Semqy|88bIX2S?!xgNL41hclc8Zhor2Q%AUBN&0035c12m<5Pg SftU@5*@2j2yK4mJ>6rjg@*ZLU