diff --git a/apps/server/drizzle/0006_gray_stardust.sql b/apps/server/drizzle/0006_gray_stardust.sql new file mode 100644 index 000000000..c1b647d7f --- /dev/null +++ b/apps/server/drizzle/0006_gray_stardust.sql @@ -0,0 +1,6 @@ +ALTER TABLE "chat_members" ALTER COLUMN "user_id" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "chat_members" ADD COLUMN "member_type" text NOT NULL;--> statement-breakpoint +ALTER TABLE "chat_members" ADD COLUMN "character_id" text;--> statement-breakpoint +ALTER TABLE "chats" ADD COLUMN "title" text;--> statement-breakpoint +ALTER TABLE "messages" ADD COLUMN "role" text NOT NULL;--> statement-breakpoint +ALTER TABLE "chat_members" ADD CONSTRAINT "chat_members_character_id_characters_id_fk" FOREIGN KEY ("character_id") REFERENCES "public"."characters"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/apps/server/drizzle/meta/0006_snapshot.json b/apps/server/drizzle/meta/0006_snapshot.json new file mode 100644 index 000000000..f7fe834ad --- /dev/null +++ b/apps/server/drizzle/meta/0006_snapshot.json @@ -0,0 +1,1536 @@ +{ + "id": "72183d19-1430-4cac-a0be-48de27227b4a", + "prevId": "4a4f0e4f-3f20-4316-8f9e-6902455167ac", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.avatar_model": { + "name": "avatar_model", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "character_id": { + "name": "character_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "avatar_model_character_id_characters_id_fk": { + "name": "avatar_model_character_id_characters_id_fk", + "tableFrom": "avatar_model", + "tableTo": "characters", + "columnsFrom": [ + "character_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.characters": { + "name": "characters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cover_url": { + "name": "cover_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "character_id": { + "name": "character_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "creator_role": { + "name": "creator_role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "price_credit": { + "name": "price_credit", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "likes_count": { + "name": "likes_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "bookmarks_count": { + "name": "bookmarks_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "interactions_count": { + "name": "interactions_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "forks_count": { + "name": "forks_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "characters_creator_id_user_id_fk": { + "name": "characters_creator_id_user_id_fk", + "tableFrom": "characters", + "tableTo": "user", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "characters_owner_id_user_id_fk": { + "name": "characters_owner_id_user_id_fk", + "tableFrom": "characters", + "tableTo": "user", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.character_capabilities": { + "name": "character_capabilities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "character_id": { + "name": "character_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "character_capabilities_character_id_characters_id_fk": { + "name": "character_capabilities_character_id_characters_id_fk", + "tableFrom": "character_capabilities", + "tableTo": "characters", + "columnsFrom": [ + "character_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.character_covers": { + "name": "character_covers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "character_id": { + "name": "character_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "foreground_url": { + "name": "foreground_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "background_url": { + "name": "background_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "character_covers_character_id_characters_id_fk": { + "name": "character_covers_character_id_characters_id_fk", + "tableFrom": "character_covers", + "tableTo": "characters", + "columnsFrom": [ + "character_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.character_i18n": { + "name": "character_i18n", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "character_id": { + "name": "character_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tagline": { + "name": "tagline", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "character_i18n_character_id_characters_id_fk": { + "name": "character_i18n_character_id_characters_id_fk", + "tableFrom": "character_i18n", + "tableTo": "characters", + "columnsFrom": [ + "character_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.character_prompts": { + "name": "character_prompts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "character_id": { + "name": "character_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "character_prompts_character_id_characters_id_fk": { + "name": "character_prompts_character_id_characters_id_fk", + "tableFrom": "character_prompts", + "tableTo": "characters", + "columnsFrom": [ + "character_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_members": { + "name": "chat_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "member_type": { + "name": "member_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "character_id": { + "name": "character_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "chat_members_chat_id_chats_id_fk": { + "name": "chat_members_chat_id_chats_id_fk", + "tableFrom": "chat_members", + "tableTo": "chats", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_members_user_id_user_id_fk": { + "name": "chat_members_user_id_user_id_fk", + "tableFrom": "chat_members", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_members_character_id_characters_id_fk": { + "name": "chat_members_character_id_characters_id_fk", + "tableFrom": "chat_members", + "tableTo": "characters", + "columnsFrom": [ + "character_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chats": { + "name": "chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.media": { + "name": "media", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.messages": { + "name": "messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sender_id": { + "name": "sender_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "media_ids": { + "name": "media_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "sticker_ids": { + "name": "sticker_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "reply_message_id": { + "name": "reply_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "forward_from_message_id": { + "name": "forward_from_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "messages_chat_id_chats_id_fk": { + "name": "messages_chat_id_chats_id_fk", + "tableFrom": "messages", + "tableTo": "chats", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sticker_packs": { + "name": "sticker_packs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.stickers": { + "name": "stickers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.system_provider_configs": { + "name": "system_provider_configs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "definition_id": { + "name": "definition_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "validated": { + "name": "validated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "validation_bypassed": { + "name": "validation_bypassed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_provider_configs": { + "name": "user_provider_configs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "definition_id": { + "name": "definition_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "validated": { + "name": "validated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "validation_bypassed": { + "name": "validation_bypassed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_provider_configs_owner_id_user_id_fk": { + "name": "user_provider_configs_owner_id_user_id_fk", + "tableFrom": "user_provider_configs", + "tableTo": "user", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_character_bookmarks": { + "name": "user_character_bookmarks", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "character_id": { + "name": "character_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_character_bookmarks_user_id_user_id_fk": { + "name": "user_character_bookmarks_user_id_user_id_fk", + "tableFrom": "user_character_bookmarks", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_character_bookmarks_character_id_characters_id_fk": { + "name": "user_character_bookmarks_character_id_characters_id_fk", + "tableFrom": "user_character_bookmarks", + "tableTo": "characters", + "columnsFrom": [ + "character_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_character_bookmarks_user_id_character_id_pk": { + "name": "user_character_bookmarks_user_id_character_id_pk", + "columns": [ + "user_id", + "character_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_character_likes": { + "name": "user_character_likes", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "character_id": { + "name": "character_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_character_likes_user_id_user_id_fk": { + "name": "user_character_likes_user_id_user_id_fk", + "tableFrom": "user_character_likes", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_character_likes_character_id_characters_id_fk": { + "name": "user_character_likes_character_id_characters_id_fk", + "tableFrom": "user_character_likes", + "tableTo": "characters", + "columnsFrom": [ + "character_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_character_likes_user_id_character_id_pk": { + "name": "user_character_likes_user_id_character_id_pk", + "columns": [ + "user_id", + "character_id" + ] + } + }, + "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/server/drizzle/meta/_journal.json b/apps/server/drizzle/meta/_journal.json index 4361b9e0a..66c375dad 100644 --- a/apps/server/drizzle/meta/_journal.json +++ b/apps/server/drizzle/meta/_journal.json @@ -43,6 +43,13 @@ "when": 1767792829241, "tag": "0005_true_namor", "breakpoints": true + }, + { + "idx": 6, + "version": "7", + "when": 1769186900274, + "tag": "0006_gray_stardust", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/server/src/api/chats.schema.ts b/apps/server/src/api/chats.schema.ts new file mode 100644 index 000000000..0a3961aad --- /dev/null +++ b/apps/server/src/api/chats.schema.ts @@ -0,0 +1,45 @@ +import { array, literal, number, object, optional, string, union } from 'valibot' + +const ChatTypeSchema = union([ + literal('private'), + literal('bot'), + literal('group'), + literal('channel'), +]) + +const ChatMemberTypeSchema = union([ + literal('user'), + literal('character'), + literal('bot'), +]) + +const ChatMessageRoleSchema = union([ + literal('system'), + literal('user'), + literal('assistant'), + literal('tool'), + literal('error'), +]) + +export const ChatSyncMessageSchema = object({ + id: string(), + role: ChatMessageRoleSchema, + content: string(), + createdAt: optional(number()), +}) + +export const ChatSyncSchema = object({ + chat: object({ + id: string(), + type: optional(ChatTypeSchema), + title: optional(string()), + createdAt: optional(number()), + updatedAt: optional(number()), + }), + members: optional(array(object({ + type: ChatMemberTypeSchema, + userId: optional(string()), + characterId: optional(string()), + }))), + messages: array(ChatSyncMessageSchema), +}) diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index 74ab22641..c275ebb8e 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -11,9 +11,11 @@ import { createLoggLogger, injeca } from 'injeca' import { sessionMiddleware } from './middlewares/auth' import { createCharacterRoutes } from './routes/characters' +import { createChatRoutes } from './routes/chats' import { createProviderRoutes } from './routes/providers' import { createAuth } from './services/auth' import { createCharacterService } from './services/characters' +import { createChatService } from './services/chats' import { createDrizzle } from './services/db' import { parsedEnv } from './services/env' import { createProviderService } from './services/providers' @@ -24,15 +26,17 @@ import * as schema from './schemas' type AuthService = ReturnType type CharacterService = ReturnType +type ChatService = ReturnType type ProviderService = ReturnType interface AppDeps { auth: AuthService characterService: CharacterService + chatService: ChatService providerService: ProviderService } -function buildApp({ auth, characterService, providerService }: AppDeps) { +function buildApp({ auth, characterService, chatService, providerService }: AppDeps) { const logger = useLogger('app').useGlobalConfig() return new Hono() @@ -77,6 +81,11 @@ function buildApp({ auth, characterService, providerService }: AppDeps) { * Provider routes are handled by the provider service. */ .route('/api/providers', createProviderRoutes(providerService)) + + /** + * Chat routes are handled by the chat service. + */ + .route('/api/chats', createChatRoutes(chatService)) } export type AppType = ReturnType @@ -114,11 +123,17 @@ async function createApp() { build: ({ dependsOn }) => createProviderService(dependsOn.db), }) + const chatService = injeca.provide('services:chats', { + dependsOn: { db }, + build: ({ dependsOn }) => createChatService(dependsOn.db), + }) + await injeca.start() - const resolved = await injeca.resolve({ auth, characterService, providerService }) + const resolved = await injeca.resolve({ auth, characterService, chatService, providerService }) const app = buildApp({ auth: resolved.auth, characterService: resolved.characterService, + chatService: resolved.chatService, providerService: resolved.providerService, }) diff --git a/apps/server/src/routes/chats.ts b/apps/server/src/routes/chats.ts new file mode 100644 index 000000000..b02f18a1a --- /dev/null +++ b/apps/server/src/routes/chats.ts @@ -0,0 +1,26 @@ +import type { ChatService } from '../services/chats' +import type { HonoEnv } from '../types/hono' + +import { Hono } from 'hono' +import { safeParse } from 'valibot' + +import { ChatSyncSchema } from '../api/chats.schema' +import { authGuard } from '../middlewares/auth' +import { createBadRequestError } from '../utils/error' + +export function createChatRoutes(chatService: ChatService) { + return new Hono() + .use('*', authGuard) + .post('/sync', async (c) => { + const user = c.get('user')! + + const body = await c.req.json() + const result = safeParse(ChatSyncSchema, body) + + if (!result.success) + throw createBadRequestError('Invalid Request', 'INVALID_REQUEST', result.issues) + + const synced = await chatService.syncChat(user.id, result.output) + return c.json(synced) + }) +} diff --git a/apps/server/src/schemas/chats.ts b/apps/server/src/schemas/chats.ts index 5d9db9aa8..5bcecf8d9 100644 --- a/apps/server/src/schemas/chats.ts +++ b/apps/server/src/schemas/chats.ts @@ -1,7 +1,10 @@ +import type { InferInsertModel, InferSelectModel } from 'drizzle-orm' + import { integer, pgTable, text, timestamp } from 'drizzle-orm/pg-core' import { nanoid } from '../utils/id' import { user } from './accounts' +import { character } from './characters' export const media = pgTable( 'media', @@ -37,6 +40,7 @@ export const stickerPacks = pgTable( ) type ChatType = 'private' | 'bot' | 'group' | 'channel' +type ChatMemberType = 'user' | 'character' | 'bot' export const chats = pgTable( 'chats', @@ -44,6 +48,7 @@ export const chats = pgTable( id: text('id').primaryKey().$defaultFn(() => nanoid()), type: text('type').notNull().$type(), + title: text('title'), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), @@ -51,12 +56,17 @@ export const chats = pgTable( }, ) +export type Chat = InferSelectModel +export type NewChat = InferInsertModel + export const chatMembers = pgTable( 'chat_members', { id: text('id').primaryKey().$defaultFn(() => nanoid()), chatId: text('chat_id').notNull().references(() => chats.id, { onDelete: 'cascade' }), - userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }), + memberType: text('member_type').notNull().$type(), + userId: text('user_id').references(() => user.id, { onDelete: 'cascade' }), + characterId: text('character_id').references(() => character.id, { onDelete: 'cascade' }), }, ) @@ -67,6 +77,7 @@ export const messages = pgTable( chatId: text('chat_id').notNull().references(() => chats.id, { onDelete: 'cascade' }), senderId: text('sender_id').notNull(), + role: text('role').notNull(), content: text('content').notNull(), mediaIds: text('media_ids').array().notNull(), @@ -80,3 +91,6 @@ export const messages = pgTable( deletedAt: timestamp('deleted_at'), }, ) + +export type Message = InferSelectModel +export type NewMessage = InferInsertModel diff --git a/apps/server/src/services/chats.ts b/apps/server/src/services/chats.ts new file mode 100644 index 000000000..f453be7a4 --- /dev/null +++ b/apps/server/src/services/chats.ts @@ -0,0 +1,172 @@ +import type * as fullSchema from '../schemas' +import type { Database } from './db' + +import { and, eq } from 'drizzle-orm' + +import { createConflictError, createForbiddenError } from '../utils/error' + +import * as schema from '../schemas/chats' + +type ChatType = 'private' | 'bot' | 'group' | 'channel' +type MessageRole = 'system' | 'user' | 'assistant' | 'tool' | 'error' +type ChatMemberType = 'user' | 'character' | 'bot' + +interface SyncChatMessagePayload { + id: string + role: MessageRole + content: string + createdAt?: number +} + +interface SyncChatMemberPayload { + type: ChatMemberType + userId?: string + characterId?: string +} + +interface SyncChatPayload { + chat: { + id: string + type?: ChatType + title?: string + createdAt?: number + updatedAt?: number + } + members?: SyncChatMemberPayload[] + messages: SyncChatMessagePayload[] +} + +function resolveSenderId(role: MessageRole, userId: string, characterId?: string) { + if (role === 'user') + return userId + return characterId ?? role +} + +function pickCharacterId(members: SyncChatMemberPayload[] | undefined) { + return members?.find(member => member.type === 'character' && member.characterId)?.characterId +} + +export function createChatService(db: Database) { + return { + async syncChat(userId: string, payload: SyncChatPayload) { + return await db.transaction(async (tx) => { + const now = new Date() + const chatId = payload.chat.id + const members = payload.members ?? [] + const characterId = pickCharacterId(members) + + const existingChat = await tx.query.chats.findFirst({ + where: eq(schema.chats.id, chatId), + }) + + if (existingChat) { + const member = await tx.query.chatMembers.findFirst({ + where: and( + eq(schema.chatMembers.chatId, chatId), + eq(schema.chatMembers.memberType, 'user'), + eq(schema.chatMembers.userId, userId), + ), + }) + + if (!member) + throw createForbiddenError() + } + + if (!existingChat) { + await tx.insert(schema.chats).values({ + id: chatId, + type: payload.chat.type ?? 'group', + title: payload.chat.title, + createdAt: payload.chat.createdAt ? new Date(payload.chat.createdAt) : now, + updatedAt: payload.chat.updatedAt ? new Date(payload.chat.updatedAt) : now, + }) + } + else { + const updates: Partial = { + updatedAt: payload.chat.updatedAt ? new Date(payload.chat.updatedAt) : now, + } + + if (payload.chat.type) + updates.type = payload.chat.type + if (payload.chat.title !== undefined) + updates.title = payload.chat.title + + await tx.update(schema.chats) + .set(updates) + .where(eq(schema.chats.id, chatId)) + } + + const desiredMembers: SyncChatMemberPayload[] = [ + { type: 'user', userId }, + ...members.filter(member => member.type !== 'user'), + ] + + for (const member of desiredMembers) { + if (member.type === 'user' && !member.userId) + continue + if (member.type === 'character' && !member.characterId) + continue + + const existingMember = await tx.query.chatMembers.findFirst({ + where: and( + eq(schema.chatMembers.chatId, chatId), + eq(schema.chatMembers.memberType, member.type), + member.type === 'user' + ? eq(schema.chatMembers.userId, member.userId!) + : eq(schema.chatMembers.characterId, member.characterId!), + ), + }) + + if (!existingMember) { + await tx.insert(schema.chatMembers).values({ + chatId, + memberType: member.type, + userId: member.type === 'user' ? member.userId : null, + characterId: member.type === 'character' ? member.characterId : null, + }) + } + } + + for (const message of payload.messages) { + const existing = await tx.query.messages.findFirst({ + where: eq(schema.messages.id, message.id), + }) + + const senderId = resolveSenderId(message.role, userId, characterId) + const createdAt = message.createdAt ? new Date(message.createdAt) : now + + if (existing) { + if (existing.chatId !== chatId) + throw createConflictError('Message already belongs to another chat') + + await tx.update(schema.messages) + .set({ + senderId, + role: message.role, + content: message.content, + updatedAt: now, + }) + .where(eq(schema.messages.id, message.id)) + continue + } + + await tx.insert(schema.messages).values({ + id: message.id, + chatId, + senderId, + role: message.role, + content: message.content, + mediaIds: [], + stickerIds: [], + createdAt, + updatedAt: now, + }) + } + + return { chatId } + }) + }, + } +} + +export type ChatService = ReturnType diff --git a/packages/stage-ui/src/stores/chat.ts b/packages/stage-ui/src/stores/chat.ts index e13eb1b71..01773a4b7 100644 --- a/packages/stage-ui/src/stores/chat.ts +++ b/packages/stage-ui/src/stores/chat.ts @@ -170,7 +170,7 @@ export const useChatOrchestratorStore = defineStore('chat-orchestrator', () => { return const sessionMessagesForSend = chatSession.getSessionMessages(sessionId) - sessionMessagesForSend.push({ role: 'user', content: finalContent }) + sessionMessagesForSend.push({ role: 'user', content: finalContent, createdAt: sendingCreatedAt }) chatSession.persistSessionMessages(sessionId) const categorizer = createStreamingCategorizer(activeProvider.value) diff --git a/packages/stage-ui/src/stores/chat/session-store.ts b/packages/stage-ui/src/stores/chat/session-store.ts index 307d915c1..e4ef0f65e 100644 --- a/packages/stage-ui/src/stores/chat/session-store.ts +++ b/packages/stage-ui/src/stores/chat/session-store.ts @@ -1,5 +1,3 @@ -import type { SystemMessage } from '@xsai/shared-chat' - import type { ChatHistoryItem } from '../../types/chat' import type { ChatSessionMeta, ChatSessionRecord, ChatSessionsExport, ChatSessionsIndex } from '../../types/chat-session' @@ -7,12 +5,14 @@ import { nanoid } from 'nanoid' import { defineStore, storeToRefs } from 'pinia' import { computed, ref, watch } from 'vue' +import { client } from '../../composables/api' +import { useLocalFirstRequest } from '../../composables/use-local-first' import { chatSessionsRepo } from '../../database/repos/chat-sessions.repo' import { useAuthStore } from '../auth' import { useAiriCardStore } from '../modules/airi-card' export const useChatSessionStore = defineStore('chat-session', () => { - const { userId } = storeToRefs(useAuthStore()) + const { userId, isAuthenticated } = storeToRefs(useAuthStore()) const { activeCardId, systemPrompt } = storeToRefs(useAiriCardStore()) const activeSessionId = ref('') @@ -27,6 +27,7 @@ export const useChatSessionStore = defineStore('chat-session', () => { let initializePromise: Promise | null = null let persistQueue = Promise.resolve() + let syncQueue = Promise.resolve() const loadedSessions = new Set() const loadingSessions = new Map>() @@ -47,17 +48,115 @@ export const useChatSessionStore = defineStore('chat-session', () => { return persistQueue } + function enqueueSync(task: () => Promise) { + syncQueue = syncQueue.then(task, task) + return syncQueue + } + function snapshotMessages(messages: ChatHistoryItem[]) { return JSON.parse(JSON.stringify(messages)) as ChatHistoryItem[] } + function extractMessageContent(message: ChatHistoryItem) { + if (typeof message.content === 'string') + return message.content + if (Array.isArray(message.content)) { + return message.content.map((part) => { + if (typeof part === 'string') + return part + if (part && typeof part === 'object' && 'text' in part) + return String(part.text ?? '') + return '' + }).join('') + } + return '' + } + + function buildMessageId(sessionId: string, message: ChatHistoryItem, index: number) { + const createdAt = message.createdAt ?? 0 + return `${sessionId}:${createdAt}:${index}` + } + + function buildSyncMessages(sessionId: string, messages: ChatHistoryItem[]) { + return messages.map((message, index) => ({ + id: buildMessageId(sessionId, message, index), + role: message.role, + content: extractMessageContent(message), + createdAt: message.createdAt, + })) + } + + async function syncSessionToRemote(sessionId: string) { + let cachedRecord: ChatSessionRecord | null | undefined + const request = useLocalFirstRequest({ + local: async () => { + cachedRecord = await chatSessionsRepo.getSession(sessionId) + return cachedRecord + }, + remote: async () => { + if (!cachedRecord) + cachedRecord = await chatSessionsRepo.getSession(sessionId) + if (!cachedRecord) + return cachedRecord + + const members: Array< + | { type: 'user', userId: string } + | { type: 'character', characterId: string } + > = [ + { type: 'user', userId: userId.value }, + ] + + if (cachedRecord.meta.characterId) { + members.push({ + type: 'character', + characterId: cachedRecord.meta.characterId, + }) + } + + const res = await client.api.chats.sync.$post({ + json: { + chat: { + id: cachedRecord.meta.sessionId, + type: 'group', + title: cachedRecord.meta.title, + createdAt: cachedRecord.meta.createdAt, + updatedAt: cachedRecord.meta.updatedAt, + }, + members, + messages: buildSyncMessages(cachedRecord.meta.sessionId, cachedRecord.messages), + }, + }) + + if (!res.ok) + throw new Error('Failed to sync chat session') + return cachedRecord + }, + allowRemote: () => isAuthenticated.value, + lazy: true, + }) + + await request.execute() + } + + function scheduleSync(sessionId: string) { + void enqueueSync(async () => { + try { + await syncSessionToRemote(sessionId) + } + catch (error) { + console.warn('Failed to sync chat session', error) + } + }) + } + function generateInitialMessageFromPrompt(prompt: string) { const content = codeBlockSystemPrompt + mathSyntaxSystemPrompt + prompt return { role: 'system', content, - } satisfies SystemMessage + createdAt: Date.now(), + } satisfies ChatHistoryItem } function generateInitialMessage() { @@ -113,6 +212,7 @@ export const useChatSessionStore = defineStore('chat-session', () => { await enqueuePersist(() => chatSessionsRepo.saveSession(sessionId, record)) await persistIndex() + scheduleSync(sessionId) } function persistSessionMessages(sessionId: string) { @@ -178,8 +278,10 @@ export const useChatSessionStore = defineStore('chat-session', () => { characterIndex.activeSessionId = sessionId index.value.characters[characterId] = characterIndex - await enqueuePersist(() => chatSessionsRepo.saveSession(sessionId, { meta, messages: initialMessages })) + const record: ChatSessionRecord = { meta, messages: initialMessages } + await enqueuePersist(() => chatSessionsRepo.saveSession(sessionId, record)) await persistIndex() + scheduleSync(sessionId) if (options?.setActive !== false) activeSessionId.value = sessionId