diff --git a/README.md b/README.md index c933bdf..d1b628f 100644 --- a/README.md +++ b/README.md @@ -200,9 +200,5 @@ Set `NO_SUDO=1` if your user is in the docker group (recommended). groups per account the picker still loads the alphabetical top-200; operators with >200 groups need to use the list page's search to find anything past 'L'. -- **Composite index on `(account_id, name)`** for the groups list - page's `ORDER BY name LIMIT 200` query — currently a sort + limit; - the GIN trigram on `name` plus the unique on `(account_id, - wa_group_jid)` already cover most cases. - **Self-service password reset** (email link, etc.) — out of scope for v1; admins use the Users page. diff --git a/apps/web/src/lib/queries.ts b/apps/web/src/lib/queries.ts index 51d77df..f8c012f 100644 --- a/apps/web/src/lib/queries.ts +++ b/apps/web/src/lib/queries.ts @@ -82,11 +82,19 @@ export async function listGroupsForAccount(operatorId: string, accountId: string const account = await getAccount(operatorId, accountId); if (!account) return null; const trimmed = (q ?? "").trim(); + // Hide archived groups from the picker by default. They're rows + // that disappeared from the live participant list (group deleted, + // bot kicked, etc.) but still have reminder_targets pointing at + // them — see the soft-archive flow in apps/bot/src/whatsapp/ + // group-sync.ts. Surfacing archived rows here would let an + // operator pick a group the bot can't actually reach. const rows = trimmed ? await db.execute(sql` SELECT id, account_id, wa_group_jid, name, participant_count, is_archived, last_synced_at FROM whatsapp_groups - WHERE account_id = ${accountId} AND name % ${trimmed} + WHERE account_id = ${accountId} + AND is_archived = false + AND name % ${trimmed} ORDER BY similarity(name, ${trimmed}) DESC LIMIT 50 `) @@ -94,6 +102,7 @@ export async function listGroupsForAccount(operatorId: string, accountId: string SELECT id, account_id, wa_group_jid, name, participant_count, is_archived, last_synced_at FROM whatsapp_groups WHERE account_id = ${accountId} + AND is_archived = false ORDER BY name ASC LIMIT 200 `); diff --git a/packages/db/migrations/0014_lame_puck.sql b/packages/db/migrations/0014_lame_puck.sql new file mode 100644 index 0000000..95b2c16 --- /dev/null +++ b/packages/db/migrations/0014_lame_puck.sql @@ -0,0 +1 @@ +CREATE INDEX IF NOT EXISTS "whatsapp_groups_account_name_idx" ON "whatsapp_groups" USING btree ("account_id","name"); \ No newline at end of file diff --git a/packages/db/migrations/meta/0014_snapshot.json b/packages/db/migrations/meta/0014_snapshot.json new file mode 100644 index 0000000..955e12b --- /dev/null +++ b/packages/db/migrations/meta/0014_snapshot.json @@ -0,0 +1,1093 @@ +{ + "id": "8c71ca85-8842-4999-8d67-1932778390c5", + "prevId": "0782928f-32ab-410b-b777-5efc63d9353d", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "operator_id": { + "name": "operator_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "audit_log_operator_id_operators_id_fk": { + "name": "audit_log_operator_id_operators_id_fk", + "tableFrom": "audit_log", + "tableTo": "operators", + "columnsFrom": [ + "operator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_sessions": { + "name": "auth_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "operator_id": { + "name": "operator_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ip_address": { + "name": "ip_address", + "type": "inet", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "auth_sessions_operator_id_operators_id_fk": { + "name": "auth_sessions_operator_id_operators_id_fk", + "tableFrom": "auth_sessions", + "tableTo": "operators", + "columnsFrom": [ + "operator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auth_sessions_token_hash_unique": { + "name": "auth_sessions_token_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "token_hash" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cache_entries": { + "name": "cache_entries", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.media_files": { + "name": "media_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "operator_id": { + "name": "operator_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "filename_original": { + "name": "filename_original", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_path": { + "name": "storage_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "media_files_operator_id_operators_id_fk": { + "name": "media_files_operator_id_operators_id_fk", + "tableFrom": "media_files", + "tableTo": "operators", + "columnsFrom": [ + "operator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.operators": { + "name": "operators", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'admin'" + }, + "default_timezone": { + "name": "default_timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Asia/Kuala_Lumpur'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "operators_username_uq": { + "name": "operators_username_uq", + "columns": [ + { + "expression": "lower(\"username\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "operators_email_uq": { + "name": "operators_email_uq", + "columns": [ + { + "expression": "lower(\"email\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"operators\".\"email\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rate_limit_buckets": { + "name": "rate_limit_buckets", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "window_start": { + "name": "window_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reminder_messages": { + "name": "reminder_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "reminder_id": { + "name": "reminder_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text_content": { + "name": "text_content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "media_id": { + "name": "media_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "reminder_messages_reminder_id_reminders_id_fk": { + "name": "reminder_messages_reminder_id_reminders_id_fk", + "tableFrom": "reminder_messages", + "tableTo": "reminders", + "columnsFrom": [ + "reminder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "reminder_messages_media_id_media_files_id_fk": { + "name": "reminder_messages_media_id_media_files_id_fk", + "tableFrom": "reminder_messages", + "tableTo": "media_files", + "columnsFrom": [ + "media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reminder_run_targets": { + "name": "reminder_run_targets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "group_label": { + "name": "group_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "wa_message_id": { + "name": "wa_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "reminder_run_targets_run_id_reminder_runs_id_fk": { + "name": "reminder_run_targets_run_id_reminder_runs_id_fk", + "tableFrom": "reminder_run_targets", + "tableTo": "reminder_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "reminder_run_targets_group_id_whatsapp_groups_id_fk": { + "name": "reminder_run_targets_group_id_whatsapp_groups_id_fk", + "tableFrom": "reminder_run_targets", + "tableTo": "whatsapp_groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reminder_runs": { + "name": "reminder_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "reminder_id": { + "name": "reminder_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reminder_name": { + "name": "reminder_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fired_at": { + "name": "fired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error_summary": { + "name": "error_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "reminder_runs_reminder_id_reminders_id_fk": { + "name": "reminder_runs_reminder_id_reminders_id_fk", + "tableFrom": "reminder_runs", + "tableTo": "reminders", + "columnsFrom": [ + "reminder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reminder_targets": { + "name": "reminder_targets", + "schema": "", + "columns": { + "reminder_id": { + "name": "reminder_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "reminder_targets_reminder_id_reminders_id_fk": { + "name": "reminder_targets_reminder_id_reminders_id_fk", + "tableFrom": "reminder_targets", + "tableTo": "reminders", + "columnsFrom": [ + "reminder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "reminder_targets_group_id_whatsapp_groups_id_fk": { + "name": "reminder_targets_group_id_whatsapp_groups_id_fk", + "tableFrom": "reminder_targets", + "tableTo": "whatsapp_groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "reminder_targets_reminder_id_group_id_pk": { + "name": "reminder_targets_reminder_id_group_id_pk", + "columns": [ + "reminder_id", + "group_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reminders": { + "name": "reminders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_kind": { + "name": "schedule_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scheduled_at": { + "name": "scheduled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rrule": { + "name": "rrule", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ends_at": { + "name": "ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "max_runs": { + "name": "max_runs", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_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()" + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "delivery_window_start_hour": { + "name": "delivery_window_start_hour", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 6 + }, + "delivery_window_end_hour": { + "name": "delivery_window_end_hour", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 24 + } + }, + "indexes": {}, + "foreignKeys": { + "reminders_account_id_whatsapp_accounts_id_fk": { + "name": "reminders_account_id_whatsapp_accounts_id_fk", + "tableFrom": "reminders", + "tableTo": "whatsapp_accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "reminders_created_by_operators_id_fk": { + "name": "reminders_created_by_operators_id_fk", + "tableFrom": "reminders", + "tableTo": "operators", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.whatsapp_accounts": { + "name": "whatsapp_accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "operator_id": { + "name": "operator_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "phone_number": { + "name": "phone_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "last_connected_at": { + "name": "last_connected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_qr_at": { + "name": "last_qr_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_qr_png": { + "name": "last_qr_png", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "whatsapp_accounts_operator_label_uq": { + "name": "whatsapp_accounts_operator_label_uq", + "columns": [ + { + "expression": "operator_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "label", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "whatsapp_accounts_operator_id_operators_id_fk": { + "name": "whatsapp_accounts_operator_id_operators_id_fk", + "tableFrom": "whatsapp_accounts", + "tableTo": "operators", + "columnsFrom": [ + "operator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.whatsapp_groups": { + "name": "whatsapp_groups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "wa_group_jid": { + "name": "wa_group_jid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "participant_count": { + "name": "participant_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_archived": { + "name": "is_archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "whatsapp_groups_account_jid_uq": { + "name": "whatsapp_groups_account_jid_uq", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "wa_group_jid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "whatsapp_groups_account_name_idx": { + "name": "whatsapp_groups_account_name_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "whatsapp_groups_account_id_whatsapp_accounts_id_fk": { + "name": "whatsapp_groups_account_id_whatsapp_accounts_id_fk", + "tableFrom": "whatsapp_groups", + "tableTo": "whatsapp_accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 1026249..d9e30f5 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -99,6 +99,13 @@ "when": 1778464004000, "tag": "0013_tricky_yellowjacket", "breakpoints": true + }, + { + "idx": 14, + "version": "7", + "when": 1778464005000, + "tag": "0014_lame_puck", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index af5ad4e..b5f70c4 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -10,6 +10,7 @@ import { jsonb, primaryKey, uniqueIndex, + index, inet, } from "drizzle-orm/pg-core"; @@ -80,6 +81,16 @@ export const whatsappGroups = pgTable( }, (t) => ({ accountJidUnique: uniqueIndex("whatsapp_groups_account_jid_uq").on(t.accountId, t.waGroupJid), + // Backs `WHERE account_id=? ORDER BY name ASC LIMIT 200` on the + // groups list page. Without this, PG falls back to the unique + // (account_id, wa_group_jid) index for the WHERE clause and then + // does an explicit sort on `name` — fine at small scale, slow + // when an operator has 3 000+ groups. Drizzle import is `index`, + // declared in this same file's import block. + accountNameIdx: index("whatsapp_groups_account_name_idx").on( + t.accountId, + t.name, + ), }), );