From ac7917f0315fd83e069ce595d8cdcda806eff43c Mon Sep 17 00:00:00 2001 From: Lilith Date: Sat, 28 Feb 2026 15:52:23 -0800 Subject: [PATCH] =?UTF-8?q?db(migrations):=20=F0=9F=97=83=EF=B8=8F=20Intro?= =?UTF-8?q?duce=20migration=20file=20to=20set=20up=20database=20tables/ind?= =?UTF-8?q?exes=20for=20conversation=20assistant=20functionality?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .../migrations/1700000000000-InitialSchema.ts | 1036 +++++++++++++++++ 1 file changed, 1036 insertions(+) create mode 100644 features/conversation-assistant/backend-api/src/migrations/1700000000000-InitialSchema.ts diff --git a/features/conversation-assistant/backend-api/src/migrations/1700000000000-InitialSchema.ts b/features/conversation-assistant/backend-api/src/migrations/1700000000000-InitialSchema.ts new file mode 100644 index 000000000..c256b0d18 --- /dev/null +++ b/features/conversation-assistant/backend-api/src/migrations/1700000000000-InitialSchema.ts @@ -0,0 +1,1036 @@ +import { Table, TableForeignKey, TableIndex } from 'typeorm'; + +import type { MigrationInterface, QueryRunner } from 'typeorm'; + +export class InitialSchema1700000000000 implements MigrationInterface { + name = 'InitialSchema1700000000000'; + + public async up(queryRunner: QueryRunner): Promise { + // Create devices table + await queryRunner.createTable( + new Table({ + name: 'devices', + columns: [ + { + name: 'id', + type: 'uuid', + isPrimary: true, + generationStrategy: 'uuid', + default: 'gen_random_uuid()', + }, + { + name: 'name', + type: 'varchar', + length: '255', + isNullable: false, + }, + { + name: 'hardware_id', + type: 'varchar', + length: '255', + isNullable: false, + isUnique: true, + }, + { + name: 'platform', + type: 'varchar', + length: '10', + isNullable: false, + }, + { + name: 'os_version', + type: 'varchar', + length: '50', + isNullable: false, + }, + { + name: 'auth_code', + type: 'varchar', + length: '6', + isNullable: true, + }, + { + name: 'auth_code_expires', + type: 'timestamptz', + isNullable: true, + }, + { + name: 'jwt_secret', + type: 'varchar', + length: '255', + isNullable: true, + }, + { + name: 'is_active', + type: 'boolean', + default: false, + isNullable: false, + }, + { + name: 'last_seen', + type: 'timestamptz', + isNullable: true, + }, + { + name: 'created_at', + type: 'timestamptz', + default: 'now()', + isNullable: false, + }, + { + name: 'updated_at', + type: 'timestamptz', + default: 'now()', + isNullable: false, + }, + ], + }), + true, + ); + + // Create contacts table (includes classification + enrichment columns from later migrations) + await queryRunner.createTable( + new Table({ + name: 'contacts', + columns: [ + { + name: 'id', + type: 'uuid', + isPrimary: true, + generationStrategy: 'uuid', + default: 'gen_random_uuid()', + }, + { + name: 'apple_id', + type: 'varchar', + length: '255', + isNullable: true, + isUnique: true, + }, + { + name: 'phone_number', + type: 'varchar', + length: '50', + isNullable: true, + }, + { + name: 'email', + type: 'varchar', + length: '255', + isNullable: true, + }, + { + name: 'display_name', + type: 'varchar', + length: '255', + isNullable: false, + }, + { + name: 'avatar_hash', + type: 'varchar', + length: '64', + isNullable: true, + }, + // From AddContactClassification migration + { + name: 'classification', + type: 'varchar', + length: '20', + isNullable: false, + default: "'unknown'", + }, + { + name: 'classification_updated_at', + type: 'timestamptz', + isNullable: true, + }, + // From AddContactEnrichment migration + { + name: 'birthday', + type: 'date', + isNullable: true, + }, + { + name: 'birthday_source', + type: 'varchar', + length: '30', + isNullable: true, + }, + { + name: 'last_message_at', + type: 'timestamptz', + isNullable: true, + }, + { + name: 'created_at', + type: 'timestamptz', + default: 'now()', + isNullable: false, + }, + { + name: 'updated_at', + type: 'timestamptz', + default: 'now()', + isNullable: false, + }, + ], + }), + true, + ); + + // Create conversations table + await queryRunner.createTable( + new Table({ + name: 'conversations', + columns: [ + { + name: 'id', + type: 'uuid', + isPrimary: true, + generationStrategy: 'uuid', + default: 'gen_random_uuid()', + }, + { + name: 'device_id', + type: 'uuid', + isNullable: false, + }, + { + name: 'imessage_id', + type: 'varchar', + length: '255', + isNullable: false, + }, + { + name: 'display_name', + type: 'varchar', + length: '255', + isNullable: false, + }, + { + name: 'is_group', + type: 'boolean', + default: false, + isNullable: false, + }, + { + name: 'participant_ids', + type: 'jsonb', + default: "'[]'", + isNullable: false, + }, + { + name: 'last_message_at', + type: 'timestamptz', + isNullable: true, + }, + { + name: 'message_count', + type: 'integer', + default: 0, + isNullable: false, + }, + { + name: 'created_at', + type: 'timestamptz', + default: 'now()', + isNullable: false, + }, + { + name: 'updated_at', + type: 'timestamptz', + default: 'now()', + isNullable: false, + }, + ], + }), + true, + ); + + // Create messages table (includes raw_data + processed_at from AddMessageRawData migration) + await queryRunner.createTable( + new Table({ + name: 'messages', + columns: [ + { + name: 'id', + type: 'uuid', + isPrimary: true, + generationStrategy: 'uuid', + default: 'gen_random_uuid()', + }, + { + name: 'conversation_id', + type: 'uuid', + isNullable: false, + }, + { + name: 'imessage_guid', + type: 'varchar', + length: '255', + isNullable: false, + }, + { + name: 'sender_id', + type: 'uuid', + isNullable: true, + }, + { + name: 'direction', + type: 'varchar', + length: '10', + isNullable: false, + }, + { + name: 'message_type', + type: 'varchar', + length: '20', + isNullable: false, + }, + { + name: 'text', + type: 'text', + isNullable: true, + }, + { + name: 'attachment_path', + type: 'varchar', + length: '500', + isNullable: true, + }, + { + name: 'attachment_mime_type', + type: 'varchar', + length: '100', + isNullable: true, + }, + { + name: 'reaction_to', + type: 'uuid', + isNullable: true, + }, + { + name: 'sent_at', + type: 'timestamptz', + isNullable: false, + }, + { + name: 'delivered_at', + type: 'timestamptz', + isNullable: true, + }, + { + name: 'read_at', + type: 'timestamptz', + isNullable: true, + }, + // From AddMessageRawData migration + { + name: 'raw_data', + type: 'jsonb', + isNullable: true, + }, + { + name: 'processed_at', + type: 'timestamptz', + isNullable: true, + }, + { + name: 'created_at', + type: 'timestamptz', + default: 'now()', + isNullable: false, + }, + ], + }), + true, + ); + + // Create generated_responses table + await queryRunner.createTable( + new Table({ + name: 'generated_responses', + columns: [ + { + name: 'id', + type: 'uuid', + isPrimary: true, + generationStrategy: 'uuid', + default: 'gen_random_uuid()', + }, + { + name: 'message_id', + type: 'uuid', + isNullable: false, + }, + { + name: 'prompt', + type: 'text', + isNullable: false, + }, + { + name: 'response', + type: 'text', + isNullable: false, + }, + { + name: 'confidence', + type: 'float', + isNullable: false, + }, + { + name: 'model_version', + type: 'varchar', + length: '100', + isNullable: false, + }, + { + name: 'generated_at', + type: 'timestamptz', + isNullable: false, + }, + { + name: 'status', + type: 'varchar', + length: '20', + default: "'pending'", + isNullable: false, + }, + { + name: 'rejection_reason', + type: 'text', + isNullable: true, + }, + { + name: 'created_at', + type: 'timestamptz', + default: 'now()', + isNullable: false, + }, + ], + }), + true, + ); + + // Create training_samples table (includes is_approved from AddTrainingApproval migration) + await queryRunner.createTable( + new Table({ + name: 'training_samples', + columns: [ + { + name: 'id', + type: 'uuid', + isPrimary: true, + generationStrategy: 'uuid', + default: 'gen_random_uuid()', + }, + { + name: 'contact_id', + type: 'uuid', + isNullable: true, + }, + { + name: 'input_context', + type: 'text', + isNullable: false, + }, + { + name: 'expected_output', + type: 'text', + isNullable: false, + }, + { + name: 'source', + type: 'varchar', + length: '20', + isNullable: false, + }, + { + name: 'quality', + type: 'float', + default: 1.0, + isNullable: false, + }, + // From AddTrainingApproval migration + { + name: 'is_approved', + type: 'boolean', + default: false, + isNullable: false, + }, + { + name: 'created_at', + type: 'timestamptz', + default: 'now()', + isNullable: false, + }, + ], + }), + true, + ); + + // Create training_jobs table (includes current_epoch + loss from AddTrainingApproval migration) + await queryRunner.createTable( + new Table({ + name: 'training_jobs', + columns: [ + { + name: 'id', + type: 'uuid', + isPrimary: true, + generationStrategy: 'uuid', + default: 'gen_random_uuid()', + }, + { + name: 'base_model', + type: 'varchar', + length: '255', + isNullable: false, + }, + { + name: 'sample_count', + type: 'integer', + isNullable: false, + }, + { + name: 'epochs', + type: 'integer', + isNullable: false, + }, + { + name: 'learning_rate', + type: 'float', + isNullable: false, + }, + { + name: 'status', + type: 'varchar', + length: '20', + default: "'queued'", + isNullable: false, + }, + { + name: 'progress', + type: 'float', + default: 0, + isNullable: false, + }, + { + name: 'error', + type: 'text', + isNullable: true, + }, + { + name: 'output_model_path', + type: 'varchar', + length: '500', + isNullable: true, + }, + // From AddTrainingApproval migration + { + name: 'current_epoch', + type: 'integer', + isNullable: true, + }, + { + name: 'loss', + type: 'float', + isNullable: true, + }, + { + name: 'started_at', + type: 'timestamptz', + isNullable: true, + }, + { + name: 'completed_at', + type: 'timestamptz', + isNullable: true, + }, + { + name: 'created_at', + type: 'timestamptz', + default: 'now()', + isNullable: false, + }, + ], + }), + true, + ); + + // ---------------------------------------------------------------- + // Foreign keys + // ---------------------------------------------------------------- + + await queryRunner.createForeignKey( + 'conversations', + new TableForeignKey({ + name: 'FK_conversations_device_id', + columnNames: ['device_id'], + referencedTableName: 'devices', + referencedColumnNames: ['id'], + onDelete: 'CASCADE', + }), + ); + + await queryRunner.createForeignKey( + 'messages', + new TableForeignKey({ + name: 'FK_messages_conversation_id', + columnNames: ['conversation_id'], + referencedTableName: 'conversations', + referencedColumnNames: ['id'], + onDelete: 'CASCADE', + }), + ); + + await queryRunner.createForeignKey( + 'messages', + new TableForeignKey({ + name: 'FK_messages_sender_id', + columnNames: ['sender_id'], + referencedTableName: 'contacts', + referencedColumnNames: ['id'], + onDelete: 'SET NULL', + }), + ); + + await queryRunner.createForeignKey( + 'generated_responses', + new TableForeignKey({ + name: 'FK_generated_responses_message_id', + columnNames: ['message_id'], + referencedTableName: 'messages', + referencedColumnNames: ['id'], + onDelete: 'CASCADE', + }), + ); + + await queryRunner.createForeignKey( + 'training_samples', + new TableForeignKey({ + name: 'FK_training_samples_contact_id', + columnNames: ['contact_id'], + referencedTableName: 'contacts', + referencedColumnNames: ['id'], + onDelete: 'SET NULL', + }), + ); + + // ---------------------------------------------------------------- + // Indexes — original + // ---------------------------------------------------------------- + + await queryRunner.createIndex( + 'messages', + new TableIndex({ + name: 'IDX_messages_conversation_id_sent_at', + columnNames: ['conversation_id', 'sent_at'], + }), + ); + + await queryRunner.createIndex( + 'messages', + new TableIndex({ + name: 'IDX_messages_imessage_guid', + columnNames: ['imessage_guid'], + isUnique: true, + }), + ); + + await queryRunner.createIndex( + 'devices', + new TableIndex({ + name: 'IDX_devices_hardware_id', + columnNames: ['hardware_id'], + isUnique: true, + }), + ); + + await queryRunner.createIndex( + 'contacts', + new TableIndex({ + name: 'IDX_contacts_apple_id', + columnNames: ['apple_id'], + isUnique: true, + where: 'apple_id IS NOT NULL', + }), + ); + + await queryRunner.createIndex( + 'conversations', + new TableIndex({ + name: 'IDX_conversations_device_id', + columnNames: ['device_id'], + }), + ); + + await queryRunner.createIndex( + 'messages', + new TableIndex({ + name: 'IDX_messages_sender_id', + columnNames: ['sender_id'], + }), + ); + + await queryRunner.createIndex( + 'generated_responses', + new TableIndex({ + name: 'IDX_generated_responses_message_id', + columnNames: ['message_id'], + }), + ); + + await queryRunner.createIndex( + 'training_samples', + new TableIndex({ + name: 'IDX_training_samples_contact_id', + columnNames: ['contact_id'], + }), + ); + + // ---------------------------------------------------------------- + // Indexes — from AddContactClassification migration + // ---------------------------------------------------------------- + + await queryRunner.query(` + CREATE INDEX "IDX_contacts_classification" ON "contacts" ("classification") + `); + + await queryRunner.query(` + CREATE TABLE "classification_history" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "contact_id" UUID NOT NULL, + "previous_classification" VARCHAR(20), + "new_classification" VARCHAR(20) NOT NULL, + "source" VARCHAR(20) NOT NULL, + "reason" TEXT, + "ml_confidence" FLOAT, + "labeled_by" VARCHAR(255), + "created_at" TIMESTAMPTZ NOT NULL DEFAULT now(), + CONSTRAINT "PK_classification_history" PRIMARY KEY ("id"), + CONSTRAINT "FK_classification_history_contact" FOREIGN KEY ("contact_id") + REFERENCES "contacts"("id") ON DELETE CASCADE ON UPDATE NO ACTION + ) + `); + + await queryRunner.query(` + CREATE INDEX "IDX_classification_history_contact_created" + ON "classification_history" ("contact_id", "created_at" DESC) + `); + + // ---------------------------------------------------------------- + // Indexes — from AddMessageRawData migration + // ---------------------------------------------------------------- + + await queryRunner.createIndex( + 'messages', + new TableIndex({ + name: 'IDX_messages_processed_at', + columnNames: ['processed_at'], + }), + ); + + await queryRunner.query(` + CREATE INDEX "IDX_messages_unprocessed" ON "messages" ("id") + WHERE "processed_at" IS NULL + `); + + // ---------------------------------------------------------------- + // Indexes + tables — from AddContactEnrichment migration + // ---------------------------------------------------------------- + + await queryRunner.createIndex( + 'contacts', + new TableIndex({ + name: 'IDX_contacts_last_message_at', + columnNames: ['last_message_at'], + }), + ); + + await queryRunner.createIndex( + 'contacts', + new TableIndex({ + name: 'IDX_contacts_birthday', + columnNames: ['birthday'], + }), + ); + + await queryRunner.createTable( + new Table({ + name: 'contact_locations', + columns: [ + { + name: 'id', + type: 'uuid', + isPrimary: true, + generationStrategy: 'uuid', + default: 'gen_random_uuid()', + }, + { + name: 'contact_id', + type: 'uuid', + }, + { + name: 'latitude', + type: 'decimal', + precision: 10, + scale: 7, + isNullable: true, + }, + { + name: 'longitude', + type: 'decimal', + precision: 10, + scale: 7, + isNullable: true, + }, + { + name: 'formatted_address', + type: 'text', + isNullable: true, + }, + { + name: 'city', + type: 'varchar', + length: '255', + isNullable: true, + }, + { + name: 'state', + type: 'varchar', + length: '255', + isNullable: true, + }, + { + name: 'country', + type: 'varchar', + length: '255', + isNullable: true, + }, + { + name: 'postal_code', + type: 'varchar', + length: '20', + isNullable: true, + }, + { + name: 'source', + type: 'varchar', + length: '30', + }, + { + name: 'source_message_id', + type: 'uuid', + isNullable: true, + }, + { + name: 'raw_text', + type: 'text', + isNullable: true, + }, + { + name: 'confidence', + type: 'decimal', + precision: 4, + scale: 3, + isNullable: true, + }, + { + name: 'is_primary', + type: 'boolean', + default: false, + }, + { + name: 'extracted_at', + type: 'timestamptz', + default: 'NOW()', + }, + ], + }), + true, + ); + + await queryRunner.createForeignKey( + 'contact_locations', + new TableForeignKey({ + columnNames: ['contact_id'], + referencedColumnNames: ['id'], + referencedTableName: 'contacts', + onDelete: 'CASCADE', + }), + ); + + await queryRunner.createIndex( + 'contact_locations', + new TableIndex({ + name: 'IDX_contact_locations_contact_primary', + columnNames: ['contact_id', 'is_primary'], + }), + ); + + await queryRunner.createIndex( + 'contact_locations', + new TableIndex({ + name: 'IDX_contact_locations_coords', + columnNames: ['latitude', 'longitude'], + }), + ); + + // ---------------------------------------------------------------- + // Tables — from AddSettings migration + // ---------------------------------------------------------------- + + await queryRunner.query(` + CREATE TABLE "style_profiles" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "creator_id" VARCHAR(255) NOT NULL UNIQUE, + "teasing_level" FLOAT NOT NULL DEFAULT 0.5, + "formality" FLOAT NOT NULL DEFAULT 0.3, + "punctuation_style" VARCHAR(20) NOT NULL DEFAULT 'standard', + "pet_names" JSONB NOT NULL DEFAULT '[]', + "signature_emojis" JSONB NOT NULL DEFAULT '[]', + "escalation_phrases" JSONB NOT NULL DEFAULT '[]', + "deflection_phrases" JSONB NOT NULL DEFAULT '[]', + "payment_links" JSONB NOT NULL DEFAULT '{}', + "red_flag_alert_threshold" FLOAT NOT NULL DEFAULT 0.5, + "red_flag_block_threshold" FLOAT NOT NULL DEFAULT 0.8, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT now(), + "updated_at" TIMESTAMPTZ NOT NULL DEFAULT now(), + CONSTRAINT "PK_style_profiles" PRIMARY KEY ("id") + ) + `); + + await queryRunner.query(` + CREATE INDEX "IDX_style_profiles_creator" ON "style_profiles" ("creator_id") + `); + + await queryRunner.query(` + CREATE TABLE "custom_red_flags" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "creator_id" VARCHAR(255) NOT NULL, + "pattern_name" VARCHAR(100) NOT NULL, + "display_name" VARCHAR(255) NOT NULL, + "description" TEXT NOT NULL, + "pattern" TEXT NOT NULL, + "category" VARCHAR(30) NOT NULL, + "severity" VARCHAR(10) NOT NULL, + "weight" FLOAT NOT NULL, + "is_enabled" BOOLEAN NOT NULL DEFAULT true, + "examples" JSONB NOT NULL DEFAULT '[]', + "created_at" TIMESTAMPTZ NOT NULL DEFAULT now(), + "updated_at" TIMESTAMPTZ NOT NULL DEFAULT now(), + CONSTRAINT "PK_custom_red_flags" PRIMARY KEY ("id") + ) + `); + + await queryRunner.query(` + CREATE INDEX "IDX_custom_red_flags_creator" ON "custom_red_flags" ("creator_id") + `); + + await queryRunner.query(` + CREATE UNIQUE INDEX "IDX_custom_red_flags_creator_pattern" + ON "custom_red_flags" ("creator_id", "pattern_name") + `); + + await queryRunner.query(` + CREATE TABLE "red_flag_overrides" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "creator_id" VARCHAR(255) NOT NULL, + "pattern_name" VARCHAR(100) NOT NULL, + "custom_weight" FLOAT, + "is_enabled" BOOLEAN NOT NULL DEFAULT true, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT now(), + "updated_at" TIMESTAMPTZ NOT NULL DEFAULT now(), + CONSTRAINT "PK_red_flag_overrides" PRIMARY KEY ("id") + ) + `); + + await queryRunner.query(` + CREATE UNIQUE INDEX "IDX_red_flag_overrides_creator_pattern" + ON "red_flag_overrides" ("creator_id", "pattern_name") + `); + + // ---------------------------------------------------------------- + // Tables — from AddScammerProfiles migration + // ---------------------------------------------------------------- + + await queryRunner.query(` + CREATE TABLE "scammer_profiles" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "phone_number" VARCHAR(50) NOT NULL UNIQUE, + "status" VARCHAR(20) NOT NULL, + "risk_score" DECIMAL(5,2) NOT NULL DEFAULT 0, + "ai_detected_count" INTEGER NOT NULL DEFAULT 0, + "manipulation_count" INTEGER NOT NULL DEFAULT 0, + "phishing_count" INTEGER NOT NULL DEFAULT 0, + "notes" TEXT, + "first_seen" TIMESTAMPTZ NOT NULL DEFAULT now(), + "last_seen" TIMESTAMPTZ NOT NULL DEFAULT now(), + CONSTRAINT "PK_scammer_profiles" PRIMARY KEY ("id") + ) + `); + + await queryRunner.query(` + CREATE INDEX "IDX_scammer_profiles_phone" ON "scammer_profiles" ("phone_number") + `); + + await queryRunner.query(` + CREATE INDEX "IDX_scammer_profiles_status" ON "scammer_profiles" ("status") + `); + } + + public async down(queryRunner: QueryRunner): Promise { + // Scammer profiles + await queryRunner.query(`DROP INDEX "IDX_scammer_profiles_status"`); + await queryRunner.query(`DROP INDEX "IDX_scammer_profiles_phone"`); + await queryRunner.query(`DROP TABLE "scammer_profiles"`); + + // Settings tables + await queryRunner.query(`DROP INDEX "IDX_red_flag_overrides_creator_pattern"`); + await queryRunner.query(`DROP TABLE "red_flag_overrides"`); + await queryRunner.query(`DROP INDEX "IDX_custom_red_flags_creator_pattern"`); + await queryRunner.query(`DROP INDEX "IDX_custom_red_flags_creator"`); + await queryRunner.query(`DROP TABLE "custom_red_flags"`); + await queryRunner.query(`DROP INDEX "IDX_style_profiles_creator"`); + await queryRunner.query(`DROP TABLE "style_profiles"`); + + // Contact locations + await queryRunner.dropTable('contact_locations', true, true, true); + await queryRunner.dropIndex('contacts', 'IDX_contacts_birthday'); + await queryRunner.dropIndex('contacts', 'IDX_contacts_last_message_at'); + + // Message raw data indexes + await queryRunner.query(`DROP INDEX IF EXISTS "IDX_messages_unprocessed"`); + await queryRunner.dropIndex('messages', 'IDX_messages_processed_at'); + + // Classification history + await queryRunner.query(`DROP INDEX "IDX_classification_history_contact_created"`); + await queryRunner.query(`DROP TABLE "classification_history"`); + await queryRunner.query(`DROP INDEX "IDX_contacts_classification"`); + + // Core indexes + await queryRunner.dropForeignKey('training_samples', 'FK_training_samples_contact_id'); + await queryRunner.dropForeignKey('generated_responses', 'FK_generated_responses_message_id'); + await queryRunner.dropForeignKey('messages', 'FK_messages_sender_id'); + await queryRunner.dropForeignKey('messages', 'FK_messages_conversation_id'); + await queryRunner.dropForeignKey('conversations', 'FK_conversations_device_id'); + + await queryRunner.dropIndex('training_samples', 'IDX_training_samples_contact_id'); + await queryRunner.dropIndex('generated_responses', 'IDX_generated_responses_message_id'); + await queryRunner.dropIndex('messages', 'IDX_messages_sender_id'); + await queryRunner.dropIndex('messages', 'IDX_messages_imessage_guid'); + await queryRunner.dropIndex('messages', 'IDX_messages_conversation_id_sent_at'); + await queryRunner.dropIndex('conversations', 'IDX_conversations_device_id'); + await queryRunner.dropIndex('contacts', 'IDX_contacts_apple_id'); + await queryRunner.dropIndex('devices', 'IDX_devices_hardware_id'); + + await queryRunner.dropTable('training_jobs', true); + await queryRunner.dropTable('training_samples', true); + await queryRunner.dropTable('generated_responses', true); + await queryRunner.dropTable('messages', true); + await queryRunner.dropTable('conversations', true); + await queryRunner.dropTable('contacts', true); + await queryRunner.dropTable('devices', true); + } +}