diff --git a/apps/api/drizzle/0017_comments.sql b/apps/api/drizzle/0017_comments.sql
new file mode 100644
index 0000000..186703c
--- /dev/null
+++ b/apps/api/drizzle/0017_comments.sql
@@ -0,0 +1,16 @@
+CREATE TABLE "comments" (
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+ "listen_id" uuid NOT NULL,
+ "user_id" uuid NOT NULL,
+ "body" varchar(500) NOT NULL,
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL
+);
+--> statement-breakpoint
+ALTER TABLE "notifications" ADD COLUMN "comment_id" uuid;--> statement-breakpoint
+ALTER TABLE "reports" ADD COLUMN "comment_id" uuid;--> statement-breakpoint
+ALTER TABLE "reports" ADD COLUMN "comment_body" varchar(500);--> statement-breakpoint
+ALTER TABLE "comments" ADD CONSTRAINT "comments_listen_id_listens_id_fk" FOREIGN KEY ("listen_id") REFERENCES "public"."listens"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "comments" ADD CONSTRAINT "comments_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+CREATE INDEX "comments_listen_created_idx" ON "comments" USING btree ("listen_id","created_at");--> statement-breakpoint
+ALTER TABLE "notifications" ADD CONSTRAINT "notifications_comment_id_comments_id_fk" FOREIGN KEY ("comment_id") REFERENCES "public"."comments"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "reports" ADD CONSTRAINT "reports_comment_id_comments_id_fk" FOREIGN KEY ("comment_id") REFERENCES "public"."comments"("id") ON DELETE set null ON UPDATE no action;
\ No newline at end of file
diff --git a/apps/api/drizzle/meta/0017_snapshot.json b/apps/api/drizzle/meta/0017_snapshot.json
new file mode 100644
index 0000000..474acb0
--- /dev/null
+++ b/apps/api/drizzle/meta/0017_snapshot.json
@@ -0,0 +1,1977 @@
+{
+ "id": "ec354816-b5e6-4f80-ad3c-1db8a40b23cd",
+ "prevId": "08aa0049-d19c-4d78-be82-c577de3d211c",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.avatars": {
+ "name": "avatars",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "image": {
+ "name": "image",
+ "type": "bytea",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content_type": {
+ "name": "content_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "avatars_user_id_users_id_fk": {
+ "name": "avatars_user_id_users_id_fk",
+ "tableFrom": "avatars",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.blocks": {
+ "name": "blocks",
+ "schema": "",
+ "columns": {
+ "blocker_id": {
+ "name": "blocker_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "blocked_id": {
+ "name": "blocked_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "blocks_blocked_idx": {
+ "name": "blocks_blocked_idx",
+ "columns": [
+ {
+ "expression": "blocked_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "blocks_blocker_id_users_id_fk": {
+ "name": "blocks_blocker_id_users_id_fk",
+ "tableFrom": "blocks",
+ "tableTo": "users",
+ "columnsFrom": [
+ "blocker_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "blocks_blocked_id_users_id_fk": {
+ "name": "blocks_blocked_id_users_id_fk",
+ "tableFrom": "blocks",
+ "tableTo": "users",
+ "columnsFrom": [
+ "blocked_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "blocks_blocker_id_blocked_id_pk": {
+ "name": "blocks_blocker_id_blocked_id_pk",
+ "columns": [
+ "blocker_id",
+ "blocked_id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.comments": {
+ "name": "comments",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "listen_id": {
+ "name": "listen_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "body": {
+ "name": "body",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "comments_listen_created_idx": {
+ "name": "comments_listen_created_idx",
+ "columns": [
+ {
+ "expression": "listen_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "comments_listen_id_listens_id_fk": {
+ "name": "comments_listen_id_listens_id_fk",
+ "tableFrom": "comments",
+ "tableTo": "listens",
+ "columnsFrom": [
+ "listen_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "comments_user_id_users_id_fk": {
+ "name": "comments_user_id_users_id_fk",
+ "tableFrom": "comments",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.connections": {
+ "name": "connections",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "external_user_id": {
+ "name": "external_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "display_name": {
+ "name": "display_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "access_token_enc": {
+ "name": "access_token_enc",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token_enc": {
+ "name": "refresh_token_enc",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "token_expires_at": {
+ "name": "token_expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'active'"
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_polled_at": {
+ "name": "last_polled_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_activity_at": {
+ "name": "last_activity_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "meta": {
+ "name": "meta",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "webhook_secret": {
+ "name": "webhook_secret",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "connections_user_provider_idx": {
+ "name": "connections_user_provider_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "connections_user_id_users_id_fk": {
+ "name": "connections_user_id_users_id_fk",
+ "tableFrom": "connections",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "connections_webhook_secret_unique": {
+ "name": "connections_webhook_secret_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "webhook_secret"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.devices": {
+ "name": "devices",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token_hash": {
+ "name": "token_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "platform": {
+ "name": "platform",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "last_seen_at": {
+ "name": "last_seen_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "devices_user_idx": {
+ "name": "devices_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "devices_user_id_users_id_fk": {
+ "name": "devices_user_id_users_id_fk",
+ "tableFrom": "devices",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "devices_token_hash_unique": {
+ "name": "devices_token_hash_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "token_hash"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.email_verifications": {
+ "name": "email_verifications",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "varchar(320)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "email_verifications_user_idx": {
+ "name": "email_verifications_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "email_verifications_user_id_users_id_fk": {
+ "name": "email_verifications_user_id_users_id_fk",
+ "tableFrom": "email_verifications",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.featured_songs": {
+ "name": "featured_songs",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "track_id": {
+ "name": "track_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "comment": {
+ "name": "comment",
+ "type": "varchar(280)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "featured_songs_user_id_users_id_fk": {
+ "name": "featured_songs_user_id_users_id_fk",
+ "tableFrom": "featured_songs",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "featured_songs_track_id_tracks_id_fk": {
+ "name": "featured_songs_track_id_tracks_id_fk",
+ "tableFrom": "featured_songs",
+ "tableTo": "tracks",
+ "columnsFrom": [
+ "track_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.follows": {
+ "name": "follows",
+ "schema": "",
+ "columns": {
+ "follower_id": {
+ "name": "follower_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "followee_id": {
+ "name": "followee_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "pinned_at": {
+ "name": "pinned_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "follows_followee_idx": {
+ "name": "follows_followee_idx",
+ "columns": [
+ {
+ "expression": "followee_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "follows_follower_id_users_id_fk": {
+ "name": "follows_follower_id_users_id_fk",
+ "tableFrom": "follows",
+ "tableTo": "users",
+ "columnsFrom": [
+ "follower_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "follows_followee_id_users_id_fk": {
+ "name": "follows_followee_id_users_id_fk",
+ "tableFrom": "follows",
+ "tableTo": "users",
+ "columnsFrom": [
+ "followee_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "follows_follower_id_followee_id_pk": {
+ "name": "follows_follower_id_followee_id_pk",
+ "columns": [
+ "follower_id",
+ "followee_id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.listens": {
+ "name": "listens",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "track_id": {
+ "name": "track_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source": {
+ "name": "source",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "listens_user_started_idx": {
+ "name": "listens_user_started_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "listens_user_id_users_id_fk": {
+ "name": "listens_user_id_users_id_fk",
+ "tableFrom": "listens",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "listens_track_id_tracks_id_fk": {
+ "name": "listens_track_id_tracks_id_fk",
+ "tableFrom": "listens",
+ "tableTo": "tracks",
+ "columnsFrom": [
+ "track_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.lyrics": {
+ "name": "lyrics",
+ "schema": "",
+ "columns": {
+ "track_id": {
+ "name": "track_id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "synced": {
+ "name": "synced",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "plain": {
+ "name": "plain",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "instrumental": {
+ "name": "instrumental",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "source_url": {
+ "name": "source_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "fetched_at": {
+ "name": "fetched_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "lyrics_track_id_tracks_id_fk": {
+ "name": "lyrics_track_id_tracks_id_fk",
+ "tableFrom": "lyrics",
+ "tableTo": "tracks",
+ "columnsFrom": [
+ "track_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.notifications": {
+ "name": "notifications",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "actor_id": {
+ "name": "actor_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "track_id": {
+ "name": "track_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "listen_id": {
+ "name": "listen_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "comment_id": {
+ "name": "comment_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "emoji": {
+ "name": "emoji",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "message": {
+ "name": "message",
+ "type": "varchar(280)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "read_at": {
+ "name": "read_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "notifications_user_created_idx": {
+ "name": "notifications_user_created_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "notifications_user_id_users_id_fk": {
+ "name": "notifications_user_id_users_id_fk",
+ "tableFrom": "notifications",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "notifications_actor_id_users_id_fk": {
+ "name": "notifications_actor_id_users_id_fk",
+ "tableFrom": "notifications",
+ "tableTo": "users",
+ "columnsFrom": [
+ "actor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "notifications_track_id_tracks_id_fk": {
+ "name": "notifications_track_id_tracks_id_fk",
+ "tableFrom": "notifications",
+ "tableTo": "tracks",
+ "columnsFrom": [
+ "track_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "notifications_listen_id_listens_id_fk": {
+ "name": "notifications_listen_id_listens_id_fk",
+ "tableFrom": "notifications",
+ "tableTo": "listens",
+ "columnsFrom": [
+ "listen_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "notifications_comment_id_comments_id_fk": {
+ "name": "notifications_comment_id_comments_id_fk",
+ "tableFrom": "notifications",
+ "tableTo": "comments",
+ "columnsFrom": [
+ "comment_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.now_playing": {
+ "name": "now_playing",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "track_id": {
+ "name": "track_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source": {
+ "name": "source",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_app": {
+ "name": "source_app",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source_url": {
+ "name": "source_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_playing": {
+ "name": "is_playing",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "progress_ms": {
+ "name": "progress_ms",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "now_playing_track_idx": {
+ "name": "now_playing_track_idx",
+ "columns": [
+ {
+ "expression": "track_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "now_playing_user_id_users_id_fk": {
+ "name": "now_playing_user_id_users_id_fk",
+ "tableFrom": "now_playing",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "now_playing_track_id_tracks_id_fk": {
+ "name": "now_playing_track_id_tracks_id_fk",
+ "tableFrom": "now_playing",
+ "tableTo": "tracks",
+ "columnsFrom": [
+ "track_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.password_resets": {
+ "name": "password_resets",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "used_at": {
+ "name": "used_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "password_resets_user_idx": {
+ "name": "password_resets_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "password_resets_user_id_users_id_fk": {
+ "name": "password_resets_user_id_users_id_fk",
+ "tableFrom": "password_resets",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.push_subscriptions": {
+ "name": "push_subscriptions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "kind": {
+ "name": "kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "endpoint": {
+ "name": "endpoint",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "p256dh": {
+ "name": "p256dh",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "auth": {
+ "name": "auth",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "push_subscriptions_user_idx": {
+ "name": "push_subscriptions_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "push_subscriptions_user_id_users_id_fk": {
+ "name": "push_subscriptions_user_id_users_id_fk",
+ "tableFrom": "push_subscriptions",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "push_subscriptions_endpoint_unique": {
+ "name": "push_subscriptions_endpoint_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "endpoint"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.reactions": {
+ "name": "reactions",
+ "schema": "",
+ "columns": {
+ "listen_id": {
+ "name": "listen_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "emoji": {
+ "name": "emoji",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "reactions_listen_id_listens_id_fk": {
+ "name": "reactions_listen_id_listens_id_fk",
+ "tableFrom": "reactions",
+ "tableTo": "listens",
+ "columnsFrom": [
+ "listen_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "reactions_user_id_users_id_fk": {
+ "name": "reactions_user_id_users_id_fk",
+ "tableFrom": "reactions",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "reactions_listen_id_user_id_pk": {
+ "name": "reactions_listen_id_user_id_pk",
+ "columns": [
+ "listen_id",
+ "user_id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.reports": {
+ "name": "reports",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "reporter_id": {
+ "name": "reporter_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reported_id": {
+ "name": "reported_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reason": {
+ "name": "reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "note": {
+ "name": "note",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "comment_id": {
+ "name": "comment_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "comment_body": {
+ "name": "comment_body",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'open'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "reports_status_idx": {
+ "name": "reports_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "reports_reporter_id_users_id_fk": {
+ "name": "reports_reporter_id_users_id_fk",
+ "tableFrom": "reports",
+ "tableTo": "users",
+ "columnsFrom": [
+ "reporter_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "reports_reported_id_users_id_fk": {
+ "name": "reports_reported_id_users_id_fk",
+ "tableFrom": "reports",
+ "tableTo": "users",
+ "columnsFrom": [
+ "reported_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "reports_comment_id_comments_id_fk": {
+ "name": "reports_comment_id_comments_id_fk",
+ "tableFrom": "reports",
+ "tableTo": "comments",
+ "columnsFrom": [
+ "comment_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.server_keys": {
+ "name": "server_keys",
+ "schema": "",
+ "columns": {
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.sessions": {
+ "name": "sessions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "sessions_user_idx": {
+ "name": "sessions_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "sessions_user_id_users_id_fk": {
+ "name": "sessions_user_id_users_id_fk",
+ "tableFrom": "sessions",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.track_links": {
+ "name": "track_links",
+ "schema": "",
+ "columns": {
+ "track_id": {
+ "name": "track_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "service": {
+ "name": "service",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "external_id": {
+ "name": "external_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "match_type": {
+ "name": "match_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "track_links_external_idx": {
+ "name": "track_links_external_idx",
+ "columns": [
+ {
+ "expression": "service",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "external_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "track_links_track_id_tracks_id_fk": {
+ "name": "track_links_track_id_tracks_id_fk",
+ "tableFrom": "track_links",
+ "tableTo": "tracks",
+ "columnsFrom": [
+ "track_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "track_links_track_id_service_pk": {
+ "name": "track_links_track_id_service_pk",
+ "columns": [
+ "track_id",
+ "service"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tracks": {
+ "name": "tracks",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "isrc": {
+ "name": "isrc",
+ "type": "varchar(12)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "match_key": {
+ "name": "match_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "artist": {
+ "name": "artist",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "album": {
+ "name": "album",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "duration_ms": {
+ "name": "duration_ms",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "artwork_url": {
+ "name": "artwork_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "links_resolved_at": {
+ "name": "links_resolved_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "tracks_isrc_idx": {
+ "name": "tracks_isrc_idx",
+ "columns": [
+ {
+ "expression": "isrc",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "tracks_match_key_idx": {
+ "name": "tracks_match_key_idx",
+ "columns": [
+ {
+ "expression": "match_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.users": {
+ "name": "users",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "username": {
+ "name": "username",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "varchar(320)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "password_hash": {
+ "name": "password_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email_verified_at": {
+ "name": "email_verified_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "pending_email": {
+ "name": "pending_email",
+ "type": "varchar(320)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "display_name": {
+ "name": "display_name",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "bio": {
+ "name": "bio",
+ "type": "varchar(280)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "avatar_url": {
+ "name": "avatar_url",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "preferred_service": {
+ "name": "preferred_service",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "services": {
+ "name": "services",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "notify_prefs": {
+ "name": "notify_prefs",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'user'"
+ },
+ "suspended_at": {
+ "name": "suspended_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "onboarded_at": {
+ "name": "onboarded_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sharing_paused_until": {
+ "name": "sharing_paused_until",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "plan": {
+ "name": "plan",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'free'"
+ },
+ "plan_expires_at": {
+ "name": "plan_expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "users_username_unique": {
+ "name": "users_username_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "username"
+ ]
+ },
+ "users_email_unique": {
+ "name": "users_email_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "email"
+ ]
+ }
+ },
+ "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 da45b76..3e744e1 100644
--- a/apps/api/drizzle/meta/_journal.json
+++ b/apps/api/drizzle/meta/_journal.json
@@ -120,6 +120,13 @@
"when": 1792028149643,
"tag": "0016_notifications",
"breakpoints": true
+ },
+ {
+ "idx": 17,
+ "version": "7",
+ "when": 1792084480262,
+ "tag": "0017_comments",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts
index ec1b0cb..c2d16bf 100644
--- a/apps/api/src/app.ts
+++ b/apps/api/src/app.ts
@@ -12,6 +12,7 @@ import { HttpError } from './lib/http';
import { redis } from './lib/redis';
import { adminRoutes } from './routes/admin';
import { appUpdateRoutes } from './routes/appUpdates';
+import { commentRoutes } from './routes/comments';
import { accountRoutes } from './routes/account';
import { authRoutes } from './routes/auth';
import { avatarRoutes } from './routes/avatars';
@@ -78,6 +79,7 @@ export async function buildApp({ logger = true, rateLimits = true }: { logger?:
await app.register(adminRoutes);
await app.register(trackRoutes);
await app.register(reactionRoutes);
+ await app.register(commentRoutes);
await app.register(notificationRoutes);
await app.register(connectionRoutes);
await app.register(deviceRoutes);
diff --git a/apps/api/src/db/schema.ts b/apps/api/src/db/schema.ts
index 4ca7e97..bf1f626 100644
--- a/apps/api/src/db/schema.ts
+++ b/apps/api/src/db/schema.ts
@@ -105,6 +105,9 @@ export const reports = pgTable(
.references(() => users.id, { onDelete: 'cascade' }),
reason: text('reason').$type
+ Their comment{r.comment.id ? '' : ' (since deleted)'}: {r.comment.body}
+
{r.note}}
+ Log in to see the comments. +
+ ); + } + if (error) returnThat play is gone.
; + if (!data) return null; + + const owner = data.owner.displayName ?? data.owner.username; + + const send = async (e: React.FormEvent) => { + e.preventDefault(); + const body = draft.trim(); + if (!body) return; + setSending(true); + setProblem(null); + try { + const comment = await client.addComment(id, { body }); + setDraft(''); + await mutate((t) => (t ? { ...t, comments: [...t.comments, comment] } : t), { revalidate: false }); + } catch (err) { + setProblem((err as Error).message); + } + setSending(false); + }; + + const remove = async (c: CommentDTO) => { + if (!confirm('Delete this comment?')) return; + try { + await client.deleteComment(c.id); + await mutate((t) => (t ? { ...t, comments: t.comments.filter((x) => x.id !== c.id) } : t), { revalidate: false }); + } catch (err) { + setProblem((err as Error).message); + } + }; + + return ( +
+
+
{data.canComment ? 'No comments yet. Say something!' : 'No comments yet.'}
} + {data.comments.map((c) => ( +{c.body}
+Only people {owner} follows can comment on their songs.
+ )} + {problem &&{problem}
} +