diff --git a/packages/discovery-provider/ddl/migrations/0047_ix_plays_user_item_date.sql b/packages/discovery-provider/ddl/migrations/0047_ix_plays_user_item_date.sql new file mode 100644 index 00000000000..16541974561 --- /dev/null +++ b/packages/discovery-provider/ddl/migrations/0047_ix_plays_user_item_date.sql @@ -0,0 +1,6 @@ +begin; +lock table plays in access exclusive mode; + +create index ix_plays_user_track_date on plays(user_id, play_item_id, created_at) where user_id is not null; + +commit; diff --git a/packages/trpc-server/.env b/packages/trpc-server/.env deleted file mode 100644 index b26b91b38ce..00000000000 --- a/packages/trpc-server/.env +++ /dev/null @@ -1,2 +0,0 @@ -# audius_elasticsearch_url=http://...:9200 -# audius_db_url=postgres://postgres:postgres@...:5432/audius_discovery diff --git a/packages/trpc-server/.gitignore b/packages/trpc-server/.gitignore new file mode 100644 index 00000000000..4c49bd78f1d --- /dev/null +++ b/packages/trpc-server/.gitignore @@ -0,0 +1 @@ +.env diff --git a/packages/trpc-server/README.md b/packages/trpc-server/README.md index b9e859856e1..13d3a826050 100644 --- a/packages/trpc-server/README.md +++ b/packages/trpc-server/README.md @@ -2,3 +2,53 @@ * run once: `bash test.sh run` * start watch mode: `bash test.sh` + +A global fixture set is used across all tests +and is populated into both postgres and elasticsearch. + +To add more fixtures, edit `test/_fixtures.ts` and restart `bash test.sh`. + +Be sure to follow ID range conventions: +* 100-199: users +* 200-299: tracks +* 300-399: playlists + +The upside of global fixture set is that a more complex dataset can be done once, and all tests can benefit from having tracks of all different visibility levels to test against. +This is the downside is that adding reposts can break existing tests that assert on repost count. +But I think it is worth it... and this is how rails fixtures work. + +## Running against stage / prod + +> in the future it would be nice to have stable sandbox + staging nodes +> so you can just pull down a staging .env file and start server +> for now it's manual style + +You can run server locally (`npm run dev`) and point it at a staging or sandbox database. + +First, `ssh some-sandbox` add this to `audius-docker-compose/discovery-provider/.env`. +These are only accessible via VPN so this is safe to do: + +``` +EXPOSE_POSTGRES: :5432 +EXPOSE_ELASTICSEARCH: :9200 +``` + +Now create `.env` file in this directory +(using the external IP of `some-sandbox`): + +``` +audius_elasticsearch_url=http://1.2.3.4:9200 +audius_db_url=postgres://postgres:postgres@1.2.3.4:5432/audius_discovery +``` + +Start server: + +``` +npm run dev +``` + +Finally start client but override `VITE_TRPC_ENDPOINT`: + +``` +VITE_TRPC_ENDPOINT=http://localhost:2022/trpc npm run web:prod +``` diff --git a/packages/trpc-server/docker-compose.yml b/packages/trpc-server/docker-compose.yml index 8962bd9d223..9be7c31c391 100644 --- a/packages/trpc-server/docker-compose.yml +++ b/packages/trpc-server/docker-compose.yml @@ -11,6 +11,11 @@ services: - 35764:5432 volumes: - ../discovery-provider/ddl:/ddl + healthcheck: + test: ["CMD-SHELL", "pg_isready"] + interval: 10s + timeout: 5s + retries: 5 elasticsearch: image: docker.elastic.co/elasticsearch/elasticsearch:8.10.2 diff --git a/packages/trpc-server/src/db-tables.ts b/packages/trpc-server/src/db-tables.ts index a96b81f8aad..c4454dd1613 100644 --- a/packages/trpc-server/src/db-tables.ts +++ b/packages/trpc-server/src/db-tables.ts @@ -1,810 +1,868 @@ /* - * This file was generated by a tool. - * Rerun sql-ts to regenerate this file. - */ +* This file was generated by a tool. +* Rerun sql-ts to regenerate this file. +*/ export interface AggregateDailyAppNameMetricRow { - applicationName: string - count: number - createdAt?: Date - id?: number - timestamp: Date - updatedAt?: Date + 'applicationName': string; + 'count': number; + 'createdAt'?: Date; + 'id'?: number; + 'timestamp': Date; + 'updatedAt'?: Date; } export interface AggregateDailyTotalUsersMetricRow { - count: number - createdAt?: Date - id?: number - timestamp: Date - updatedAt?: Date + 'count': number; + 'createdAt'?: Date; + 'id'?: number; + 'personalCount'?: number | null; + 'timestamp': Date; + 'updatedAt'?: Date; } export interface AggregateDailyUniqueUsersMetricRow { - count: number - createdAt?: Date - id?: number - summedCount?: number | null - timestamp: Date - updatedAt?: Date + 'count': number; + 'createdAt'?: Date; + 'id'?: number; + 'personalCount'?: number | null; + 'summedCount'?: number | null; + 'timestamp': Date; + 'updatedAt'?: Date; } export interface AggregateIntervalPlayRow { - createdAt?: Date | null - genre?: string | null - monthListenCounts?: string | null - trackId?: number | null - weekListenCounts?: string | null + 'createdAt'?: Date | null; + 'genre'?: string | null; + 'monthListenCounts'?: string | null; + 'trackId'?: number | null; + 'weekListenCounts'?: string | null; } export interface AggregateMonthlyAppNameMetricRow { - applicationName: string - count: number - createdAt?: Date - id?: number - timestamp: Date - updatedAt?: Date + 'applicationName': string; + 'count': number; + 'createdAt'?: Date; + 'id'?: number; + 'timestamp': Date; + 'updatedAt'?: Date; } export interface AggregateMonthlyPlayRow { - count: number - playItemId: number - timestamp?: Date + 'count': number; + 'playItemId': number; + 'timestamp'?: Date; } export interface AggregateMonthlyTotalUsersMetricRow { - count: number - createdAt?: Date - id?: number - timestamp: Date - updatedAt?: Date + 'count': number; + 'createdAt'?: Date; + 'id'?: number; + 'personalCount'?: number | null; + 'timestamp': Date; + 'updatedAt'?: Date; } export interface AggregateMonthlyUniqueUsersMetricRow { - count: number - createdAt?: Date - id?: number - summedCount?: number | null - timestamp: Date - updatedAt?: Date + 'count': number; + 'createdAt'?: Date; + 'id'?: number; + 'personalCount'?: number | null; + 'summedCount'?: number | null; + 'timestamp': Date; + 'updatedAt'?: Date; } export interface AggregatePlaylistRow { - isAlbum?: boolean | null - playlistId: number - repostCount?: number | null - saveCount?: number | null + 'isAlbum'?: boolean | null; + 'playlistId': number; + 'repostCount'?: number | null; + 'saveCount'?: number | null; } export interface AggregatePlayRow { - count?: string | null - playItemId: number + 'count'?: string | null; + 'playItemId': number; } export interface AggregateTrackRow { - repostCount?: number - saveCount?: number - trackId: number + 'repostCount'?: number; + 'saveCount'?: number; + 'trackId': number; } export interface AggregateUserRow { - albumCount?: string | null - followerCount?: string | null - followingCount?: string | null - playlistCount?: string | null - repostCount?: string | null - supporterCount?: number - supportingCount?: number - trackCount?: string | null - trackSaveCount?: string | null - userId: number + 'albumCount'?: string | null; + 'followerCount'?: string | null; + 'followingCount'?: string | null; + 'playlistCount'?: string | null; + 'repostCount'?: string | null; + 'supporterCount'?: number; + 'supportingCount'?: number; + 'trackCount'?: string | null; + 'trackSaveCount'?: string | null; + 'userId': number; } export interface AggregateUserTipRow { - amount: string - receiverUserId: number - senderUserId: number -} -export interface AlembicVersionRow { - versionNum: string + 'amount': string; + 'receiverUserId': number; + 'senderUserId': number; } export interface AppNameMetricRow { - applicationName: string - count: number - createdAt?: Date - id?: string - ip?: string | null - timestamp?: Date - updatedAt?: Date + 'applicationName': string; + 'count': number; + 'createdAt'?: Date; + 'id'?: string; + 'ip'?: string | null; + 'timestamp'?: Date; + 'updatedAt'?: Date; } export interface AppNameMetricsAllTimeRow { - count?: string | null - name?: string | null + 'count'?: string | null; + 'name'?: string | null; } export interface AppNameMetricsTrailingMonthRow { - count?: string | null - name?: string | null + 'count'?: string | null; + 'name'?: string | null; } export interface AppNameMetricsTrailingWeekRow { - count?: string | null - name?: string | null + 'count'?: string | null; + 'name'?: string | null; } export interface AssociatedWalletRow { - blockhash: string - blocknumber: number - chain: wallet_chain - id?: number - isCurrent: boolean - isDelete: boolean - userId: number - wallet: string + 'blockhash': string; + 'blocknumber': number; + 'chain': wallet_chain; + 'id'?: number; + 'isCurrent': boolean; + 'isDelete': boolean; + 'userId': number; + 'wallet': string; } export interface AudioTransactionsHistoryRow { - balance: string - change: string - createdAt?: Date - method: string - signature: string - slot: number - transactionCreatedAt: Date - transactionType: string - txMetadata?: string | null - updatedAt?: Date - userBank: string + 'balance': string; + 'change': string; + 'createdAt'?: Date; + 'method': string; + 'signature': string; + 'slot': number; + 'transactionCreatedAt': Date; + 'transactionType': string; + 'txMetadata'?: string | null; + 'updatedAt'?: Date; + 'userBank': string; } export interface AudiusDataTxRow { - signature: string - slot: number + 'signature': string; + 'slot': number; } export interface BlockRow { - blockhash: string - isCurrent?: boolean | null - number?: number | null - parenthash?: string | null + 'blockhash': string; + 'isCurrent'?: boolean | null; + 'number'?: number | null; + 'parenthash'?: string | null; } export interface ChallengeDisbursementRow { - amount: string - challengeId: string - signature: string - slot: number - specifier: string - userId: number + 'amount': string; + 'challengeId': string; + 'createdAt'?: Date | null; + 'signature': string; + 'slot': number; + 'specifier': string; + 'userId': number; } export interface ChallengeListenStreakRow { - lastListenDate?: Date | null - listenStreak: number - userId?: number + 'lastListenDate'?: Date | null; + 'listenStreak': number; + 'userId'?: number; } export interface ChallengeProfileCompletionRow { - favorites: boolean - follows: boolean - profileCoverPhoto: boolean - profileDescription: boolean - profileName: boolean - profilePicture: boolean - reposts: boolean - userId?: number + 'favorites': boolean; + 'follows': boolean; + 'profileCoverPhoto': boolean; + 'profileDescription': boolean; + 'profileName': boolean; + 'profilePicture': boolean; + 'reposts': boolean; + 'userId'?: number; } export interface ChallengeRow { - active: boolean - amount: string - id: string - startingBlock?: number | null - stepCount?: number | null - type: challengetype + 'active': boolean; + 'amount': string; + 'cooldownDays'?: number | null; + 'id': string; + 'startingBlock'?: number | null; + 'stepCount'?: number | null; + 'type': challengetype; + 'weeklyPool'?: number | null; } export interface ChatRow { - chatId: string - createdAt: Date - lastMessage?: string | null - lastMessageAt: Date + 'chatId': string; + 'createdAt': Date; + 'lastMessage'?: string | null; + 'lastMessageAt': Date; } export interface ChatBanRow { - isBanned: boolean - updatedAt: Date - userId: number + 'isBanned': boolean; + 'updatedAt': Date; + 'userId': number; } export interface ChatBlockedUserRow { - blockeeUserId: number - blockerUserId: number - createdAt?: Date + 'blockeeUserId': number; + 'blockerUserId': number; + 'createdAt'?: Date; } export interface ChatMemberRow { - chatId: string - clearedHistoryAt?: Date | null - createdAt: Date - inviteCode: string - invitedByUserId: number - lastActiveAt?: Date | null - unreadCount?: number - userId: number + 'chatId': string; + 'clearedHistoryAt'?: Date | null; + 'createdAt': Date; + 'inviteCode': string; + 'invitedByUserId': number; + 'lastActiveAt'?: Date | null; + 'unreadCount'?: number; + 'userId': number; } export interface ChatMessageRow { - chatId: string - ciphertext: string - createdAt: Date - messageId: string - userId: number + 'chatId': string; + 'ciphertext': string; + 'createdAt': Date; + 'messageId': string; + 'userId': number; } export interface ChatMessageReactionRow { - createdAt?: Date - messageId: string - reaction: string - updatedAt?: Date - userId: number + 'createdAt'?: Date; + 'messageId': string; + 'reaction': string; + 'updatedAt'?: Date; + 'userId': number; } export interface ChatPermissionRow { - permits?: string | null - updatedAt?: Date - userId: number + 'permits'?: string | null; + 'updatedAt'?: Date; + 'userId': number; } export interface CidDataRow { - cid: string - data?: any | null - type?: string | null + 'cid': string; + 'data'?: any | null; + 'type'?: string | null; +} +export interface DashboardWalletUserRow { + 'blockhash'?: string | null; + 'blocknumber'?: number | null; + 'createdAt': Date; + 'isDelete'?: boolean; + 'txhash': string; + 'updatedAt': Date; + 'userId': number; + 'wallet': string; } export interface DelistStatusCursorRow { - createdAt: Date - entity: delist_entity - host: string + 'createdAt': Date; + 'entity': delist_entity; + 'host': string; } export interface DeveloperAppRow { - address: string - blockhash?: string | null - blocknumber?: number | null - createdAt: Date - description?: string | null - isCurrent: boolean - isDelete?: boolean - isPersonalAccess?: boolean - name: string - txhash: string - updatedAt: Date - userId?: number | null + 'address': string; + 'blockhash'?: string | null; + 'blocknumber'?: number | null; + 'createdAt': Date; + 'description'?: string | null; + 'isCurrent': boolean; + 'isDelete'?: boolean; + 'isPersonalAccess'?: boolean; + 'name': string; + 'txhash': string; + 'updatedAt': Date; + 'userId'?: number | null; } export interface EthBlockRow { - createdAt?: Date - lastScannedBlock?: number - updatedAt?: Date + 'createdAt'?: Date; + 'lastScannedBlock'?: number; + 'updatedAt'?: Date; } export interface FollowRow { - blockhash?: string | null - blocknumber?: number | null - createdAt: Date - followeeUserId: number - followerUserId: number - isCurrent: boolean - isDelete: boolean - slot?: number | null - txhash?: string + 'blockhash'?: string | null; + 'blocknumber'?: number | null; + 'createdAt': Date; + 'followeeUserId': number; + 'followerUserId': number; + 'isCurrent': boolean; + 'isDelete': boolean; + 'slot'?: number | null; + 'txhash'?: string; } export interface GrantRow { - blockhash?: string | null - blocknumber?: number | null - createdAt: Date - granteeAddress: string - isApproved?: boolean - isCurrent: boolean - isRevoked?: boolean - txhash: string - updatedAt: Date - userId: number + 'blockhash'?: string | null; + 'blocknumber'?: number | null; + 'createdAt': Date; + 'granteeAddress': string; + 'isApproved'?: boolean; + 'isCurrent': boolean; + 'isRevoked'?: boolean; + 'txhash': string; + 'updatedAt': Date; + 'userId': number; } export interface HourlyPlayCountRow { - hourlyTimestamp: Date - playCount: number + 'hourlyTimestamp': Date; + 'playCount': number; } export interface IndexingCheckpointRow { - lastCheckpoint: number - signature?: string | null - tablename: string + 'lastCheckpoint': number; + 'signature'?: string | null; + 'tablename': string; } export interface MilestoneRow { - blocknumber?: number | null - id: number - name: string - slot?: number | null - threshold: number - timestamp: Date + 'blocknumber'?: number | null; + 'id': number; + 'name': string; + 'slot'?: number | null; + 'threshold': number; + 'timestamp': Date; } export interface NotificationRow { - blocknumber?: number | null - data?: any | null - groupId: string - id?: number - slot?: number | null - specifier: string - timestamp: Date - type: string - typeV2?: string | null - userIds?: any | null + 'blocknumber'?: number | null; + 'data'?: any | null; + 'groupId': string; + 'id'?: number; + 'slot'?: number | null; + 'specifier': string; + 'timestamp': Date; + 'type': string; + 'typeV2'?: string | null; + 'userIds'?: any | null; } export interface NotificationSeenRow { - blockhash?: string | null - blocknumber?: number | null - seenAt: Date - txhash?: string | null - userId: number + 'blockhash'?: string | null; + 'blocknumber'?: number | null; + 'seenAt': Date; + 'txhash'?: string | null; + 'userId': number; +} +export interface PaymentRouterTxRow { + 'createdAt': Date; + 'signature': string; + 'slot': number; } export interface PgStatStatementRow { - blkReadTime?: number | null - blkWriteTime?: number | null - calls?: string | null - dbid?: any | null - localBlksDirtied?: string | null - localBlksHit?: string | null - localBlksRead?: string | null - localBlksWritten?: string | null - maxTime?: number | null - meanTime?: number | null - minTime?: number | null - query?: string | null - queryid?: string | null - rows?: string | null - sharedBlksDirtied?: string | null - sharedBlksHit?: string | null - sharedBlksRead?: string | null - sharedBlksWritten?: string | null - stddevTime?: number | null - tempBlksRead?: string | null - tempBlksWritten?: string | null - totalTime?: number | null - userid?: any | null + 'blkReadTime'?: number | null; + 'blkWriteTime'?: number | null; + 'calls'?: string | null; + 'dbid'?: any | null; + 'jitEmissionCount'?: string | null; + 'jitEmissionTime'?: number | null; + 'jitFunctions'?: string | null; + 'jitGenerationTime'?: number | null; + 'jitInliningCount'?: string | null; + 'jitInliningTime'?: number | null; + 'jitOptimizationCount'?: string | null; + 'jitOptimizationTime'?: number | null; + 'localBlksDirtied'?: string | null; + 'localBlksHit'?: string | null; + 'localBlksRead'?: string | null; + 'localBlksWritten'?: string | null; + 'maxExecTime'?: number | null; + 'maxPlanTime'?: number | null; + 'meanExecTime'?: number | null; + 'meanPlanTime'?: number | null; + 'minExecTime'?: number | null; + 'minPlanTime'?: number | null; + 'plans'?: string | null; + 'query'?: string | null; + 'queryid'?: string | null; + 'rows'?: string | null; + 'sharedBlksDirtied'?: string | null; + 'sharedBlksHit'?: string | null; + 'sharedBlksRead'?: string | null; + 'sharedBlksWritten'?: string | null; + 'stddevExecTime'?: number | null; + 'stddevPlanTime'?: number | null; + 'tempBlkReadTime'?: number | null; + 'tempBlkWriteTime'?: number | null; + 'tempBlksRead'?: string | null; + 'tempBlksWritten'?: string | null; + 'toplevel'?: boolean | null; + 'totalExecTime'?: number | null; + 'totalPlanTime'?: number | null; + 'userid'?: any | null; + 'walBytes'?: string | null; + 'walFpi'?: string | null; + 'walRecords'?: string | null; +} +export interface PgStatStatementsInfoRow { + 'dealloc'?: string | null; + 'statsReset'?: Date | null; } export interface PlaylistRouteRow { - blockhash: string - blocknumber: number - collisionId: number - isCurrent: boolean - ownerId: number - playlistId: number - slug: string - titleSlug: string - txhash: string + 'blockhash': string; + 'blocknumber': number; + 'collisionId': number; + 'isCurrent': boolean; + 'ownerId': number; + 'playlistId': number; + 'slug': string; + 'titleSlug': string; + 'txhash': string; } export interface PlaylistSeenRow { - blockhash?: string | null - blocknumber?: number | null - isCurrent: boolean - playlistId: number - seenAt: Date - txhash?: string | null - userId: number + 'blockhash'?: string | null; + 'blocknumber'?: number | null; + 'isCurrent': boolean; + 'playlistId': number; + 'seenAt': Date; + 'txhash'?: string | null; + 'userId': number; } export interface PlaylistRow { - blockhash?: string | null - blocknumber?: number | null - createdAt: Date - description?: string | null - isAlbum: boolean - isCurrent: boolean - isDelete: boolean - isImageAutogenerated?: boolean - isPrivate: boolean - lastAddedTo?: Date | null - metadataMultihash?: string | null - permalink?: string | null - playlistContents: any - playlistId: number - playlistImageMultihash?: string | null - playlistImageSizesMultihash?: string | null - playlistName?: string | null - playlistOwnerId: number - slot?: number | null - slug?: string | null - txhash?: string - upc?: string | null - updatedAt: Date + 'blockhash'?: string | null; + 'blocknumber'?: number | null; + 'createdAt': Date; + 'description'?: string | null; + 'isAlbum': boolean; + 'isCurrent': boolean; + 'isDelete': boolean; + 'isImageAutogenerated'?: boolean; + 'isPrivate': boolean; + 'lastAddedTo'?: Date | null; + 'metadataMultihash'?: string | null; + 'playlistContents': any; + 'playlistId': number; + 'playlistImageMultihash'?: string | null; + 'playlistImageSizesMultihash'?: string | null; + 'playlistName'?: string | null; + 'playlistOwnerId': number; + 'slot'?: number | null; + 'txhash'?: string; + 'upc'?: string | null; + 'updatedAt': Date; } export interface PlayRow { - city?: string | null - country?: string | null - createdAt?: Date - id?: number - playItemId: number - region?: string | null - signature?: string | null - slot?: number | null - source?: string | null - updatedAt?: Date - userId?: number | null + 'city'?: string | null; + 'country'?: string | null; + 'createdAt'?: Date; + 'id'?: number; + 'playItemId': number; + 'region'?: string | null; + 'signature'?: string | null; + 'slot'?: number | null; + 'source'?: string | null; + 'updatedAt'?: Date; + 'userId'?: number | null; +} +export interface PubkeyRow { + 'pubkey'?: string | null; + 'wallet': string; } export interface ReactionRow { - id?: number - reactedTo: string - reactionType: string - reactionValue: number - senderWallet: string - slot: number - timestamp: Date - txSignature?: string | null + 'id'?: number; + 'reactedTo': string; + 'reactionType': string; + 'reactionValue': number; + 'senderWallet': string; + 'slot': number; + 'timestamp': Date; + 'txSignature'?: string | null; } export interface RelatedArtistRow { - createdAt?: Date - relatedArtistUserId: number - score: number - userId: number + 'createdAt'?: Date; + 'relatedArtistUserId': number; + 'score': number; + 'userId': number; } export interface RemixeRow { - childTrackId: number - parentTrackId: number + 'childTrackId': number; + 'parentTrackId': number; } export interface RepostRow { - blockhash?: string | null - blocknumber?: number | null - createdAt: Date - isCurrent: boolean - isDelete: boolean - isRepostOfRepost?: boolean - repostItemId: number - repostType: reposttype - slot?: number | null - txhash?: string - userId: number + 'blockhash'?: string | null; + 'blocknumber'?: number | null; + 'createdAt': Date; + 'isCurrent': boolean; + 'isDelete': boolean; + 'isRepostOfRepost'?: boolean; + 'repostItemId': number; + 'repostType': reposttype; + 'slot'?: number | null; + 'txhash'?: string; + 'userId': number; } export interface RevertBlockRow { - blocknumber: number - prevRecords: any + 'blocknumber': number; + 'prevRecords': any; } export interface RewardManagerTxRow { - createdAt: Date - signature: string - slot: number + 'createdAt': Date; + 'signature': string; + 'slot': number; } export interface RewardsManagerBackfillTxRow { - createdAt: Date - signature: string - slot: number + 'createdAt': Date; + 'signature': string; + 'slot': number; } export interface RouteMetricRow { - count: number - createdAt?: Date - id?: string - ip?: string | null - queryString?: string - routePath: string - timestamp?: Date - updatedAt?: Date - version: string + 'count': number; + 'createdAt'?: Date; + 'id'?: string; + 'ip'?: string | null; + 'queryString'?: string; + 'routePath': string; + 'timestamp'?: Date; + 'updatedAt'?: Date; + 'version': string; } export interface RouteMetricsAllTimeRow { - count?: string | null - uniqueCount?: string | null + 'count'?: string | null; + 'uniqueCount'?: string | null; } export interface RouteMetricsDayBucketRow { - count?: string | null - time?: Date | null - uniqueCount?: string | null + 'count'?: string | null; + 'time'?: Date | null; + 'uniqueCount'?: string | null; } export interface RouteMetricsMonthBucketRow { - count?: string | null - time?: Date | null - uniqueCount?: string | null + 'count'?: string | null; + 'time'?: Date | null; + 'uniqueCount'?: string | null; } export interface RouteMetricsTrailingMonthRow { - count?: string | null - uniqueCount?: string | null + 'count'?: string | null; + 'uniqueCount'?: string | null; } export interface RouteMetricsTrailingWeekRow { - count?: string | null - uniqueCount?: string | null + 'count'?: string | null; + 'uniqueCount'?: string | null; } export interface RpcCursorRow { - relayedAt: Date - relayedBy: string + 'relayedAt': Date; + 'relayedBy': string; } export interface RpcErrorRow { - errorCount?: number - errorText: string - lastAttempt: Date - rpcLogJson: any - sig: string + 'errorCount'?: number; + 'errorText': string; + 'lastAttempt': Date; + 'rpcLogJson': any; + 'sig': string; } export interface RpcLogRow { - appliedAt: Date - fromWallet: string - relayedAt: Date - relayedBy: string - rpc: Object - sig: string + 'appliedAt': Date; + 'fromWallet': string; + 'relayedAt': Date; + 'relayedBy': string; + 'rpc': Object; + 'sig': string; +} +export interface RpclogRow { + 'cuid': string; + 'jetstreamSeq'?: number | null; + 'method'?: string | null; + 'params'?: any | null; + 'wallet'?: string | null; } export interface SaveRow { - blockhash?: string | null - blocknumber?: number | null - createdAt: Date - isCurrent: boolean - isDelete: boolean - isSaveOfRepost?: boolean - saveItemId: number - saveType: savetype - slot?: number | null - txhash?: string - userId: number + 'blockhash'?: string | null; + 'blocknumber'?: number | null; + 'createdAt': Date; + 'isCurrent': boolean; + 'isDelete': boolean; + 'isSaveOfRepost'?: boolean; + 'saveItemId': number; + 'saveType': savetype; + 'slot'?: number | null; + 'txhash'?: string; + 'userId': number; } export interface SchemaMigrationRow { - version: string + 'version': string; } export interface SchemaVersionRow { - appliedAt?: Date - fileName: string - md5?: string | null + 'appliedAt'?: Date; + 'fileName': string; + 'md5'?: string | null; +} +export interface SequelizeMetaRow { + 'name': string; } export interface SkippedTransactionRow { - blockhash: string - blocknumber: number - createdAt?: Date - id?: number - level?: skippedtransactionlevel - txhash: string - updatedAt?: Date + 'blockhash': string; + 'blocknumber': number; + 'createdAt'?: Date; + 'id'?: number; + 'level'?: skippedtransactionlevel | null; + 'txhash': string; + 'updatedAt'?: Date; } export interface SplTokenBackfillTxRow { - createdAt: Date - lastScannedSlot?: number - signature: string - updatedAt: Date + 'createdAt': Date; + 'lastScannedSlot'?: number; + 'signature': string; + 'updatedAt': Date; } export interface SplTokenTxRow { - createdAt?: Date - lastScannedSlot: number - signature: string - updatedAt?: Date + 'createdAt'?: Date; + 'lastScannedSlot': number; + 'signature': string; + 'updatedAt'?: Date; } export interface StemRow { - childTrackId: number - parentTrackId: number + 'childTrackId': number; + 'parentTrackId': number; } export interface SubscriptionRow { - blockhash?: string | null - blocknumber?: number | null - createdAt?: Date - isCurrent: boolean - isDelete: boolean - subscriberId: number - txhash?: string - userId: number + 'blockhash'?: string | null; + 'blocknumber'?: number | null; + 'createdAt'?: Date; + 'isCurrent': boolean; + 'isDelete': boolean; + 'subscriberId': number; + 'txhash'?: string; + 'userId': number; } export interface SupporterRankUpRow { - rank: number - receiverUserId: number - senderUserId: number - slot: number + 'rank': number; + 'receiverUserId': number; + 'senderUserId': number; + 'slot': number; } export interface TagTrackUserRow { - ownerId?: number | null - tag?: string | null - trackId?: number | null + 'ownerId'?: number | null; + 'tag'?: string | null; + 'trackId'?: number | null; } export interface TrackDelistStatuseRow { - createdAt: Date - delisted: boolean - ownerId: number - reason: delist_track_reason - trackCid: string - trackId: number + 'createdAt': Date; + 'delisted': boolean; + 'ownerId': number; + 'reason': delist_track_reason; + 'trackCid': string; + 'trackId': number; } export interface TrackPriceHistoryRow { - blockTimestamp: Date - blocknumber: number - createdAt?: Date - splits: any - totalPriceCents: string - trackId: number + 'blockTimestamp': Date; + 'blocknumber': number; + 'createdAt'?: Date; + 'splits': any; + 'totalPriceCents': string; + 'trackId': number; } export interface TrackRouteRow { - blockhash: string - blocknumber: number - collisionId: number - isCurrent: boolean - ownerId: number - slug: string - titleSlug: string - trackId: number - txhash: string + 'blockhash': string; + 'blocknumber': number; + 'collisionId': number; + 'isCurrent': boolean; + 'ownerId': number; + 'slug': string; + 'titleSlug': string; + 'trackId': number; + 'txhash': string; } export interface TrackTrendingScoreRow { - createdAt: Date - genre?: string | null - score: number - timeRange: string - trackId: number - type: string - version: string + 'createdAt': Date; + 'genre'?: string | null; + 'score': number; + 'timeRange': string; + 'trackId': number; + 'type': string; + 'version': string; } export interface TrackRow { - aiAttributionUserId?: number | null - audioUploadId?: string | null - blockhash?: string | null - blocknumber?: number | null - coverArt?: string | null - coverArtSizes?: string | null - createDate?: string | null - createdAt: Date - creditsSplits?: string | null - description?: string | null - download?: any | null - duration?: number | null - fieldVisibility?: any | null - fileType?: string | null - genre?: string | null - isAvailable?: boolean - isCurrent: boolean - isDelete: boolean - isPlaylistUpload?: boolean - isPremium?: boolean - isUnlisted?: boolean - isrc?: string | null - iswc?: string | null - license?: string | null - metadataMultihash?: string | null - mood?: string | null - ownerId: number - premiumConditions?: any | null - previewCid?: string | null - previewStartSeconds?: number | null - releaseDate?: string | null - remixOf?: any | null - routeId?: string | null - slot?: number | null - stemOf?: any | null - tags?: string | null - title?: string | null - trackCid?: string | null - trackId: number - trackSegments: any - txhash?: string - updatedAt: Date + 'aiAttributionUserId'?: number | null; + 'audioUploadId'?: string | null; + 'blockhash'?: string | null; + 'blocknumber'?: number | null; + 'coverArt'?: string | null; + 'coverArtSizes'?: string | null; + 'createDate'?: string | null; + 'createdAt': Date; + 'creditsSplits'?: string | null; + 'description'?: string | null; + 'download'?: any | null; + 'duration'?: number | null; + 'fieldVisibility'?: any | null; + 'fileType'?: string | null; + 'genre'?: string | null; + 'isAvailable'?: boolean; + 'isCurrent': boolean; + 'isDelete': boolean; + 'isPlaylistUpload'?: boolean; + 'isPremium'?: boolean; + 'isScheduledRelease'?: boolean; + 'isUnlisted'?: boolean; + 'isrc'?: string | null; + 'iswc'?: string | null; + 'license'?: string | null; + 'metadataMultihash'?: string | null; + 'mood'?: string | null; + 'ownerId': number; + 'premiumConditions'?: any | null; + 'previewCid'?: string | null; + 'previewStartSeconds'?: number | null; + 'releaseDate'?: Date | null; + 'remixOf'?: any | null; + 'routeId'?: string | null; + 'slot'?: number | null; + 'stemOf'?: any | null; + 'tags'?: string | null; + 'title'?: string | null; + 'trackCid'?: string | null; + 'trackId': number; + 'trackSegments'?: any; + 'txhash'?: string; + 'updatedAt': Date; } export interface TrendingParamRow { - genre?: string | null - karma?: string | null - ownerFollowerCount?: string | null - ownerId?: number | null - playCount?: string | null - repostCount?: number | null - repostMonthCount?: string | null - repostWeekCount?: string | null - repostYearCount?: string | null - saveCount?: number | null - saveMonthCount?: string | null - saveWeekCount?: string | null - saveYearCount?: string | null - trackId?: number | null + 'genre'?: string | null; + 'karma'?: string | null; + 'ownerFollowerCount'?: string | null; + 'ownerId'?: number | null; + 'playCount'?: string | null; + 'repostCount'?: number | null; + 'repostMonthCount'?: string | null; + 'repostWeekCount'?: string | null; + 'repostYearCount'?: string | null; + 'saveCount'?: number | null; + 'saveMonthCount'?: string | null; + 'saveWeekCount'?: string | null; + 'saveYearCount'?: string | null; + 'trackId'?: number | null; } export interface TrendingResultRow { - id?: string | null - rank: number - type: string - userId: number - version: string - week: Date + 'id'?: string | null; + 'rank': number; + 'type': string; + 'userId': number; + 'version': string; + 'week': Date; } export interface UsdcPurchaseRow { - amount: string - buyerUserId: number - contentId: number - contentType: usdc_purchase_content_type - createdAt?: Date - extraAmount?: string - sellerUserId: number - signature: string - slot: number - updatedAt?: Date + 'amount': string; + 'buyerUserId': number; + 'contentId': number; + 'contentType': usdc_purchase_content_type; + 'createdAt'?: Date; + 'extraAmount'?: string; + 'sellerUserId': number; + 'signature': string; + 'slot': number; + 'updatedAt'?: Date; } export interface UsdcTransactionsHistoryRow { - balance: string - change: string - createdAt?: Date - method: string - signature: string - slot: number - transactionCreatedAt: Date - transactionType: string - txMetadata?: string | null - updatedAt?: Date - userBank: string + 'balance': string; + 'change': string; + 'createdAt'?: Date; + 'method': string; + 'signature': string; + 'slot': number; + 'transactionCreatedAt': Date; + 'transactionType': string; + 'txMetadata'?: string | null; + 'updatedAt'?: Date; + 'userBank': string; } export interface UsdcUserBankAccountRow { - bankAccount: string - createdAt?: Date - ethereumAddress: string - signature: string + 'bankAccount': string; + 'createdAt'?: Date; + 'ethereumAddress': string; + 'signature': string; } export interface UserBalanceChangeRow { - blocknumber: number - createdAt?: Date - currentBalance: string - previousBalance: string - updatedAt?: Date - userId?: number + 'blocknumber': number; + 'createdAt'?: Date; + 'currentBalance': string; + 'previousBalance': string; + 'updatedAt'?: Date; + 'userId'?: number; } export interface UserBalanceRow { - associatedSolWalletsBalance?: string - associatedWalletsBalance?: string - balance: string - createdAt?: Date - updatedAt?: Date - userId?: number - waudio?: string | null + 'associatedSolWalletsBalance'?: string; + 'associatedWalletsBalance'?: string; + 'balance': string; + 'createdAt'?: Date; + 'updatedAt'?: Date; + 'userId'?: number; + 'waudio'?: string | null; } export interface UserBankAccountRow { - bankAccount: string - createdAt: Date - ethereumAddress: string - signature: string + 'bankAccount': string; + 'createdAt': Date; + 'ethereumAddress': string; + 'signature': string; } export interface UserBankBackfillTxRow { - createdAt: Date - signature: string - slot: number + 'createdAt': Date; + 'signature': string; + 'slot': number; } export interface UserBankTxRow { - createdAt: Date - signature: string - slot: number + 'createdAt': Date; + 'signature': string; + 'slot': number; } export interface UserChallengeRow { - challengeId: string - completedBlocknumber?: number | null - currentStepCount?: number | null - isComplete: boolean - specifier: string - userId: number + 'amount'?: number; + 'challengeId': string; + 'completedBlocknumber'?: number | null; + 'createdAt'?: Date; + 'currentStepCount'?: number | null; + 'isComplete': boolean; + 'specifier': string; + 'userId': number; } export interface UserDelistStatuseRow { - createdAt: Date - delisted: boolean - reason: delist_user_reason - userId: number + 'createdAt': Date; + 'delisted': boolean; + 'reason': delist_user_reason; + 'userId': number; } export interface UserEventRow { - blockhash?: string | null - blocknumber?: number | null - id?: number - isCurrent: boolean - isMobileUser?: boolean - referrer?: number | null - slot?: number | null - userId: number + 'blockhash'?: string | null; + 'blocknumber'?: number | null; + 'id'?: number; + 'isCurrent': boolean; + 'isMobileUser'?: boolean; + 'referrer'?: number | null; + 'slot'?: number | null; + 'userId': number; } export interface UserListeningHistoryRow { - listeningHistory: any - userId?: number + 'listeningHistory': any; + 'userId'?: number; } export interface UserPubkeyRow { - pubkeyBase64: string - userId: number + 'pubkeyBase64': string; + 'userId': number; } export interface UserTipRow { - amount: string - createdAt?: Date - receiverUserId: number - senderUserId: number - signature: string - slot: number - updatedAt?: Date + 'amount': string; + 'createdAt'?: Date; + 'receiverUserId': number; + 'senderUserId': number; + 'signature': string; + 'slot': number; + 'updatedAt'?: Date; } export interface UserRow { - allowAiAttribution?: boolean - artistPickTrackId?: number | null - bio?: string | null - blockhash?: string | null - blocknumber?: number | null - coverPhoto?: string | null - coverPhotoSizes?: string | null - createdAt?: Date - creatorNodeEndpoint?: string | null - handle?: string | null - handleLc?: string | null - hasCollectibles?: boolean - isAvailable?: boolean - isCurrent: boolean - isDeactivated?: boolean - isStorageV2?: boolean - isVerified?: boolean - location?: string | null - metadataMultihash?: string | null - name?: string | null - playlistLibrary?: any | null - primaryId?: number | null - profilePicture?: string | null - profilePictureSizes?: string | null - replicaSetUpdateSigner?: string | null - secondaryIds?: any | null - slot?: number | null - txhash?: string - updatedAt?: Date - userAuthorityAccount?: string | null - userId: number - userStorageAccount?: string | null - wallet?: string | null + 'allowAiAttribution'?: boolean; + 'artistPickTrackId'?: number | null; + 'bio'?: string | null; + 'blockhash'?: string | null; + 'blocknumber'?: number | null; + 'coverPhoto'?: string | null; + 'coverPhotoSizes'?: string | null; + 'createdAt'?: Date; + 'creatorNodeEndpoint'?: string | null; + 'handle'?: string | null; + 'handleLc'?: string | null; + 'hasCollectibles'?: boolean; + 'isAvailable'?: boolean; + 'isCurrent': boolean; + 'isDeactivated'?: boolean; + 'isStorageV2'?: boolean; + 'isVerified'?: boolean; + 'location'?: string | null; + 'metadataMultihash'?: string | null; + 'name'?: string | null; + 'playlistLibrary'?: any | null; + 'primaryId'?: number | null; + 'profilePicture'?: string | null; + 'profilePictureSizes'?: string | null; + 'replicaSetUpdateSigner'?: string | null; + 'secondaryIds'?: any | null; + 'slot'?: number | null; + 'txhash'?: string; + 'updatedAt'?: Date; + 'userAuthorityAccount'?: string | null; + 'userId': number; + 'userStorageAccount'?: string | null; + 'wallet'?: string | null; } export enum wallet_chain { 'eth' = 'eth', diff --git a/packages/trpc-server/src/db.ts b/packages/trpc-server/src/db.ts index 3b111135d50..00cc8f4a20c 100644 --- a/packages/trpc-server/src/db.ts +++ b/packages/trpc-server/src/db.ts @@ -36,7 +36,7 @@ export async function selectUsersCamel(p: SelectUserProps) { select * from users left join aggregate_user agg using (user_id) - where is_current = true + where 1 = 1 ${p.ids ? sql`and user_id in ${sql(p.ids)}` : sql``} ${p.handle ? sql`and handle_lc = lower(${p.handle})` : sql``} ` diff --git a/packages/trpc-server/src/loaders.ts b/packages/trpc-server/src/loaders.ts index a92a240f3a5..dbfe19aa541 100644 --- a/packages/trpc-server/src/loaders.ts +++ b/packages/trpc-server/src/loaders.ts @@ -71,6 +71,31 @@ export const prepareLoaders = (myId: number | undefined) => ({ } ), + trackRelationLoader: new DataLoader( + async (ids) => { + const saved = new Set() + const reposted = new Set() + + if (myId) { + const history = await sql` + select 'save' as verb, save_item_id as id from saves where user_id = ${myId} + and save_item_id in ${sql(ids)} + union + select 'repost', repost_item_id from reposts where user_id = ${myId} + and repost_item_id in ${sql(ids)} + ` + for (const { verb, id } of history) { + verb == 'save' ? saved.add(id) : reposted.add(id) + } + } + + return ids.map((id) => ({ + saved: saved.has(id), + reposted: reposted.has(id), + })) + } + ), + actionLoaderForKind: function (kind: string) { return new DataLoader(async (ids) => { // so much save / repost + playlist / album pain diff --git a/packages/trpc-server/src/routers/me-router.ts b/packages/trpc-server/src/routers/me-router.ts index 348247f907c..47c242b0f1b 100644 --- a/packages/trpc-server/src/routers/me-router.ts +++ b/packages/trpc-server/src/routers/me-router.ts @@ -2,6 +2,7 @@ import { z } from 'zod' import { publicProcedure, router } from '../trpc' import { AggregateUserRow } from '../db-tables' import { sql } from '../db' +import { TRPCError } from '@trpc/server' export const meRouter = router({ userRelationship: publicProcedure @@ -13,16 +14,110 @@ export const meRouter = router({ return result }), + trackRelationship: publicProcedure + .input(z.string()) + .query(async ({ ctx, input }) => { + return ctx.loaders.trackRelationLoader.load(parseInt(input)) + }), + + playHistory: publicProcedure + .meta({ openapi: { method: 'GET', path: '/me/play_history' } }) + .input( + z.object({ + sort: z + .enum([ + 'trackName', + 'artistName', + 'releaseDate', + 'playDate', + 'duration', + 'playCount', + 'repostCount', + ]) + .default('playDate'), + sortAscending: z.boolean().default(false), + cursor: z.number().default(0), + limit: z.number().default(1000), + }) + ) + .output( + z.array( + z.object({ + artistId: z.number(), + artistHandle: z.string(), + artistName: z.string(), + trackId: z.number(), + routeId: z.string().nullish(), + trackName: z.string(), + releaseDate: z.date(), + duration: z.number(), + playCount: z.number(), + repostCount: z.number(), + playDate: z.date(), + }) + ) + ) + .query(async ({ ctx, input }) => { + if (!ctx.currentUserId) { + throw new TRPCError({ code: 'UNAUTHORIZED' }) + } + const sortMapping: Record = { + trackName: sql`track_name`, + artistName: sql`artist_name`, + releaseDate: sql`release_date`, + duration: sql`duration`, + playDate: sql`play_date`, + playCount: sql`play_count`, + repostCount: sql`repost_count`, + } + const sortField = sortMapping[input.sort] + const sortDirection = input.sortAscending ? sql`asc` : sql`desc` + + return sql` + with duped as ( + + select + a.user_id as artist_id, + a.handle as artist_handle, + a.name as artist_name, + + t.track_id, + t.route_id, + t.title as track_name, + coalesce(t.release_date, t.created_at) as release_date, + t.duration, + + agg_play.count as play_count, + agg_track.repost_count, + p.created_at as play_date, + row_number() over (partition by p.play_item_id order by p.created_at asc) as rownum + + from plays p + join tracks t on p.play_item_id = t.track_id + join users a on t.owner_id = a.user_id + join aggregate_plays agg_play on p.play_item_id = agg_play.play_item_id + join aggregate_track agg_track on p.play_item_id = agg_track.track_id + + where p.user_id = ${ctx.currentUserId!} + + ) + select * from duped where rownum = 1 + order by ${sortField} ${sortDirection} + limit ${input.limit} + offset ${input.cursor || 0} + ` + }), + actions: publicProcedure .input( z.object({ kind: z.string(), - id: z.string() + id: z.string(), }) ) .query(async ({ ctx, input }) => { return ctx.loaders .actionLoaderForKind(input.kind) .load(parseInt(input.id)) - }) + }), }) diff --git a/packages/trpc-server/src/routers/search-router.ts b/packages/trpc-server/src/routers/search-router.ts index f6691a9743e..1071f5363b7 100644 --- a/packages/trpc-server/src/routers/search-router.ts +++ b/packages/trpc-server/src/routers/search-router.ts @@ -1,6 +1,5 @@ import { z } from 'zod' import { publicProcedure, router } from '../trpc' -import { TRPCError } from '@trpc/server' import { Client as ES } from '@elastic/elasticsearch' export const esc = new ES({ node: process.env.audius_elasticsearch_url }) diff --git a/packages/trpc-server/src/routers/track-router.ts b/packages/trpc-server/src/routers/track-router.ts index 4233bfdd023..bbb50d3a4a0 100644 --- a/packages/trpc-server/src/routers/track-router.ts +++ b/packages/trpc-server/src/routers/track-router.ts @@ -1,6 +1,8 @@ import { z } from 'zod' import { publicProcedure, router } from '../trpc' import { TRPCError } from '@trpc/server' +import { sql } from '../db' +import { UserRow } from '../db-tables' import { esc } from './search-router' type AlbumBacklinkMetadata = { @@ -18,6 +20,50 @@ export const trackRouter = router({ return row }), + topListeners: publicProcedure + .meta({ openapi: { method: 'GET', path: '/tracks/top_listeners' } }) + .input( + z.object({ + trackId: z.number(), + limit: z.number().default(100), + }) + ) + .output(z.array(z.custom())) + .query(async ({ ctx, input }) => { + const rows: TopListenerRow[] = await sql` + with + deduped as ( + select distinct play_item_id, user_id, date_trunc('hour', created_at) as created_at + from plays + where user_id is not null + and play_item_id = ${input.trackId} + ), + counted as ( + select user_id, count(*) as play_count + from deduped + group by 1 + ) + select * + from counted + join users u using (user_id) + order by play_count desc + limit ${input.limit} + ` + + // remove some needless user fields + // this could also be done with output validator + // or we could just return (userId, playCount) + // and client can fetch user rows... + rows.forEach((r) => { + delete r.playlistLibrary + delete r.primaryId + delete r.secondaryIds + delete r.creatorNodeEndpoint + }) + + return rows + }), + getAlbumBacklink: publicProcedure .input(z.object({ trackId: z.number() })) .query(async ({ input }) => { @@ -47,3 +93,7 @@ export const trackRouter = router({ return hits[0] }), }) + +type TopListenerRow = UserRow & { + playCount: number +} diff --git a/packages/trpc-server/test.sh b/packages/trpc-server/test.sh index b6007017904..6f485efa4ca 100644 --- a/packages/trpc-server/test.sh +++ b/packages/trpc-server/test.sh @@ -2,7 +2,7 @@ set -e # start postgres + elasticsearch -docker compose up -d +docker compose up -d --wait # set env variables export audius_db_url='postgres://postgres:testing@localhost:35764' @@ -13,6 +13,9 @@ export DB_URL="$audius_db_url" # cd ../discovery-provider/ddl && ./pg_migrate.sh && cd - || exit docker exec -w '/ddl' trpc-server-db-1 './pg_migrate.sh' +# run db:gen +npm run db:gen + # populate db fixtures npx vite-node test/_fixtures.ts diff --git a/packages/trpc-server/test/_fixtures.ts b/packages/trpc-server/test/_fixtures.ts index 6eb7d363365..f2ed973fac8 100644 --- a/packages/trpc-server/test/_fixtures.ts +++ b/packages/trpc-server/test/_fixtures.ts @@ -7,10 +7,17 @@ So records should have a blocknumber to get indexed. */ import { sql } from '../src/db' -import { FollowRow, PlaylistRow, SaveRow, TrackRow } from '../src/db-tables' +import { + FollowRow, + PlayRow, + PlaylistRow, + RepostRow, + SaveRow, + TrackRow, +} from '../src/db-tables' type TableFixture = { - common: RowType + common: Partial rows: RowType[] } @@ -41,14 +48,17 @@ const fixtures = { { user_id: 101, handle: 'steve', + name: 'Steve Steve', }, { user_id: 102, handle: 'dave', + name: 'Dave Dave', }, { user_id: 103, handle: 'dave again', + name: 'Dave Again', }, ], }, @@ -157,6 +167,48 @@ const fixtures = { }, ], } as TableFixture, + + reposts: { + common: { + blocknumber: 1, + createdAt: new Date(), + isCurrent: true, + isDelete: false, + txhash: '0x123', + isRepostOfRepost: false, + }, + rows: [ + { + userId: 102, + repostType: 'track', + repostItemId: 201, + }, + ], + } as TableFixture, + + plays: { + common: {}, + rows: [ + { + userId: 101, + playItemId: 201, + createdAt: dateAddDays(-1), + updatedAt: dateAddDays(-1), + }, + { + userId: 101, + playItemId: 202, + createdAt: dateAddDays(-2), + updatedAt: dateAddDays(-2), + }, + { + userId: 101, + playItemId: 203, + createdAt: dateAddDays(-4), + updatedAt: dateAddDays(-4), + }, + ], + } as TableFixture, } function dateAddDays(dayDelta: number) { diff --git a/packages/trpc-server/test/me-router.test.ts b/packages/trpc-server/test/me-router.test.ts new file mode 100644 index 00000000000..0c9330c2d4e --- /dev/null +++ b/packages/trpc-server/test/me-router.test.ts @@ -0,0 +1,72 @@ +import { expect, test } from 'vitest' +import { testRouter } from './_test_helpers' + +test('version', async () => { + // user 101 + { + const caller = await testRouter(101) + const plays = await caller.me.playHistory({}) + + expect(plays).length(3) + expect(plays.map((r) => r.trackId)).toEqual([201, 202, 203]) + + expect(plays[0].repostCount).toEqual(1) + expect(plays[1].repostCount).toEqual(0) + + // offset + { + const plays = await caller.me.playHistory({ + cursor: 1, + }) + expect(plays).length(2) + expect(plays[0].trackId).toEqual(202) + } + + // sort + { + // track_name asc + let rows = await caller.me.playHistory({ + sort: 'trackName', + sortAscending: true, + }) + + const names = rows.map((row) => row.trackName) + expect(names).toEqual(names.sort()) + + // track_name desc + rows = await caller.me.playHistory({ + sort: 'trackName', + sortAscending: false, + }) + expect(rows.map((r) => r.trackName)).toEqual(names.sort().reverse()) + + // play_date asc + rows = await caller.me.playHistory({ + sortAscending: true, + }) + expect(rows.map((r) => r.trackId)).toEqual([203, 202, 201]) + + // try all the sorts to ensure no exceptions + for (const sort of [ + 'trackName', + 'artistName', + 'releaseDate', + 'playDate', + 'duration', + 'playCount', + 'repostCount', + ]) { + await caller.me.playHistory({ + sort: sort as any, + }) + } + } + } + + // user 102 + { + const caller = await testRouter(102) + const plays = await caller.me.playHistory({}) + expect(plays).length(0) + } +}) diff --git a/packages/trpc-server/test/search.test.ts b/packages/trpc-server/test/search.test.ts index 80d10a8f265..4c0e68e243c 100644 --- a/packages/trpc-server/test/search.test.ts +++ b/packages/trpc-server/test/search.test.ts @@ -37,9 +37,9 @@ test('search tracks', async () => { } }) -test('you can query for playlists that contain a track id', async () => { - const caller = await testRouter() +// test('you can query for playlists that contain a track id', async () => { +// const caller = await testRouter() - const playlistIds = await caller.tracks.getAlbumBacklink({ trackId: 101 }) - expect(playlistIds?.playlist_id).toEqual(['301']) -}) +// const playlistIds = await caller.tracks.getAlbumBacklink({ trackId: 101 }) +// expect(playlistIds?.playlist_id).toEqual(['301']) +// }) diff --git a/packages/web/src/app/App.tsx b/packages/web/src/app/App.tsx index b8806b1cc15..0af61dd5c67 100644 --- a/packages/web/src/app/App.tsx +++ b/packages/web/src/app/App.tsx @@ -20,6 +20,7 @@ const SignOnPage = lazy(() => import('pages/sign-on-page')) const SignOn = lazy(() => import('pages/sign-on/SignOn')) const OAuthLoginPage = lazy(() => import('pages/oauth-login-page')) const DemoTrpcPage = lazy(() => import('pages/demo-trpc/DemoTrpcPage')) +const TrpcHistoryPage = lazy(() => import('pages/demo-trpc/TrpcHistory')) const MERCHANT_ID = process.env.VITE_COINFLOW_MERCHANT_ID const IS_PRODUCTION = process.env.VITE_ENVIRONMENT === 'production' @@ -51,6 +52,9 @@ export const AppInner = () => { + + + diff --git a/packages/web/src/pages/demo-trpc/DemoTrpcPage.tsx b/packages/web/src/pages/demo-trpc/DemoTrpcPage.tsx index 195d7ac8448..ae4c3bad9a2 100644 --- a/packages/web/src/pages/demo-trpc/DemoTrpcPage.tsx +++ b/packages/web/src/pages/demo-trpc/DemoTrpcPage.tsx @@ -487,7 +487,7 @@ function RepostIndicator({ kind, id, suspense = true }: SaveRepostParams) { // ==================== Image stuff ==================== -function CidImage({ +export function CidImage({ cid, size }: { diff --git a/packages/web/src/pages/demo-trpc/TrpcHistory.module.css b/packages/web/src/pages/demo-trpc/TrpcHistory.module.css new file mode 100644 index 00000000000..f20beec045d --- /dev/null +++ b/packages/web/src/pages/demo-trpc/TrpcHistory.module.css @@ -0,0 +1,47 @@ +.historyPage { + margin: 20px; + padding: 20px; +} +.history { + border: 10px solid white; +} +.history td, +.history th { + padding: 10px; + background: white; +} +.history th { + text-align: left; + font-weight: 900; +} + +.right { + text-align: right; +} + +.history tr:hover td { + background: lightyellow; +} + +.topListener { + position: relative; +} +.topListenerPopover { + position: absolute; + text-align: left; + left: 100%; + padding: 10px; + background: white; + border: 1px solid black; + box-shadow: 3px 3px 0px #555; + white-space: nowrap; +} +.topListenerRow { + display: flex; + align-items: center; + gap: 4px; + margin-bottom: 2px; +} +.topListenerRow:hover { + background: lightyellow; +} diff --git a/packages/web/src/pages/demo-trpc/TrpcHistory.tsx b/packages/web/src/pages/demo-trpc/TrpcHistory.tsx new file mode 100644 index 00000000000..38770f53d55 --- /dev/null +++ b/packages/web/src/pages/demo-trpc/TrpcHistory.tsx @@ -0,0 +1,139 @@ +import { useState } from 'react' + +import { RouterOutput } from '@audius/trpc-server' +import { Link } from 'react-router-dom' + +import { trpc } from 'utils/trpcClientWeb' + +import { CidImage } from './DemoTrpcPage' +import styles from './TrpcHistory.module.css' + +type HistoryRow = RouterOutput['me']['playHistory'][0] + +const LIMIT = 500 + +export default function TrpcHistoryPage() { + const fetcher = trpc.me.playHistory.useInfiniteQuery( + { + limit: LIMIT + }, + { + getNextPageParam: (lastPage, allPages) => { + if (lastPage.length === LIMIT) { + const val = allPages.reduce((acc, page) => acc + page.length, 0) + return val + } + } + } + ) + return ( +
+ + + + + + + + + + + + + + + + {fetcher.data?.pages.map((page, idx) => ( + + {page.map((row, idx2) => ( + + ))} + + ))} +
Track NameArtistReleasedPlayedLengthPlaysReposts
+
+ {fetcher.hasNextPage ? ( + + ) : null} +
+
+ ) +} + +function HistoryTR({ row }: { row: HistoryRow }) { + const { data: myStatus } = trpc.me.trackRelationship.useQuery( + row.trackId.toString() + ) + return ( + + + + {/* todo: turns out route_id is half busted... where to get actual route? */} + {row.trackName} + + + {/* todo: artist popover */} + {row.artistName} + + {formatDate(row.releaseDate)} + {formatDate(row.playDate)} + {row.duration} + + + + {row.repostCount} + {/* todo: save + repost buttons with icons */} + {myStatus?.saved ? 'saved' : ''} + {myStatus?.reposted ? 'reposted' : ''} + {/* todo: ... menu */} + + ) +} + +function TopListenersPopover({ + trackId, + playCount +}: { + trackId: number + playCount: number +}) { + const [isHovered, setIsHovered] = useState(false) + const { data: topListeners, isLoading } = trpc.tracks.topListeners.useQuery( + { + trackId + }, + { + enabled: isHovered + } + ) + + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + {isHovered && !isLoading ? ( +
+ {topListeners?.map((t) => ( +
+ +
+ {t.name} +
+
{t.playCount}
+
+ ))} +
+ ) : null} + {playCount} +
+ ) +} + +function formatDate(d: string) { + return new Date(d).toLocaleDateString() +}