From bc3c2d45b8356c0debca2c7f5c228511f2c8c55d Mon Sep 17 00:00:00 2001 From: Tsiry Sandratraina Date: Thu, 1 Jan 2026 23:49:45 +0300 Subject: [PATCH] Add follows graph, DB table and endpoints --- apps/api/drizzle/0002_robust_wong.sql | 12 + apps/api/drizzle/meta/0002_snapshot.json | 3424 +++++++++++++++++ apps/api/drizzle/meta/_journal.json | 7 + apps/api/lexicons/graph/follow.json | 4 + apps/api/lexicons/graph/followAccount.json | 49 + apps/api/lexicons/graph/unfollowAccount.json | 49 + apps/api/pkl/defs/graph/follow.pkl | 7 +- apps/api/pkl/defs/graph/followAccount.pkl | 46 + apps/api/pkl/defs/graph/unfollowAccount.pkl | 46 + apps/api/src/lexicon/index.ts | 24 + apps/api/src/lexicon/lexicons.ts | 98 + .../lexicon/types/app/rocksky/graph/follow.ts | 3 +- .../types/app/rocksky/graph/followAccount.ts | 50 + .../app/rocksky/graph/unfollowAccount.ts | 50 + apps/api/src/schema/follows.ts | 33 + apps/api/src/schema/index.ts | 2 + apps/api/src/scripts/avatar.ts | 5 +- .../xrpc/app/rocksky/graph/followAccount.ts | 69 + .../xrpc/app/rocksky/graph/getFollowers.ts | 79 + .../src/xrpc/app/rocksky/graph/getFollows.ts | 79 + .../app/rocksky/graph/getKnownFollowers.ts | 63 + .../xrpc/app/rocksky/graph/unfollowAccount.ts | 64 + .../InteractionBar/InteractionBar.tsx | 2 +- crates/jetstream/src/repo.rs | 52 +- crates/jetstream/src/subscriber.rs | 1 + crates/jetstream/src/types.rs | 7 + crates/jetstream/src/xata/follow.rs | 12 + crates/jetstream/src/xata/mod.rs | 1 + 28 files changed, 4328 insertions(+), 10 deletions(-) create mode 100644 apps/api/drizzle/0002_robust_wong.sql create mode 100644 apps/api/drizzle/meta/0002_snapshot.json create mode 100644 apps/api/lexicons/graph/followAccount.json create mode 100644 apps/api/lexicons/graph/unfollowAccount.json create mode 100644 apps/api/pkl/defs/graph/followAccount.pkl create mode 100644 apps/api/pkl/defs/graph/unfollowAccount.pkl create mode 100644 apps/api/src/lexicon/types/app/rocksky/graph/followAccount.ts create mode 100644 apps/api/src/lexicon/types/app/rocksky/graph/unfollowAccount.ts create mode 100644 apps/api/src/schema/follows.ts create mode 100644 apps/api/src/xrpc/app/rocksky/graph/followAccount.ts create mode 100644 apps/api/src/xrpc/app/rocksky/graph/getFollowers.ts create mode 100644 apps/api/src/xrpc/app/rocksky/graph/getFollows.ts create mode 100644 apps/api/src/xrpc/app/rocksky/graph/getKnownFollowers.ts create mode 100644 apps/api/src/xrpc/app/rocksky/graph/unfollowAccount.ts create mode 100644 crates/jetstream/src/xata/follow.rs diff --git a/apps/api/drizzle/0002_robust_wong.sql b/apps/api/drizzle/0002_robust_wong.sql new file mode 100644 index 00000000..6869baca --- /dev/null +++ b/apps/api/drizzle/0002_robust_wong.sql @@ -0,0 +1,12 @@ +CREATE TABLE "follows" ( + "xata_id" text PRIMARY KEY DEFAULT xata_id() NOT NULL, + "uri" text NOT NULL, + "follower_did" text NOT NULL, + "subject_did" text NOT NULL, + "xata_version" integer, + "xata_createdat" timestamp DEFAULT now() NOT NULL, + "xata_updatedat" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "follows_uri_unique" UNIQUE("uri") +); +--> statement-breakpoint +CREATE UNIQUE INDEX "follows_follower_subject_unique" ON "follows" USING btree ("follower_did","subject_did"); \ No newline at end of file diff --git a/apps/api/drizzle/meta/0002_snapshot.json b/apps/api/drizzle/meta/0002_snapshot.json new file mode 100644 index 00000000..06807580 --- /dev/null +++ b/apps/api/drizzle/meta/0002_snapshot.json @@ -0,0 +1,3424 @@ +{ + "id": "ba060d7c-5d31-44b1-abd4-8de1cd5357a7", + "prevId": "6d2ede58-9041-48a4-92fc-e8995d6bc049", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.album_tracks": { + "name": "album_tracks", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "album_id": { + "name": "album_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "track_id": { + "name": "track_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "album_tracks_album_id_albums_xata_id_fk": { + "name": "album_tracks_album_id_albums_xata_id_fk", + "tableFrom": "album_tracks", + "tableTo": "albums", + "columnsFrom": [ + "album_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "album_tracks_track_id_tracks_xata_id_fk": { + "name": "album_tracks_track_id_tracks_xata_id_fk", + "tableFrom": "album_tracks", + "tableTo": "tracks", + "columnsFrom": [ + "track_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.albums": { + "name": "albums", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "artist": { + "name": "artist", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "release_date": { + "name": "release_date", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "year": { + "name": "year", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "album_art": { + "name": "album_art", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "artist_uri": { + "name": "artist_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "apple_music_link": { + "name": "apple_music_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "spotify_link": { + "name": "spotify_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tidal_link": { + "name": "tidal_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "youtube_link": { + "name": "youtube_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "albums_uri_unique": { + "name": "albums_uri_unique", + "nullsNotDistinct": false, + "columns": [ + "uri" + ] + }, + "albums_apple_music_link_unique": { + "name": "albums_apple_music_link_unique", + "nullsNotDistinct": false, + "columns": [ + "apple_music_link" + ] + }, + "albums_spotify_link_unique": { + "name": "albums_spotify_link_unique", + "nullsNotDistinct": false, + "columns": [ + "spotify_link" + ] + }, + "albums_tidal_link_unique": { + "name": "albums_tidal_link_unique", + "nullsNotDistinct": false, + "columns": [ + "tidal_link" + ] + }, + "albums_youtube_link_unique": { + "name": "albums_youtube_link_unique", + "nullsNotDistinct": false, + "columns": [ + "youtube_link" + ] + }, + "albums_sha256_unique": { + "name": "albums_sha256_unique", + "nullsNotDistinct": false, + "columns": [ + "sha256" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shared_secret": { + "name": "shared_secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_user_id_users_xata_id_fk": { + "name": "api_keys_user_id_users_xata_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.artist_albums": { + "name": "artist_albums", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "artist_id": { + "name": "artist_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "album_id": { + "name": "album_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "artist_albums_artist_id_artists_xata_id_fk": { + "name": "artist_albums_artist_id_artists_xata_id_fk", + "tableFrom": "artist_albums", + "tableTo": "artists", + "columnsFrom": [ + "artist_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "artist_albums_album_id_albums_xata_id_fk": { + "name": "artist_albums_album_id_albums_xata_id_fk", + "tableFrom": "artist_albums", + "tableTo": "albums", + "columnsFrom": [ + "album_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.artist_tracks": { + "name": "artist_tracks", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "artist_id": { + "name": "artist_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "track_id": { + "name": "track_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "artist_tracks_artist_id_artists_xata_id_fk": { + "name": "artist_tracks_artist_id_artists_xata_id_fk", + "tableFrom": "artist_tracks", + "tableTo": "artists", + "columnsFrom": [ + "artist_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "artist_tracks_track_id_tracks_xata_id_fk": { + "name": "artist_tracks_track_id_tracks_xata_id_fk", + "tableFrom": "artist_tracks", + "tableTo": "tracks", + "columnsFrom": [ + "track_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.artists": { + "name": "artists", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "biography": { + "name": "biography", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "born": { + "name": "born", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "born_in": { + "name": "born_in", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "died": { + "name": "died", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "picture": { + "name": "picture", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "apple_music_link": { + "name": "apple_music_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "spotify_link": { + "name": "spotify_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tidal_link": { + "name": "tidal_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "youtube_link": { + "name": "youtube_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "genres": { + "name": "genres", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "artists_sha256_unique": { + "name": "artists_sha256_unique", + "nullsNotDistinct": false, + "columns": [ + "sha256" + ] + }, + "artists_uri_unique": { + "name": "artists_uri_unique", + "nullsNotDistinct": false, + "columns": [ + "uri" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dropbox_accounts": { + "name": "dropbox_accounts", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_beta_user": { + "name": "is_beta_user", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_version": { + "name": "xata_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "dropbox_accounts_user_id_users_xata_id_fk": { + "name": "dropbox_accounts_user_id_users_xata_id_fk", + "tableFrom": "dropbox_accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "dropbox_accounts_email_unique": { + "name": "dropbox_accounts_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dropbox_directories": { + "name": "dropbox_directories", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dropbox_id": { + "name": "dropbox_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_version": { + "name": "xata_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "dropbox_directories_parent_id_dropbox_directories_xata_id_fk": { + "name": "dropbox_directories_parent_id_dropbox_directories_xata_id_fk", + "tableFrom": "dropbox_directories", + "tableTo": "dropbox_directories", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "dropbox_directories_file_id_unique": { + "name": "dropbox_directories_file_id_unique", + "nullsNotDistinct": false, + "columns": [ + "file_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dropbox_paths": { + "name": "dropbox_paths", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dropbox_id": { + "name": "dropbox_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "track_id": { + "name": "track_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "directory_id": { + "name": "directory_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_version": { + "name": "xata_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "dropbox_paths_directory_id_dropbox_directories_xata_id_fk": { + "name": "dropbox_paths_directory_id_dropbox_directories_xata_id_fk", + "tableFrom": "dropbox_paths", + "tableTo": "dropbox_directories", + "columnsFrom": [ + "directory_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "dropbox_paths_file_id_unique": { + "name": "dropbox_paths_file_id_unique", + "nullsNotDistinct": false, + "columns": [ + "file_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dropbox_tokens": { + "name": "dropbox_tokens", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dropbox": { + "name": "dropbox", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dropbox_token_id": { + "name": "dropbox_token_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_version": { + "name": "xata_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "dropbox_user_id_users_xata_id_fk": { + "name": "dropbox_user_id_users_xata_id_fk", + "tableFrom": "dropbox", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "dropbox_dropbox_token_id_dropbox_tokens_xata_id_fk": { + "name": "dropbox_dropbox_token_id_dropbox_tokens_xata_id_fk", + "tableFrom": "dropbox", + "tableTo": "dropbox_tokens", + "columnsFrom": [ + "dropbox_token_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.feeds": { + "name": "feeds", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "did": { + "name": "did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "feeds_user_id_users_xata_id_fk": { + "name": "feeds_user_id_users_xata_id_fk", + "tableFrom": "feeds", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "feeds_uri_unique": { + "name": "feeds_uri_unique", + "nullsNotDistinct": false, + "columns": [ + "uri" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.follows": { + "name": "follows", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "follower_did": { + "name": "follower_did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_did": { + "name": "subject_did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "follows_follower_subject_unique": { + "name": "follows_follower_subject_unique", + "columns": [ + { + "expression": "follower_did", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject_did", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "follows_uri_unique": { + "name": "follows_uri_unique", + "nullsNotDistinct": false, + "columns": [ + "uri" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.google_drive_accounts": { + "name": "google_drive_accounts", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_beta_user": { + "name": "is_beta_user", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_version": { + "name": "xata_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "google_drive_accounts_user_id_users_xata_id_fk": { + "name": "google_drive_accounts_user_id_users_xata_id_fk", + "tableFrom": "google_drive_accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "google_drive_accounts_email_unique": { + "name": "google_drive_accounts_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.google_drive_directories": { + "name": "google_drive_directories", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "google_drive_id": { + "name": "google_drive_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_version": { + "name": "xata_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "google_drive_directories_parent_id_google_drive_directories_xata_id_fk": { + "name": "google_drive_directories_parent_id_google_drive_directories_xata_id_fk", + "tableFrom": "google_drive_directories", + "tableTo": "google_drive_directories", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "google_drive_directories_file_id_unique": { + "name": "google_drive_directories_file_id_unique", + "nullsNotDistinct": false, + "columns": [ + "file_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.google_drive_paths": { + "name": "google_drive_paths", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "google_drive_id": { + "name": "google_drive_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "track_id": { + "name": "track_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "directory_id": { + "name": "directory_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_version": { + "name": "xata_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "google_drive_paths_directory_id_google_drive_directories_xata_id_fk": { + "name": "google_drive_paths_directory_id_google_drive_directories_xata_id_fk", + "tableFrom": "google_drive_paths", + "tableTo": "google_drive_directories", + "columnsFrom": [ + "directory_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "google_drive_paths_file_id_unique": { + "name": "google_drive_paths_file_id_unique", + "nullsNotDistinct": false, + "columns": [ + "file_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.google_drive_tokens": { + "name": "google_drive_tokens", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.google_drive": { + "name": "google_drive", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "google_drive_token_id": { + "name": "google_drive_token_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_version": { + "name": "xata_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "google_drive_google_drive_token_id_google_drive_tokens_xata_id_fk": { + "name": "google_drive_google_drive_token_id_google_drive_tokens_xata_id_fk", + "tableFrom": "google_drive", + "tableTo": "google_drive_tokens", + "columnsFrom": [ + "google_drive_token_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "google_drive_user_id_users_xata_id_fk": { + "name": "google_drive_user_id_users_xata_id_fk", + "tableFrom": "google_drive", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.loved_tracks": { + "name": "loved_tracks", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "track_id": { + "name": "track_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "loved_tracks_user_id_users_xata_id_fk": { + "name": "loved_tracks_user_id_users_xata_id_fk", + "tableFrom": "loved_tracks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "loved_tracks_track_id_tracks_xata_id_fk": { + "name": "loved_tracks_track_id_tracks_xata_id_fk", + "tableFrom": "loved_tracks", + "tableTo": "tracks", + "columnsFrom": [ + "track_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "loved_tracks_uri_unique": { + "name": "loved_tracks_uri_unique", + "nullsNotDistinct": false, + "columns": [ + "uri" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.playlist_tracks": { + "name": "playlist_tracks", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "playlist_id": { + "name": "playlist_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "track_id": { + "name": "track_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "playlist_tracks_playlist_id_playlists_xata_id_fk": { + "name": "playlist_tracks_playlist_id_playlists_xata_id_fk", + "tableFrom": "playlist_tracks", + "tableTo": "playlists", + "columnsFrom": [ + "playlist_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "playlist_tracks_track_id_tracks_xata_id_fk": { + "name": "playlist_tracks_track_id_tracks_xata_id_fk", + "tableFrom": "playlist_tracks", + "tableTo": "tracks", + "columnsFrom": [ + "track_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.playlists": { + "name": "playlists", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "picture": { + "name": "picture", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "spotify_link": { + "name": "spotify_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tidal_link": { + "name": "tidal_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "apple_music_link": { + "name": "apple_music_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "playlists_created_by_users_xata_id_fk": { + "name": "playlists_created_by_users_xata_id_fk", + "tableFrom": "playlists", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "playlists_uri_unique": { + "name": "playlists_uri_unique", + "nullsNotDistinct": false, + "columns": [ + "uri" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profile_shouts": { + "name": "profile_shouts", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shout_id": { + "name": "shout_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "profile_shouts_user_id_users_xata_id_fk": { + "name": "profile_shouts_user_id_users_xata_id_fk", + "tableFrom": "profile_shouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "profile_shouts_shout_id_shouts_xata_id_fk": { + "name": "profile_shouts_shout_id_shouts_xata_id_fk", + "tableFrom": "profile_shouts", + "tableTo": "shouts", + "columnsFrom": [ + "shout_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.queue_tracks": { + "name": "queue_tracks", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "track_id": { + "name": "track_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "file_uri": { + "name": "file_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "queue_tracks_user_id_users_xata_id_fk": { + "name": "queue_tracks_user_id_users_xata_id_fk", + "tableFrom": "queue_tracks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "queue_tracks_track_id_tracks_xata_id_fk": { + "name": "queue_tracks_track_id_tracks_xata_id_fk", + "tableFrom": "queue_tracks", + "tableTo": "tracks", + "columnsFrom": [ + "track_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scrobbles": { + "name": "scrobbles", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "track_id": { + "name": "track_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "album_id": { + "name": "album_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "artist_id": { + "name": "artist_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "scrobbles_user_id_users_xata_id_fk": { + "name": "scrobbles_user_id_users_xata_id_fk", + "tableFrom": "scrobbles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "scrobbles_track_id_tracks_xata_id_fk": { + "name": "scrobbles_track_id_tracks_xata_id_fk", + "tableFrom": "scrobbles", + "tableTo": "tracks", + "columnsFrom": [ + "track_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "scrobbles_album_id_albums_xata_id_fk": { + "name": "scrobbles_album_id_albums_xata_id_fk", + "tableFrom": "scrobbles", + "tableTo": "albums", + "columnsFrom": [ + "album_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "scrobbles_artist_id_artists_xata_id_fk": { + "name": "scrobbles_artist_id_artists_xata_id_fk", + "tableFrom": "scrobbles", + "tableTo": "artists", + "columnsFrom": [ + "artist_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scrobbles_uri_unique": { + "name": "scrobbles_uri_unique", + "nullsNotDistinct": false, + "columns": [ + "uri" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shout_likes": { + "name": "shout_likes", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shout_id": { + "name": "shout_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "shout_likes_user_id_users_xata_id_fk": { + "name": "shout_likes_user_id_users_xata_id_fk", + "tableFrom": "shout_likes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "shout_likes_shout_id_shouts_xata_id_fk": { + "name": "shout_likes_shout_id_shouts_xata_id_fk", + "tableFrom": "shout_likes", + "tableTo": "shouts", + "columnsFrom": [ + "shout_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "shout_likes_uri_unique": { + "name": "shout_likes_uri_unique", + "nullsNotDistinct": false, + "columns": [ + "uri" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shout_reports": { + "name": "shout_reports", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shout_id": { + "name": "shout_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "shout_reports_user_id_users_xata_id_fk": { + "name": "shout_reports_user_id_users_xata_id_fk", + "tableFrom": "shout_reports", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "shout_reports_shout_id_shouts_xata_id_fk": { + "name": "shout_reports_shout_id_shouts_xata_id_fk", + "tableFrom": "shout_reports", + "tableTo": "shouts", + "columnsFrom": [ + "shout_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shouts": { + "name": "shouts", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "track_id": { + "name": "track_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "artist_id": { + "name": "artist_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "album_id": { + "name": "album_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scrobble_id": { + "name": "scrobble_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "shouts_track_id_tracks_xata_id_fk": { + "name": "shouts_track_id_tracks_xata_id_fk", + "tableFrom": "shouts", + "tableTo": "tracks", + "columnsFrom": [ + "track_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "shouts_artist_id_users_xata_id_fk": { + "name": "shouts_artist_id_users_xata_id_fk", + "tableFrom": "shouts", + "tableTo": "users", + "columnsFrom": [ + "artist_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "shouts_album_id_albums_xata_id_fk": { + "name": "shouts_album_id_albums_xata_id_fk", + "tableFrom": "shouts", + "tableTo": "albums", + "columnsFrom": [ + "album_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "shouts_scrobble_id_scrobbles_xata_id_fk": { + "name": "shouts_scrobble_id_scrobbles_xata_id_fk", + "tableFrom": "shouts", + "tableTo": "scrobbles", + "columnsFrom": [ + "scrobble_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "shouts_author_id_users_xata_id_fk": { + "name": "shouts_author_id_users_xata_id_fk", + "tableFrom": "shouts", + "tableTo": "users", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "shouts_parent_id_shouts_xata_id_fk": { + "name": "shouts_parent_id_shouts_xata_id_fk", + "tableFrom": "shouts", + "tableTo": "shouts", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "shouts_uri_unique": { + "name": "shouts_uri_unique", + "nullsNotDistinct": false, + "columns": [ + "uri" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spotify_accounts": { + "name": "spotify_accounts", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_beta_user": { + "name": "is_beta_user", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "spotify_app_id": { + "name": "spotify_app_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "spotify_accounts_user_id_users_xata_id_fk": { + "name": "spotify_accounts_user_id_users_xata_id_fk", + "tableFrom": "spotify_accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spotify_apps": { + "name": "spotify_apps", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "spotify_app_id": { + "name": "spotify_app_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "spotify_secret": { + "name": "spotify_secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "spotify_apps_spotify_app_id_unique": { + "name": "spotify_apps_spotify_app_id_unique", + "nullsNotDistinct": false, + "columns": [ + "spotify_app_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spotify_tokens": { + "name": "spotify_tokens", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "spotify_app_id": { + "name": "spotify_app_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "spotify_tokens_user_id_users_xata_id_fk": { + "name": "spotify_tokens_user_id_users_xata_id_fk", + "tableFrom": "spotify_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tracks": { + "name": "tracks", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "artist": { + "name": "artist", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "album_artist": { + "name": "album_artist", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "album_art": { + "name": "album_art", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "album": { + "name": "album", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "track_number": { + "name": "track_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mb_id": { + "name": "mb_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "youtube_link": { + "name": "youtube_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "spotify_link": { + "name": "spotify_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "apple_music_link": { + "name": "apple_music_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tidal_link": { + "name": "tidal_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "disc_number": { + "name": "disc_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "lyrics": { + "name": "lyrics", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composer": { + "name": "composer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "genre": { + "name": "genre", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "copyright_message": { + "name": "copyright_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "album_uri": { + "name": "album_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "artist_uri": { + "name": "artist_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "tracks_mb_id_unique": { + "name": "tracks_mb_id_unique", + "nullsNotDistinct": false, + "columns": [ + "mb_id" + ] + }, + "tracks_youtube_link_unique": { + "name": "tracks_youtube_link_unique", + "nullsNotDistinct": false, + "columns": [ + "youtube_link" + ] + }, + "tracks_spotify_link_unique": { + "name": "tracks_spotify_link_unique", + "nullsNotDistinct": false, + "columns": [ + "spotify_link" + ] + }, + "tracks_apple_music_link_unique": { + "name": "tracks_apple_music_link_unique", + "nullsNotDistinct": false, + "columns": [ + "apple_music_link" + ] + }, + "tracks_tidal_link_unique": { + "name": "tracks_tidal_link_unique", + "nullsNotDistinct": false, + "columns": [ + "tidal_link" + ] + }, + "tracks_sha256_unique": { + "name": "tracks_sha256_unique", + "nullsNotDistinct": false, + "columns": [ + "sha256" + ] + }, + "tracks_uri_unique": { + "name": "tracks_uri_unique", + "nullsNotDistinct": false, + "columns": [ + "uri" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_albums": { + "name": "user_albums", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "album_id": { + "name": "album_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "scrobbles": { + "name": "scrobbles", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "user_albums_user_id_users_xata_id_fk": { + "name": "user_albums_user_id_users_xata_id_fk", + "tableFrom": "user_albums", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "user_albums_album_id_albums_xata_id_fk": { + "name": "user_albums_album_id_albums_xata_id_fk", + "tableFrom": "user_albums", + "tableTo": "albums", + "columnsFrom": [ + "album_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_albums_uri_unique": { + "name": "user_albums_uri_unique", + "nullsNotDistinct": false, + "columns": [ + "uri" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_artists": { + "name": "user_artists", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "artist_id": { + "name": "artist_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "scrobbles": { + "name": "scrobbles", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "user_artists_user_id_users_xata_id_fk": { + "name": "user_artists_user_id_users_xata_id_fk", + "tableFrom": "user_artists", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "user_artists_artist_id_artists_xata_id_fk": { + "name": "user_artists_artist_id_artists_xata_id_fk", + "tableFrom": "user_artists", + "tableTo": "artists", + "columnsFrom": [ + "artist_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_artists_uri_unique": { + "name": "user_artists_uri_unique", + "nullsNotDistinct": false, + "columns": [ + "uri" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_playlists": { + "name": "user_playlists", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "playlist_id": { + "name": "playlist_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_playlists_user_id_users_xata_id_fk": { + "name": "user_playlists_user_id_users_xata_id_fk", + "tableFrom": "user_playlists", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "user_playlists_playlist_id_playlists_xata_id_fk": { + "name": "user_playlists_playlist_id_playlists_xata_id_fk", + "tableFrom": "user_playlists", + "tableTo": "playlists", + "columnsFrom": [ + "playlist_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_playlists_uri_unique": { + "name": "user_playlists_uri_unique", + "nullsNotDistinct": false, + "columns": [ + "uri" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_tracks": { + "name": "user_tracks", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "track_id": { + "name": "track_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scrobbles": { + "name": "scrobbles", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_tracks_user_id_users_xata_id_fk": { + "name": "user_tracks_user_id_users_xata_id_fk", + "tableFrom": "user_tracks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "user_tracks_track_id_tracks_xata_id_fk": { + "name": "user_tracks_track_id_tracks_xata_id_fk", + "tableFrom": "user_tracks", + "tableTo": "tracks", + "columnsFrom": [ + "track_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_tracks_uri_unique": { + "name": "user_tracks_uri_unique", + "nullsNotDistinct": false, + "columns": [ + "uri" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "did": { + "name": "did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_did_unique": { + "name": "users_did_unique", + "nullsNotDistinct": false, + "columns": [ + "did" + ] + }, + "users_handle_unique": { + "name": "users_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "handle" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webscrobblers": { + "name": "webscrobblers", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uuid": { + "name": "uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "webscrobblers_user_id_users_xata_id_fk": { + "name": "webscrobblers_user_id_users_xata_id_fk", + "tableFrom": "webscrobblers", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/api/drizzle/meta/_journal.json b/apps/api/drizzle/meta/_journal.json index 3749b93d..da919182 100644 --- a/apps/api/drizzle/meta/_journal.json +++ b/apps/api/drizzle/meta/_journal.json @@ -15,6 +15,13 @@ "when": 1766863536746, "tag": "0001_parched_random", "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1767252686993, + "tag": "0002_robust_wong", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/api/lexicons/graph/follow.json b/apps/api/lexicons/graph/follow.json index d97078e3..a839774d 100644 --- a/apps/api/lexicons/graph/follow.json +++ b/apps/api/lexicons/graph/follow.json @@ -18,6 +18,10 @@ "format": "datetime" }, "subject": { + "type": "string", + "format": "did" + }, + "via": { "type": "ref", "ref": "com.atproto.repo.strongRef" } diff --git a/apps/api/lexicons/graph/followAccount.json b/apps/api/lexicons/graph/followAccount.json new file mode 100644 index 00000000..356b08b4 --- /dev/null +++ b/apps/api/lexicons/graph/followAccount.json @@ -0,0 +1,49 @@ +{ + "lexicon": 1, + "id": "app.rocksky.graph.followAccount", + "defs": { + "main": { + "type": "procedure", + "description": "Creates a 'follow' relationship from the authenticated account to a specified account.", + "parameters": { + "type": "params", + "required": [ + "account" + ], + "properties": { + "account": { + "type": "string", + "format": "at-identifier" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "subject", + "followers" + ], + "properties": { + "subject": { + "type": "ref", + "ref": "app.rocksky.actor.defs#profileViewBasic" + }, + "followers": { + "type": "array", + "items": { + "type": "ref", + "ref": "app.rocksky.actor.defs#profileViewBasic" + } + }, + "cursor": { + "type": "string", + "description": "A cursor value to pass to subsequent calls to get the next page of results." + } + } + } + } + } + } +} diff --git a/apps/api/lexicons/graph/unfollowAccount.json b/apps/api/lexicons/graph/unfollowAccount.json new file mode 100644 index 00000000..948a69b6 --- /dev/null +++ b/apps/api/lexicons/graph/unfollowAccount.json @@ -0,0 +1,49 @@ +{ + "lexicon": 1, + "id": "app.rocksky.graph.unfollowAccount", + "defs": { + "main": { + "type": "procedure", + "description": "Removes a 'follow' relationship from the authenticated account to a specified account.", + "parameters": { + "type": "params", + "required": [ + "account" + ], + "properties": { + "account": { + "type": "string", + "format": "at-identifier" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "subject", + "followers" + ], + "properties": { + "subject": { + "type": "ref", + "ref": "app.rocksky.actor.defs#profileViewBasic" + }, + "followers": { + "type": "array", + "items": { + "type": "ref", + "ref": "app.rocksky.actor.defs#profileViewBasic" + } + }, + "cursor": { + "type": "string", + "description": "A cursor value to pass to subsequent calls to get the next page of results." + } + } + } + } + } + } +} diff --git a/apps/api/pkl/defs/graph/follow.pkl b/apps/api/pkl/defs/graph/follow.pkl index 5c83827a..345da6cb 100644 --- a/apps/api/pkl/defs/graph/follow.pkl +++ b/apps/api/pkl/defs/graph/follow.pkl @@ -16,7 +16,12 @@ defs = new Mapping { format = "datetime" } - ["subject"] = new Ref { + ["subject"] = new StringType { + type = "string" + format = "did" + } + + ["via"] = new Ref { type = "ref" ref = "com.atproto.repo.strongRef" } diff --git a/apps/api/pkl/defs/graph/followAccount.pkl b/apps/api/pkl/defs/graph/followAccount.pkl new file mode 100644 index 00000000..ae7503f0 --- /dev/null +++ b/apps/api/pkl/defs/graph/followAccount.pkl @@ -0,0 +1,46 @@ +amends "../../schema/lexicon.pkl" + +lexicon = 1 +id = "app.rocksky.graph.followAccount" +defs = new Mapping { + ["main"] { + type = "procedure" + description = + "Creates a 'follow' relationship from the authenticated account to a specified account." + parameters = new Params { + required = List("account") + properties { + ["account"] = new StringType { + type = "string" + format = "at-identifier" + } + } + } + + output { + encoding = "application/json" + schema = new ObjectType { + type = "object" + required = List("subject", "followers") + properties { + ["subject"] = new Ref { + type = "ref" + ref = "app.rocksky.actor.defs#profileViewBasic" + } + ["followers"] = new Array { + type = "array" + items = new Ref { + type = "ref" + ref = "app.rocksky.actor.defs#profileViewBasic" + } + } + ["cursor"] = new StringType { + type = "string" + description = + "A cursor value to pass to subsequent calls to get the next page of results." + } + } + } + } + } +} diff --git a/apps/api/pkl/defs/graph/unfollowAccount.pkl b/apps/api/pkl/defs/graph/unfollowAccount.pkl new file mode 100644 index 00000000..52dcafec --- /dev/null +++ b/apps/api/pkl/defs/graph/unfollowAccount.pkl @@ -0,0 +1,46 @@ +amends "../../schema/lexicon.pkl" + +lexicon = 1 +id = "app.rocksky.graph.unfollowAccount" +defs = new Mapping { + ["main"] { + type = "procedure" + description = + "Removes a 'follow' relationship from the authenticated account to a specified account." + parameters = new Params { + required = List("account") + properties { + ["account"] = new StringType { + type = "string" + format = "at-identifier" + } + } + } + + output { + encoding = "application/json" + schema = new ObjectType { + type = "object" + required = List("subject", "followers") + properties { + ["subject"] = new Ref { + type = "ref" + ref = "app.rocksky.actor.defs#profileViewBasic" + } + ["followers"] = new Array { + type = "array" + items = new Ref { + type = "ref" + ref = "app.rocksky.actor.defs#profileViewBasic" + } + } + ["cursor"] = new StringType { + type = "string" + description = + "A cursor value to pass to subsequent calls to get the next page of results." + } + } + } + } + } +} diff --git a/apps/api/src/lexicon/index.ts b/apps/api/src/lexicon/index.ts index 1c78ccd5..a1ef62a7 100644 --- a/apps/api/src/lexicon/index.ts +++ b/apps/api/src/lexicon/index.ts @@ -48,9 +48,11 @@ import type * as AppRockskyFeedSearch from "./types/app/rocksky/feed/search"; import type * as AppRockskyGoogledriveDownloadFile from "./types/app/rocksky/googledrive/downloadFile"; import type * as AppRockskyGoogledriveGetFile from "./types/app/rocksky/googledrive/getFile"; import type * as AppRockskyGoogledriveGetFiles from "./types/app/rocksky/googledrive/getFiles"; +import type * as AppRockskyGraphFollowAccount from "./types/app/rocksky/graph/followAccount"; import type * as AppRockskyGraphGetFollowers from "./types/app/rocksky/graph/getFollowers"; import type * as AppRockskyGraphGetFollows from "./types/app/rocksky/graph/getFollows"; import type * as AppRockskyGraphGetKnownFollowers from "./types/app/rocksky/graph/getKnownFollowers"; +import type * as AppRockskyGraphUnfollowAccount from "./types/app/rocksky/graph/unfollowAccount"; import type * as AppRockskyLikeDislikeShout from "./types/app/rocksky/like/dislikeShout"; import type * as AppRockskyLikeDislikeSong from "./types/app/rocksky/like/dislikeSong"; import type * as AppRockskyLikeLikeShout from "./types/app/rocksky/like/likeShout"; @@ -717,6 +719,17 @@ export class AppRockskyGraphNS { this._server = server; } + followAccount( + cfg: ConfigOf< + AV, + AppRockskyGraphFollowAccount.Handler>, + AppRockskyGraphFollowAccount.HandlerReqCtx> + >, + ) { + const nsid = "app.rocksky.graph.followAccount"; // @ts-ignore + return this._server.xrpc.method(nsid, cfg); + } + getFollowers( cfg: ConfigOf< AV, @@ -749,6 +762,17 @@ export class AppRockskyGraphNS { const nsid = "app.rocksky.graph.getKnownFollowers"; // @ts-ignore return this._server.xrpc.method(nsid, cfg); } + + unfollowAccount( + cfg: ConfigOf< + AV, + AppRockskyGraphUnfollowAccount.Handler>, + AppRockskyGraphUnfollowAccount.HandlerReqCtx> + >, + ) { + const nsid = "app.rocksky.graph.unfollowAccount"; // @ts-ignore + return this._server.xrpc.method(nsid, cfg); + } } export class AppRockskyLikeNS { diff --git a/apps/api/src/lexicon/lexicons.ts b/apps/api/src/lexicon/lexicons.ts index 7eed1eed..5988ab39 100644 --- a/apps/api/src/lexicon/lexicons.ts +++ b/apps/api/src/lexicon/lexicons.ts @@ -2799,6 +2799,10 @@ export const schemaDict = { format: "datetime", }, subject: { + type: "string", + format: "did", + }, + via: { type: "ref", ref: "lex:com.atproto.repo.strongRef", }, @@ -2807,6 +2811,52 @@ export const schemaDict = { }, }, }, + AppRockskyGraphFollowAccount: { + lexicon: 1, + id: "app.rocksky.graph.followAccount", + defs: { + main: { + type: "procedure", + description: + "Creates a 'follow' relationship from the authenticated account to a specified account.", + parameters: { + type: "params", + required: ["account"], + properties: { + account: { + type: "string", + format: "at-identifier", + }, + }, + }, + output: { + encoding: "application/json", + schema: { + type: "object", + required: ["subject", "followers"], + properties: { + subject: { + type: "ref", + ref: "lex:app.rocksky.actor.defs#profileViewBasic", + }, + followers: { + type: "array", + items: { + type: "ref", + ref: "lex:app.rocksky.actor.defs#profileViewBasic", + }, + }, + cursor: { + type: "string", + description: + "A cursor value to pass to subsequent calls to get the next page of results.", + }, + }, + }, + }, + }, + }, + }, AppRockskyGraphGetFollowers: { lexicon: 1, id: "app.rocksky.graph.getFollowers", @@ -2972,6 +3022,52 @@ export const schemaDict = { }, }, }, + AppRockskyGraphUnfollowAccount: { + lexicon: 1, + id: "app.rocksky.graph.unfollowAccount", + defs: { + main: { + type: "procedure", + description: + "Removes a 'follow' relationship from the authenticated account to a specified account.", + parameters: { + type: "params", + required: ["account"], + properties: { + account: { + type: "string", + format: "at-identifier", + }, + }, + }, + output: { + encoding: "application/json", + schema: { + type: "object", + required: ["subject", "followers"], + properties: { + subject: { + type: "ref", + ref: "lex:app.rocksky.actor.defs#profileViewBasic", + }, + followers: { + type: "array", + items: { + type: "ref", + ref: "lex:app.rocksky.actor.defs#profileViewBasic", + }, + }, + cursor: { + type: "string", + description: + "A cursor value to pass to subsequent calls to get the next page of results.", + }, + }, + }, + }, + }, + }, + }, AppRockskyLikeDislikeShout: { lexicon: 1, id: "app.rocksky.like.dislikeShout", @@ -5634,9 +5730,11 @@ export const ids = { AppRockskyGoogledriveGetFiles: "app.rocksky.googledrive.getFiles", AppRockskyGraphDefs: "app.rocksky.graph.defs", AppRockskyGraphFollow: "app.rocksky.graph.follow", + AppRockskyGraphFollowAccount: "app.rocksky.graph.followAccount", AppRockskyGraphGetFollowers: "app.rocksky.graph.getFollowers", AppRockskyGraphGetFollows: "app.rocksky.graph.getFollows", AppRockskyGraphGetKnownFollowers: "app.rocksky.graph.getKnownFollowers", + AppRockskyGraphUnfollowAccount: "app.rocksky.graph.unfollowAccount", AppRockskyLikeDislikeShout: "app.rocksky.like.dislikeShout", AppRockskyLikeDislikeSong: "app.rocksky.like.dislikeSong", AppRockskyLike: "app.rocksky.like", diff --git a/apps/api/src/lexicon/types/app/rocksky/graph/follow.ts b/apps/api/src/lexicon/types/app/rocksky/graph/follow.ts index eec9ecc0..8808b783 100644 --- a/apps/api/src/lexicon/types/app/rocksky/graph/follow.ts +++ b/apps/api/src/lexicon/types/app/rocksky/graph/follow.ts @@ -9,7 +9,8 @@ import type * as ComAtprotoRepoStrongRef from "../../../com/atproto/repo/strongR export interface Record { createdAt: string; - subject: ComAtprotoRepoStrongRef.Main; + subject: string; + via?: ComAtprotoRepoStrongRef.Main; [k: string]: unknown; } diff --git a/apps/api/src/lexicon/types/app/rocksky/graph/followAccount.ts b/apps/api/src/lexicon/types/app/rocksky/graph/followAccount.ts new file mode 100644 index 00000000..99fc07a3 --- /dev/null +++ b/apps/api/src/lexicon/types/app/rocksky/graph/followAccount.ts @@ -0,0 +1,50 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import type express from "express"; +import { ValidationResult, BlobRef } from "@atproto/lexicon"; +import { lexicons } from "../../../../lexicons"; +import { isObj, hasProp } from "../../../../util"; +import { CID } from "multiformats/cid"; +import type { HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server"; +import type * as AppRockskyActorDefs from "../actor/defs"; + +export interface QueryParams { + account: string; +} + +export type InputSchema = undefined; + +export interface OutputSchema { + subject: AppRockskyActorDefs.ProfileViewBasic; + followers: AppRockskyActorDefs.ProfileViewBasic[]; + /** A cursor value to pass to subsequent calls to get the next page of results. */ + cursor?: string; + [k: string]: unknown; +} + +export type HandlerInput = undefined; + +export interface HandlerSuccess { + encoding: "application/json"; + body: OutputSchema; + headers?: { [key: string]: string }; +} + +export interface HandlerError { + status: number; + message?: string; +} + +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough; +export type HandlerReqCtx = { + auth: HA; + params: QueryParams; + input: HandlerInput; + req: express.Request; + res: express.Response; + resetRouteRateLimits: () => Promise; +}; +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput; diff --git a/apps/api/src/lexicon/types/app/rocksky/graph/unfollowAccount.ts b/apps/api/src/lexicon/types/app/rocksky/graph/unfollowAccount.ts new file mode 100644 index 00000000..99fc07a3 --- /dev/null +++ b/apps/api/src/lexicon/types/app/rocksky/graph/unfollowAccount.ts @@ -0,0 +1,50 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import type express from "express"; +import { ValidationResult, BlobRef } from "@atproto/lexicon"; +import { lexicons } from "../../../../lexicons"; +import { isObj, hasProp } from "../../../../util"; +import { CID } from "multiformats/cid"; +import type { HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server"; +import type * as AppRockskyActorDefs from "../actor/defs"; + +export interface QueryParams { + account: string; +} + +export type InputSchema = undefined; + +export interface OutputSchema { + subject: AppRockskyActorDefs.ProfileViewBasic; + followers: AppRockskyActorDefs.ProfileViewBasic[]; + /** A cursor value to pass to subsequent calls to get the next page of results. */ + cursor?: string; + [k: string]: unknown; +} + +export type HandlerInput = undefined; + +export interface HandlerSuccess { + encoding: "application/json"; + body: OutputSchema; + headers?: { [key: string]: string }; +} + +export interface HandlerError { + status: number; + message?: string; +} + +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough; +export type HandlerReqCtx = { + auth: HA; + params: QueryParams; + input: HandlerInput; + req: express.Request; + res: express.Response; + resetRouteRateLimits: () => Promise; +}; +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput; diff --git a/apps/api/src/schema/follows.ts b/apps/api/src/schema/follows.ts new file mode 100644 index 00000000..9f21dca5 --- /dev/null +++ b/apps/api/src/schema/follows.ts @@ -0,0 +1,33 @@ +import type { InferInsertModel, InferSelectModel } from "drizzle-orm"; +import { sql } from "drizzle-orm"; +import { + integer, + pgTable, + text, + timestamp, + uniqueIndex, +} from "drizzle-orm/pg-core"; + +const follows = pgTable( + "follows", + { + id: text("xata_id").primaryKey().default(sql`xata_id()`), + uri: text("uri").notNull().unique(), + follower_did: text("follower_did").notNull(), + subject_did: text("subject_did").notNull(), + xataVersion: integer("xata_version"), + createdAt: timestamp("xata_createdat").defaultNow().notNull(), + updatedAt: timestamp("xata_updatedat").defaultNow().notNull(), + }, + (t) => [ + uniqueIndex("follows_follower_subject_unique").on( + t.follower_did, + t.subject_did, + ), + ], +); + +export type SelectFollows = InferSelectModel; +export type InsertFollows = InferInsertModel; + +export default follows; diff --git a/apps/api/src/schema/index.ts b/apps/api/src/schema/index.ts index dd35dc48..68a8a1c4 100644 --- a/apps/api/src/schema/index.ts +++ b/apps/api/src/schema/index.ts @@ -10,6 +10,7 @@ import dropboxDirectories from "./dropbox-directories"; import dropboxPaths from "./dropbox-paths"; import dropboxTokens from "./dropbox-tokens"; import feeds from "./feeds"; +import follows from "./follows"; import googleDriveAccounts from "./google-drive-accounts"; import googleDriveDirectories from "./google-drive-directories"; import googleDrivePaths from "./google-drive-paths"; @@ -70,4 +71,5 @@ export default { googleDrive, queueTracks, feeds, + follows, }; diff --git a/apps/api/src/scripts/avatar.ts b/apps/api/src/scripts/avatar.ts index 8ceee14c..79bcb354 100644 --- a/apps/api/src/scripts/avatar.ts +++ b/apps/api/src/scripts/avatar.ts @@ -55,10 +55,7 @@ async function processUser(user: SelectUser) { }; console.log(userPayload); - await ctx.nc.publish( - "rocksky.user", - Buffer.from(JSON.stringify(userPayload)), - ); + ctx.nc.publish("rocksky.user", Buffer.from(JSON.stringify(userPayload))); } if (args.length > 0) { diff --git a/apps/api/src/xrpc/app/rocksky/graph/followAccount.ts b/apps/api/src/xrpc/app/rocksky/graph/followAccount.ts new file mode 100644 index 00000000..b489fb9a --- /dev/null +++ b/apps/api/src/xrpc/app/rocksky/graph/followAccount.ts @@ -0,0 +1,69 @@ +import type { HandlerAuth } from "@atproto/xrpc-server"; +import type { Context } from "context"; +import { eq } from "drizzle-orm"; +import { Effect, pipe } from "effect"; +import type { Server } from "lexicon"; +import { ProfileViewBasic } from "lexicon/types/app/rocksky/actor/defs"; +import type { QueryParams } from "lexicon/types/app/rocksky/graph/followAccount"; +import tables from "schema"; + +export default function (server: Server, ctx: Context) { + const followAccount = (params: QueryParams, auth: HandlerAuth) => + pipe( + { params, ctx, did: auth.credentials?.did }, + handleFollow, + Effect.flatMap(presentation), + Effect.retry({ times: 3 }), + Effect.timeout("10 seconds"), + Effect.catchAll((err) => { + console.error(err); + return Effect.succeed({ + subject: {} satisfies ProfileViewBasic, + followers: [], + }); + }), + ); + server.app.rocksky.graph.followAccount({ + auth: ctx.authVerifier, + handler: async ({ params, auth }) => { + const result = await Effect.runPromise(followAccount(params, auth)); + return { + encoding: "application/json", + body: result, + }; + }, + }); +} + +const handleFollow = ({ + params, + ctx, + did, +}: { + params: QueryParams; + ctx: Context; + did?: string; +}): Effect.Effect => { + return Effect.tryPromise({ + try: async () => { + if (!did) { + throw new Error("User is not authenticated"); + } + return []; + }, + catch: (error) => new Error(`Failed to retrieve follow: ${error}`), + }); +}; + +const presentation = ( + followers: any[], +): Effect.Effect< + { subject: ProfileViewBasic; followers: ProfileViewBasic[] }, + never +> => { + // Logic to format the response for play action + return Effect.sync(() => ({ + subject: {} satisfies ProfileViewBasic, + followers: [], + })); +}; diff --git a/apps/api/src/xrpc/app/rocksky/graph/getFollowers.ts b/apps/api/src/xrpc/app/rocksky/graph/getFollowers.ts new file mode 100644 index 00000000..598fff02 --- /dev/null +++ b/apps/api/src/xrpc/app/rocksky/graph/getFollowers.ts @@ -0,0 +1,79 @@ +import type { Context } from "context"; +import { eq, or, sql } from "drizzle-orm"; +import { Effect, pipe } from "effect"; +import type { Server } from "lexicon"; +import type { QueryParams } from "lexicon/types/app/rocksky/graph/getFollowers"; +import type { ProfileViewBasic } from "lexicon/types/app/rocksky/actor/defs"; +import tables from "schema"; + +export default function (server: Server, ctx: Context) { + const getFollowers = (params: QueryParams) => + pipe( + { params, ctx }, + retrieve, + Effect.flatMap(presentation), + Effect.retry({ times: 3 }), + Effect.timeout("120 seconds"), + Effect.catchAll((err) => { + console.error(err); + return Effect.succeed({ + subject: {} satisfies ProfileViewBasic, + followers: [] as ProfileViewBasic[], + }); + }), + ); + server.app.rocksky.graph.getFollowers({ + handler: async ({ params }) => { + const result = await Effect.runPromise(getFollowers(params)); + return { + encoding: "application/json", + body: result, + }; + }, + }); +} + +const retrieve = ({ + params, + ctx, +}: { + params: QueryParams; + ctx: Context; +}): Effect.Effect => { + return Effect.tryPromise({ + try: () => + ctx.db + .select() + .from(tables.follows) + .where(eq(tables.follows.subject_did, params.actor)) + .leftJoin( + tables.users, + eq(tables.users.did, tables.follows.follower_did), + ) + .execute() + .then((rows) => + rows.map(({ users }) => ({ + id: users.id, + did: users.did, + handle: users.handle, + displayName: users.displayName, + avatar: users.avatar, + createdAt: users.createdAt.toISOString(), + updatedAt: users.updatedAt.toISOString(), + })), + ), + catch: (error) => new Error(`Failed to retrieve user followers: ${error}`), + }); +}; + +const presentation = ( + followers: any[], +): Effect.Effect< + { subject: ProfileViewBasic; followers: ProfileViewBasic[] }, + never +> => { + return Effect.sync(() => ({ + subject: {} satisfies ProfileViewBasic, + followers: [], + })); +}; diff --git a/apps/api/src/xrpc/app/rocksky/graph/getFollows.ts b/apps/api/src/xrpc/app/rocksky/graph/getFollows.ts new file mode 100644 index 00000000..b4130108 --- /dev/null +++ b/apps/api/src/xrpc/app/rocksky/graph/getFollows.ts @@ -0,0 +1,79 @@ +import type { Context } from "context"; +import { eq, or, sql } from "drizzle-orm"; +import { Effect, pipe } from "effect"; +import type { Server } from "lexicon"; +import type { QueryParams } from "lexicon/types/app/rocksky/graph/getFollowers"; +import type { ProfileViewBasic } from "lexicon/types/app/rocksky/actor/defs"; +import tables from "schema"; + +export default function (server: Server, ctx: Context) { + const getFollows = (params) => + pipe( + { params, ctx }, + retrieve, + Effect.flatMap(presentation), + Effect.retry({ times: 3 }), + Effect.timeout("120 seconds"), + Effect.catchAll((err) => { + console.error(err); + return Effect.succeed({ + subject: {} satisfies ProfileViewBasic, + follows: [], + }); + }), + ); + server.app.rocksky.graph.getFollows({ + handler: async ({ params }) => { + const result = await Effect.runPromise(getFollows(params)); + return { + encoding: "application/json", + body: result, + }; + }, + }); +} + +const retrieve = ({ + params, + ctx, +}: { + params: QueryParams; + ctx: Context; +}): Effect.Effect => { + return Effect.tryPromise({ + try: () => + ctx.db + .select() + .from(tables.follows) + .where(eq(tables.follows.follower_did, params.actor)) + .leftJoin( + tables.users, + eq(tables.users.did, tables.follows.follower_did), + ) + .execute() + .then((rows) => + rows.map(({ users }) => ({ + id: users.id, + did: users.did, + handle: users.handle, + displayName: users.displayName, + avatar: users.avatar, + createdAt: users.createdAt.toISOString(), + updatedAt: users.updatedAt.toISOString(), + })), + ), + catch: (error) => new Error(`Failed to retrieve user follows: ${error}`), + }); +}; + +const presentation = ( + followers: any[], +): Effect.Effect< + { subject: ProfileViewBasic; follows: ProfileViewBasic[] }, + never +> => { + return Effect.sync(() => ({ + subject: {} satisfies ProfileViewBasic, + follows: [], + })); +}; diff --git a/apps/api/src/xrpc/app/rocksky/graph/getKnownFollowers.ts b/apps/api/src/xrpc/app/rocksky/graph/getKnownFollowers.ts new file mode 100644 index 00000000..0c349c7e --- /dev/null +++ b/apps/api/src/xrpc/app/rocksky/graph/getKnownFollowers.ts @@ -0,0 +1,63 @@ +import type { Context } from "context"; +import { eq, or, sql } from "drizzle-orm"; +import { Effect, pipe } from "effect"; +import type { Server } from "lexicon"; +import type { QueryParams } from "lexicon/types/app/rocksky/graph/getKnownFollowers"; +import type { ProfileViewBasic } from "lexicon/types/app/rocksky/actor/defs"; +import tables from "schema"; + +export default function (server: Server, ctx: Context) { + const getKnownFollowers = (params) => + pipe( + { params, ctx }, + retrieve, + Effect.flatMap(presentation), + Effect.retry({ times: 3 }), + Effect.timeout("120 seconds"), + Effect.catchAll((err) => { + console.error(err); + return Effect.succeed({ + subject: {} satisfies ProfileViewBasic, + followers: [] as ProfileViewBasic[], + }); + }), + ); + server.app.rocksky.graph.getKnownFollowers({ + handler: async ({ params }) => { + const result = await Effect.runPromise(getKnownFollowers(params)); + return { + encoding: "application/json", + body: { + ...result, + subject: {} satisfies ProfileViewBasic, + }, + }; + }, + }); +} + +const retrieve = ({ + params, + ctx, +}: { + params: QueryParams; + ctx: Context; +}): Effect.Effect => { + return Effect.tryPromise({ + try: async () => [], + catch: (error) => + new Error(`Failed to retrieve user known followers: ${error}`), + }); +}; + +const presentation = ( + followers: any[], +): Effect.Effect< + { subject: ProfileViewBasic; followers: ProfileViewBasic[] }, + never +> => { + return Effect.sync(() => ({ + subject: {} satisfies ProfileViewBasic, + followers: [], + })); +}; diff --git a/apps/api/src/xrpc/app/rocksky/graph/unfollowAccount.ts b/apps/api/src/xrpc/app/rocksky/graph/unfollowAccount.ts new file mode 100644 index 00000000..a09d5139 --- /dev/null +++ b/apps/api/src/xrpc/app/rocksky/graph/unfollowAccount.ts @@ -0,0 +1,64 @@ +import type { HandlerAuth } from "@atproto/xrpc-server"; +import type { Context } from "context"; +import { eq } from "drizzle-orm"; +import { Effect, pipe } from "effect"; +import type { Server } from "lexicon"; +import { ProfileViewBasic } from "lexicon/types/app/rocksky/actor/defs"; +import type { QueryParams } from "lexicon/types/app/rocksky/graph/unfollowAccount"; +import tables from "schema"; + +export default function (server: Server, ctx: Context) { + const unfollowAccount = (params: QueryParams, auth: HandlerAuth) => + pipe( + { params, ctx, did: auth.credentials?.did }, + handleFollow, + Effect.flatMap(presentation), + Effect.retry({ times: 3 }), + Effect.timeout("10 seconds"), + Effect.catchAll((err) => { + console.error(err); + return Effect.succeed({ + subject: {} satisfies ProfileViewBasic, + followers: [], + }); + }), + ); + server.app.rocksky.graph.followAccount({ + auth: ctx.authVerifier, + handler: async ({ params, auth }) => { + const result = await Effect.runPromise(unfollowAccount(params, auth)); + return { + encoding: "application/json", + body: result, + }; + }, + }); +} + +const handleFollow = ({ + params, + ctx, + did, +}: { + params: QueryParams; + ctx: Context; + did?: string; +}): Effect.Effect => { + return Effect.tryPromise({ + try: async () => [], + catch: (error) => new Error(`Failed to unfollow: ${error}`), + }); +}; + +const presentation = ( + followers: any[], +): Effect.Effect< + { subject: ProfileViewBasic; followers: ProfileViewBasic[] }, + never +> => { + // Logic to format the response for play action + return Effect.sync(() => ({ + subject: {} satisfies ProfileViewBasic, + followers: [], + })); +}; diff --git a/apps/web/src/components/SongCover/InteractionBar/InteractionBar.tsx b/apps/web/src/components/SongCover/InteractionBar/InteractionBar.tsx index 73b41946..6f1da280 100644 --- a/apps/web/src/components/SongCover/InteractionBar/InteractionBar.tsx +++ b/apps/web/src/components/SongCover/InteractionBar/InteractionBar.tsx @@ -23,7 +23,7 @@ function InteractionBar({ likesCount, liked, onLike }: InteractionBarProps) { {liked && } {likesCount > 0 && ( - + {likesCount} )} diff --git a/crates/jetstream/src/repo.rs b/crates/jetstream/src/repo.rs index 92530c53..5a30527f 100644 --- a/crates/jetstream/src/repo.rs +++ b/crates/jetstream/src/repo.rs @@ -8,8 +8,13 @@ use tokio::sync::Mutex; use crate::{ profile::did_to_profile, - subscriber::{ALBUM_NSID, ARTIST_NSID, FEED_GENERATOR_NSID, SCROBBLE_NSID, SONG_NSID}, - types::{AlbumRecord, ArtistRecord, Commit, FeedGeneratorRecord, ScrobbleRecord, SongRecord}, + subscriber::{ + ALBUM_NSID, ARTIST_NSID, FEED_GENERATOR_NSID, FOLLOW_NSID, SCROBBLE_NSID, SONG_NSID, + }, + types::{ + AlbumRecord, ArtistRecord, Commit, FeedGeneratorRecord, FollowRecord, ScrobbleRecord, + SongRecord, + }, webhook::discord::{ self, model::{ScrobbleData, WebhookEnvelope}, @@ -35,6 +40,7 @@ pub async fn save_scrobble( ALBUM_NSID, SONG_NSID, FEED_GENERATOR_NSID, + FOLLOW_NSID, ] .contains(&commit.collection.as_str()) { @@ -186,11 +192,24 @@ pub async fn save_scrobble( let uri = format!("at://{}/app.rocksky.feed.generator/{}", did, commit.rkey); let feed_generator_record: FeedGeneratorRecord = - serde_json::from_value(commit.record)?; + serde_json::from_value(commit.record.clone())?; save_feed_generator(&mut tx, &user_id, feed_generator_record, &uri).await?; tx.commit().await?; } + + if commit.collection == FOLLOW_NSID { + let mut tx = pool.begin().await?; + + save_user(&mut tx, did).await?; + let uri = format!("at://{}/app.rocksky.graph.follow/{}", did, commit.rkey); + + let follow_record: FollowRecord = serde_json::from_value(commit.record)?; + save_user(&mut tx, &follow_record.subject).await?; + save_follow(&mut tx, did, follow_record, &uri).await?; + + tx.commit().await?; + } } _ => { tracing::warn!(operation = %commit.operation, "Unsupported operation"); @@ -1076,3 +1095,30 @@ pub async fn save_feed_generator( .await?; Ok(()) } + +pub async fn save_follow( + tx: &mut sqlx::Transaction<'_, Postgres>, + did: &str, + record: FollowRecord, + uri: &str, +) -> Result<(), Error> { + tracing::info!(did = %did, uri = %uri, "Saving follow"); + + sqlx::query( + r#" + INSERT INTO follows ( + follower_did, + subject_did, + uri + ) VALUES ( + $1, $2, $3 + ) + "#, + ) + .bind(did) + .bind(record.subject) + .bind(uri) + .execute(&mut **tx) + .await?; + Ok(()) +} diff --git a/crates/jetstream/src/subscriber.rs b/crates/jetstream/src/subscriber.rs index 8208b319..1ef4f032 100644 --- a/crates/jetstream/src/subscriber.rs +++ b/crates/jetstream/src/subscriber.rs @@ -17,6 +17,7 @@ pub const PLAYLIST_NSID: &str = "app.rocksky.playlist"; pub const LIKE_NSID: &str = "app.rocksky.like"; pub const SHOUT_NSID: &str = "app.rocksky.shout"; pub const FEED_GENERATOR_NSID: &str = "app.rocksky.feed.generator"; +pub const FOLLOW_NSID: &str = "app.rocksky.graph.follow"; pub struct ScrobbleSubscriber { pub service_url: String, diff --git a/crates/jetstream/src/types.rs b/crates/jetstream/src/types.rs index 2eafda6b..31ebcaaf 100644 --- a/crates/jetstream/src/types.rs +++ b/crates/jetstream/src/types.rs @@ -244,3 +244,10 @@ pub struct FeedGeneratorRecord { pub did: String, pub created_at: String, } + +#[derive(Debug, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct FollowRecord { + pub subject: String, + pub created_at: String, +} diff --git a/crates/jetstream/src/xata/follow.rs b/crates/jetstream/src/xata/follow.rs new file mode 100644 index 00000000..59aa7dec --- /dev/null +++ b/crates/jetstream/src/xata/follow.rs @@ -0,0 +1,12 @@ +use chrono::{DateTime, Utc}; +use serde::Deserialize; + +#[derive(Debug, sqlx::FromRow, Deserialize, Clone)] +pub struct Follow { + pub xata_id: String, + pub uri: String, + pub follower_did: String, + pub subject_did: String, + #[serde(with = "chrono::serde::ts_seconds")] + pub xata_createdat: DateTime, +} diff --git a/crates/jetstream/src/xata/mod.rs b/crates/jetstream/src/xata/mod.rs index 6ff157d4..e9986411 100644 --- a/crates/jetstream/src/xata/mod.rs +++ b/crates/jetstream/src/xata/mod.rs @@ -4,6 +4,7 @@ pub mod artist; pub mod artist_album; pub mod artist_track; pub mod feed; +pub mod follow; pub mod loved_track; pub mod scrobble; pub mod track; -- 2.51.2