diff --git a/apps/api/drizzle/0028_broadcast_archive.sql b/apps/api/drizzle/0028_broadcast_archive.sql new file mode 100644 index 0000000..7d64b06 --- /dev/null +++ b/apps/api/drizzle/0028_broadcast_archive.sql @@ -0,0 +1 @@ +ALTER TABLE "sequences" ADD COLUMN "archived_at" timestamp with time zone; \ No newline at end of file diff --git a/apps/api/drizzle/meta/0028_snapshot.json b/apps/api/drizzle/meta/0028_snapshot.json new file mode 100644 index 0000000..b04a24f --- /dev/null +++ b/apps/api/drizzle/meta/0028_snapshot.json @@ -0,0 +1,11645 @@ +{ + "id": "e8de4127-8e8e-466d-8b61-06a39e87feb3", + "prevId": "ca27064d-1db2-4440-b74b-f81cc44838a5", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.abuse_flags": { + "name": "abuse_flags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "flag_id": { + "name": "flag_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "sequence_email_id": { + "name": "sequence_email_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "resolved_by": { + "name": "resolved_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolution_note": { + "name": "resolution_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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()" + } + }, + "indexes": { + "abuse_flags_status_created_at_idx": { + "name": "abuse_flags_status_created_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "abuse_flags_organization_kind_status_idx": { + "name": "abuse_flags_organization_kind_status_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "abuse_flags_organization_id_organizations_id_fk": { + "name": "abuse_flags_organization_id_organizations_id_fk", + "tableFrom": "abuse_flags", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "abuse_flags_team_id_teams_id_fk": { + "name": "abuse_flags_team_id_teams_id_fk", + "tableFrom": "abuse_flags", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "abuse_flags_sequence_email_id_sequence_emails_id_fk": { + "name": "abuse_flags_sequence_email_id_sequence_emails_id_fk", + "tableFrom": "abuse_flags", + "tableTo": "sequence_emails", + "columnsFrom": [ + "sequence_email_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "abuse_flags_flag_id_unique": { + "name": "abuse_flags_flag_id_unique", + "nullsNotDistinct": false, + "columns": [ + "flag_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "abuse_flags_flag_id_check": { + "name": "abuse_flags_flag_id_check", + "value": "\"abuse_flags\".\"flag_id\" ~ '^abf_'" + }, + "abuse_flags_kind_check": { + "name": "abuse_flags_kind_check", + "value": "\"abuse_flags\".\"kind\" IN ('signup', 'content', 'import', 'broadcast')" + }, + "abuse_flags_severity_check": { + "name": "abuse_flags_severity_check", + "value": "\"abuse_flags\".\"severity\" IN ('low', 'medium', 'high')" + }, + "abuse_flags_status_check": { + "name": "abuse_flags_status_check", + "value": "\"abuse_flags\".\"status\" IN ('open', 'released', 'confirmed')" + } + }, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "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 with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "auth_account_user_id_idx": { + "name": "auth_account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auth_account_issuer_account_id_uidx": { + "name": "auth_account_issuer_account_id_uidx", + "columns": [ + { + "expression": "issuer", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "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.contact_custom_field_values": { + "name": "contact_custom_field_values", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value_type": { + "name": "value_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value_text": { + "name": "value_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "value_number": { + "name": "value_number", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "value_boolean": { + "name": "value_boolean", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "value_date": { + "name": "value_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "contact_custom_field_values_contact_key_idx": { + "name": "contact_custom_field_values_contact_key_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "contact_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "contact_custom_field_values_text_lookup_idx": { + "name": "contact_custom_field_values_text_lookup_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "value_text", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "contact_custom_field_values_number_lookup_idx": { + "name": "contact_custom_field_values_number_lookup_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "value_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "contact_custom_field_values_boolean_lookup_idx": { + "name": "contact_custom_field_values_boolean_lookup_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "value_boolean", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "contact_custom_field_values_date_lookup_idx": { + "name": "contact_custom_field_values_date_lookup_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "value_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "contact_custom_field_values_team_id_teams_id_fk": { + "name": "contact_custom_field_values_team_id_teams_id_fk", + "tableFrom": "contact_custom_field_values", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "contact_custom_field_values_contact_id_contacts_id_fk": { + "name": "contact_custom_field_values_contact_id_contacts_id_fk", + "tableFrom": "contact_custom_field_values", + "tableTo": "contacts", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contact_event_matches": { + "name": "contact_event_matches", + "schema": "", + "columns": { + "contact_event_id": { + "name": "contact_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sequence_id": { + "name": "sequence_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "enrolled": { + "name": "enrolled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "contact_event_matches_contact_event_id_contact_events_id_fk": { + "name": "contact_event_matches_contact_event_id_contact_events_id_fk", + "tableFrom": "contact_event_matches", + "tableTo": "contact_events", + "columnsFrom": [ + "contact_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "contact_event_matches_sequence_id_sequences_id_fk": { + "name": "contact_event_matches_sequence_id_sequences_id_fk", + "tableFrom": "contact_event_matches", + "tableTo": "sequences", + "columnsFrom": [ + "sequence_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "contact_event_matches_contact_event_id_sequence_id_pk": { + "name": "contact_event_matches_contact_event_id_sequence_id_pk", + "columns": [ + "contact_event_id", + "sequence_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contact_events": { + "name": "contact_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "properties": { + "name": "properties", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backfill": { + "name": "backfill", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "triggers": { + "name": "triggers", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "contact_events_team_id_idempotency_key_idx": { + "name": "contact_events_team_id_idempotency_key_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"contact_events\".\"idempotency_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "contact_events_contact_id_occurred_at_idx": { + "name": "contact_events_contact_id_occurred_at_idx", + "columns": [ + { + "expression": "contact_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "contact_events_unprocessed_idx": { + "name": "contact_events_unprocessed_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"contact_events\".\"processed_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "contact_events_team_id_teams_id_fk": { + "name": "contact_events_team_id_teams_id_fk", + "tableFrom": "contact_events", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "contact_events_contact_id_contacts_id_fk": { + "name": "contact_events_contact_id_contacts_id_fk", + "tableFrom": "contact_events", + "tableTo": "contacts", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "contact_events_event_id_unique": { + "name": "contact_events_event_id_unique", + "nullsNotDistinct": false, + "columns": [ + "event_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "contact_events_event_id_check": { + "name": "contact_events_event_id_check", + "value": "\"contact_events\".\"event_id\" ~ '^evt_'" + } + }, + "isRLSEnabled": false + }, + "public.contacts": { + "name": "contacts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subscribed": { + "name": "subscribed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "custom_fields": { + "name": "custom_fields", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "unsubscribe_token": { + "name": "unsubscribe_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "contacts_team_id_email_idx": { + "name": "contacts_team_id_email_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "contacts_team_id_teams_id_fk": { + "name": "contacts_team_id_teams_id_fk", + "tableFrom": "contacts", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "contacts_contact_id_unique": { + "name": "contacts_contact_id_unique", + "nullsNotDistinct": false, + "columns": [ + "contact_id" + ] + }, + "contacts_unsubscribe_token_unique": { + "name": "contacts_unsubscribe_token_unique", + "nullsNotDistinct": false, + "columns": [ + "unsubscribe_token" + ] + } + }, + "policies": {}, + "checkConstraints": { + "contacts_contact_id_check": { + "name": "contacts_contact_id_check", + "value": "\"contacts\".\"contact_id\" ~ '^cnt_'" + } + }, + "isRLSEnabled": false + }, + "public.email_content_versions": { + "name": "email_content_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "version_id": { + "name": "version_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sequence_email_id": { + "name": "sequence_email_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "email_content_versions_email_hash_idx": { + "name": "email_content_versions_email_hash_idx", + "columns": [ + { + "expression": "sequence_email_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "content_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_content_versions_team_id_teams_id_fk": { + "name": "email_content_versions_team_id_teams_id_fk", + "tableFrom": "email_content_versions", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "email_content_versions_sequence_email_id_sequence_emails_id_fk": { + "name": "email_content_versions_sequence_email_id_sequence_emails_id_fk", + "tableFrom": "email_content_versions", + "tableTo": "sequence_emails", + "columnsFrom": [ + "sequence_email_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "email_content_versions_version_id_unique": { + "name": "email_content_versions_version_id_unique", + "nullsNotDistinct": false, + "columns": [ + "version_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "email_content_versions_version_id_check": { + "name": "email_content_versions_version_id_check", + "value": "\"email_content_versions\".\"version_id\" ~ '^ver_'" + } + }, + "isRLSEnabled": false + }, + "public.email_deliveries": { + "name": "email_deliveries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sequence_id": { + "name": "sequence_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "email_id": { + "name": "email_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "content_version_id": { + "name": "content_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "email_deliveries_team_id_teams_id_fk": { + "name": "email_deliveries_team_id_teams_id_fk", + "tableFrom": "email_deliveries", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "email_deliveries_sequence_id_sequences_id_fk": { + "name": "email_deliveries_sequence_id_sequences_id_fk", + "tableFrom": "email_deliveries", + "tableTo": "sequences", + "columnsFrom": [ + "sequence_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "email_deliveries_contact_id_contacts_id_fk": { + "name": "email_deliveries_contact_id_contacts_id_fk", + "tableFrom": "email_deliveries", + "tableTo": "contacts", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "email_deliveries_email_id_sequence_emails_id_fk": { + "name": "email_deliveries_email_id_sequence_emails_id_fk", + "tableFrom": "email_deliveries", + "tableTo": "sequence_emails", + "columnsFrom": [ + "email_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "email_deliveries_content_version_id_email_content_versions_id_fk": { + "name": "email_deliveries_content_version_id_email_content_versions_id_fk", + "tableFrom": "email_deliveries", + "tableTo": "email_content_versions", + "columnsFrom": [ + "content_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_delivery_events": { + "name": "email_delivery_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "receipt_id": { + "name": "receipt_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "outbound_message_id": { + "name": "outbound_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_event_key": { + "name": "provider_event_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_message_id": { + "name": "provider_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recipient_email": { + "name": "recipient_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "normalized_recipient": { + "name": "normalized_recipient", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bounce_class": { + "name": "bounce_class", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "smtp_code": { + "name": "smtp_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enhanced_status_code": { + "name": "enhanced_status_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "remote_mta": { + "name": "remote_mta", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": { + "email_delivery_events_connection_id_provider_event_key_idx": { + "name": "email_delivery_events_connection_id_provider_event_key_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_delivery_events_team_id_occurred_at_idx": { + "name": "email_delivery_events_team_id_occurred_at_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_delivery_events_outbound_message_id_idx": { + "name": "email_delivery_events_outbound_message_id_idx", + "columns": [ + { + "expression": "outbound_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_delivery_events_receipt_id_esp_webhook_receipts_id_fk": { + "name": "email_delivery_events_receipt_id_esp_webhook_receipts_id_fk", + "tableFrom": "email_delivery_events", + "tableTo": "esp_webhook_receipts", + "columnsFrom": [ + "receipt_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "email_delivery_events_connection_id_esp_feedback_connections_id_fk": { + "name": "email_delivery_events_connection_id_esp_feedback_connections_id_fk", + "tableFrom": "email_delivery_events", + "tableTo": "esp_feedback_connections", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "email_delivery_events_team_id_teams_id_fk": { + "name": "email_delivery_events_team_id_teams_id_fk", + "tableFrom": "email_delivery_events", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "email_delivery_events_outbound_message_id_outbound_messages_id_fk": { + "name": "email_delivery_events_outbound_message_id_outbound_messages_id_fk", + "tableFrom": "email_delivery_events", + "tableTo": "outbound_messages", + "columnsFrom": [ + "outbound_message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "email_delivery_events_event_id_unique": { + "name": "email_delivery_events_event_id_unique", + "nullsNotDistinct": false, + "columns": [ + "event_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "email_delivery_events_event_id_check": { + "name": "email_delivery_events_event_id_check", + "value": "\"email_delivery_events\".\"event_id\" ~ '^(evt|dev)_'" + } + }, + "isRLSEnabled": false + }, + "public.email_events": { + "name": "email_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sequence_id": { + "name": "sequence_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "email_id": { + "name": "email_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "link": { + "name": "link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "link_index": { + "name": "link_index", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "bounce_type": { + "name": "bounce_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bounce_reason": { + "name": "bounce_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "email_events_team_id_teams_id_fk": { + "name": "email_events_team_id_teams_id_fk", + "tableFrom": "email_events", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "email_events_sequence_id_sequences_id_fk": { + "name": "email_events_sequence_id_sequences_id_fk", + "tableFrom": "email_events", + "tableTo": "sequences", + "columnsFrom": [ + "sequence_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "email_events_contact_id_contacts_id_fk": { + "name": "email_events_contact_id_contacts_id_fk", + "tableFrom": "email_events", + "tableTo": "contacts", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "email_events_email_id_sequence_emails_id_fk": { + "name": "email_events_email_id_sequence_emails_id_fk", + "tableFrom": "email_events", + "tableTo": "sequence_emails", + "columnsFrom": [ + "email_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_suppression_actions": { + "name": "email_suppression_actions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "suppression_id": { + "name": "suppression_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_event_id": { + "name": "source_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "explanation": { + "name": "explanation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "email_suppression_actions_suppression_id_created_at_idx": { + "name": "email_suppression_actions_suppression_id_created_at_idx", + "columns": [ + { + "expression": "suppression_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_suppression_actions_team_id_teams_id_fk": { + "name": "email_suppression_actions_team_id_teams_id_fk", + "tableFrom": "email_suppression_actions", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "email_suppression_actions_suppression_id_email_suppressions_id_fk": { + "name": "email_suppression_actions_suppression_id_email_suppressions_id_fk", + "tableFrom": "email_suppression_actions", + "tableTo": "email_suppressions", + "columnsFrom": [ + "suppression_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "email_suppression_actions_source_event_id_email_delivery_events_id_fk": { + "name": "email_suppression_actions_source_event_id_email_delivery_events_id_fk", + "tableFrom": "email_suppression_actions", + "tableTo": "email_delivery_events", + "columnsFrom": [ + "source_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "email_suppression_actions_actor_user_id_user_id_fk": { + "name": "email_suppression_actions_actor_user_id_user_id_fk", + "tableFrom": "email_suppression_actions", + "tableTo": "user", + "columnsFrom": [ + "actor_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_suppressions": { + "name": "email_suppressions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "suppression_id": { + "name": "suppression_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "recipient_email": { + "name": "recipient_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "normalized_recipient": { + "name": "normalized_recipient", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recipient_hash": { + "name": "recipient_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hash_key_version": { + "name": "hash_key_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_event_id": { + "name": "source_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "first_suppressed_at": { + "name": "first_suppressed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_suppressed_at": { + "name": "last_suppressed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "released_at": { + "name": "released_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "released_by": { + "name": "released_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "release_reason": { + "name": "release_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "email_suppressions_team_id_recipient_hash_idx": { + "name": "email_suppressions_team_id_recipient_hash_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "recipient_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_suppressions_team_id_active_idx": { + "name": "email_suppressions_team_id_active_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_suppressions_team_id_teams_id_fk": { + "name": "email_suppressions_team_id_teams_id_fk", + "tableFrom": "email_suppressions", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "email_suppressions_source_event_id_email_delivery_events_id_fk": { + "name": "email_suppressions_source_event_id_email_delivery_events_id_fk", + "tableFrom": "email_suppressions", + "tableTo": "email_delivery_events", + "columnsFrom": [ + "source_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "email_suppressions_released_by_user_id_fk": { + "name": "email_suppressions_released_by_user_id_fk", + "tableFrom": "email_suppressions", + "tableTo": "user", + "columnsFrom": [ + "released_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "email_suppressions_suppression_id_unique": { + "name": "email_suppressions_suppression_id_unique", + "nullsNotDistinct": false, + "columns": [ + "suppression_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "email_suppressions_suppression_id_check": { + "name": "email_suppressions_suppression_id_check", + "value": "\"email_suppressions\".\"suppression_id\" ~ '^sup_'" + } + }, + "isRLSEnabled": false + }, + "public.email_templates": { + "name": "email_templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "purpose": { + "name": "purpose", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'marketing'" + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "email_templates_team_id_title_idx": { + "name": "email_templates_team_id_title_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_templates_team_id_teams_id_fk": { + "name": "email_templates_team_id_teams_id_fk", + "tableFrom": "email_templates", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "email_templates_template_id_unique": { + "name": "email_templates_template_id_unique", + "nullsNotDistinct": false, + "columns": [ + "template_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "email_templates_template_id_check": { + "name": "email_templates_template_id_check", + "value": "\"email_templates\".\"template_id\" ~ '^tpl_'" + }, + "email_templates_purpose_check": { + "name": "email_templates_purpose_check", + "value": "\"email_templates\".\"purpose\" in ('marketing', 'transactional')" + } + }, + "isRLSEnabled": false + }, + "public.esp_config_team_grants": { + "name": "esp_config_team_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "grant_id": { + "name": "grant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "esp_config_id": { + "name": "esp_config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "drain_until": { + "name": "drain_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "from_name": { + "name": "from_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reply_to": { + "name": "reply_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "daily_limit": { + "name": "daily_limit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "monthly_limit": { + "name": "monthly_limit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_by_type": { + "name": "created_by_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_id": { + "name": "created_by_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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()" + } + }, + "indexes": { + "esp_config_team_grants_non_revoked_team_idx": { + "name": "esp_config_team_grants_non_revoked_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"esp_config_team_grants\".\"status\" <> 'revoked'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "esp_config_team_grants_team_organization_fk": { + "name": "esp_config_team_grants_team_organization_fk", + "tableFrom": "esp_config_team_grants", + "tableTo": "teams", + "columnsFrom": [ + "team_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "esp_config_team_grants_esp_organization_fk": { + "name": "esp_config_team_grants_esp_organization_fk", + "tableFrom": "esp_config_team_grants", + "tableTo": "esp_configs", + "columnsFrom": [ + "esp_config_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "esp_config_team_grants_grant_id_unique": { + "name": "esp_config_team_grants_grant_id_unique", + "nullsNotDistinct": false, + "columns": [ + "grant_id" + ] + }, + "esp_config_team_grants_id_organization_id_unique": { + "name": "esp_config_team_grants_id_organization_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + }, + "esp_config_team_grants_id_team_esp_unique": { + "name": "esp_config_team_grants_id_team_esp_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "team_id", + "esp_config_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "esp_config_team_grants_public_id_check": { + "name": "esp_config_team_grants_public_id_check", + "value": "\"esp_config_team_grants\".\"grant_id\" ~ '^egr_'" + }, + "esp_config_team_grants_status_check": { + "name": "esp_config_team_grants_status_check", + "value": "\"esp_config_team_grants\".\"status\" IN ('active', 'draining', 'suspended', 'revoked')" + }, + "esp_config_team_grants_limit_check": { + "name": "esp_config_team_grants_limit_check", + "value": "(\"esp_config_team_grants\".\"daily_limit\" IS NULL OR \"esp_config_team_grants\".\"daily_limit\" >= 0)\n AND (\"esp_config_team_grants\".\"monthly_limit\" IS NULL OR \"esp_config_team_grants\".\"monthly_limit\" >= 0)" + }, + "esp_config_team_grants_created_by_type_check": { + "name": "esp_config_team_grants_created_by_type_check", + "value": "\"esp_config_team_grants\".\"created_by_type\" IN ('user', 'organization_key', 'system')" + } + }, + "isRLSEnabled": false + }, + "public.esp_configs": { + "name": "esp_configs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "esp_id": { + "name": "esp_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'smtp'" + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 587 + }, + "secure": { + "name": "secure", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_secret": { + "name": "encrypted_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "from_name": { + "name": "from_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "from_email": { + "name": "from_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reply_to": { + "name": "reply_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_send_rate": { + "name": "max_send_rate", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "daily_send_limit": { + "name": "daily_send_limit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "warmup_target_daily": { + "name": "warmup_target_daily", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "warmup_started_at": { + "name": "warmup_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dkim_selector": { + "name": "dkim_selector", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "secret_version": { + "name": "secret_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "last_tested_at": { + "name": "last_tested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_test_status": { + "name": "last_test_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_test_error": { + "name": "last_test_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "activated_at": { + "name": "activated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "drain_until": { + "name": "drain_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "retired_at": { + "name": "retired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "esp_configs_organization_id_idx": { + "name": "esp_configs_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "esp_configs_team_id_idx": { + "name": "esp_configs_team_id_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "esp_configs_organization_id_organizations_id_fk": { + "name": "esp_configs_organization_id_organizations_id_fk", + "tableFrom": "esp_configs", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "esp_configs_team_id_teams_id_fk": { + "name": "esp_configs_team_id_teams_id_fk", + "tableFrom": "esp_configs", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "esp_configs_esp_id_unique": { + "name": "esp_configs_esp_id_unique", + "nullsNotDistinct": false, + "columns": [ + "esp_id" + ] + }, + "esp_configs_id_organization_id_unique": { + "name": "esp_configs_id_organization_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + }, + "esp_configs_id_team_id_unique": { + "name": "esp_configs_id_team_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "team_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "esp_configs_esp_id_check": { + "name": "esp_configs_esp_id_check", + "value": "\"esp_configs\".\"esp_id\" ~ '^esp_'" + }, + "esp_configs_owner_check": { + "name": "esp_configs_owner_check", + "value": "(\"esp_configs\".\"owner_scope\" = 'organization' AND \"esp_configs\".\"organization_id\" IS NOT NULL AND \"esp_configs\".\"team_id\" IS NULL)\n OR (\"esp_configs\".\"owner_scope\" = 'team' AND \"esp_configs\".\"organization_id\" IS NULL AND \"esp_configs\".\"team_id\" IS NOT NULL)" + }, + "esp_configs_status_check": { + "name": "esp_configs_status_check", + "value": "\"esp_configs\".\"status\" IN ('draft', 'active', 'suspended', 'draining', 'retired')" + } + }, + "isRLSEnabled": false + }, + "public.esp_feedback_connections": { + "name": "esp_feedback_connections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "esp_config_id": { + "name": "esp_config_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_credentials": { + "name": "encrypted_credentials", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previous_encrypted_credentials": { + "name": "previous_encrypted_credentials", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previous_credential_expires_at": { + "name": "previous_credential_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expected_topic_arn": { + "name": "expected_topic_arn", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subscription_confirmed_at": { + "name": "subscription_confirmed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "last_received_at": { + "name": "last_received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_verified_at": { + "name": "last_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled_at": { + "name": "disabled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "esp_feedback_connections_team_id_idx": { + "name": "esp_feedback_connections_team_id_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "esp_feedback_connections_esp_config_active_idx": { + "name": "esp_feedback_connections_esp_config_active_idx", + "columns": [ + { + "expression": "esp_config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"esp_feedback_connections\".\"esp_config_id\" is not null and \"esp_feedback_connections\".\"status\" not in ('retiring', 'disabled')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "esp_feedback_connections_organization_id_organizations_id_fk": { + "name": "esp_feedback_connections_organization_id_organizations_id_fk", + "tableFrom": "esp_feedback_connections", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "esp_feedback_connections_team_id_teams_id_fk": { + "name": "esp_feedback_connections_team_id_teams_id_fk", + "tableFrom": "esp_feedback_connections", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "esp_feedback_connections_esp_config_id_esp_configs_id_fk": { + "name": "esp_feedback_connections_esp_config_id_esp_configs_id_fk", + "tableFrom": "esp_feedback_connections", + "tableTo": "esp_configs", + "columnsFrom": [ + "esp_config_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "esp_feedback_connections_connection_id_unique": { + "name": "esp_feedback_connections_connection_id_unique", + "nullsNotDistinct": false, + "columns": [ + "connection_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "esp_feedback_connections_connection_id_check": { + "name": "esp_feedback_connections_connection_id_check", + "value": "\"esp_feedback_connections\".\"connection_id\" ~ '^whc_'" + }, + "esp_feedback_connections_owner_check": { + "name": "esp_feedback_connections_owner_check", + "value": "(\n \"esp_feedback_connections\".\"owner_scope\" = 'organization'\n AND \"esp_feedback_connections\".\"organization_id\" IS NOT NULL\n AND \"esp_feedback_connections\".\"team_id\" IS NULL\n ) OR (\n \"esp_feedback_connections\".\"owner_scope\" = 'team'\n AND \"esp_feedback_connections\".\"organization_id\" IS NULL\n AND \"esp_feedback_connections\".\"team_id\" IS NOT NULL\n )" + } + }, + "isRLSEnabled": false + }, + "public.esp_webhook_receipts": { + "name": "esp_webhook_receipts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "receipt_id": { + "name": "receipt_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_request_id": { + "name": "provider_request_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_sha256": { + "name": "body_sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_payload": { + "name": "encrypted_payload", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "safe_headers": { + "name": "safe_headers", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_attempts": { + "name": "processing_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "esp_webhook_receipts_status_next_attempt_idx": { + "name": "esp_webhook_receipts_status_next_attempt_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "esp_webhook_receipts_connection_id_provider_request_id_idx": { + "name": "esp_webhook_receipts_connection_id_provider_request_id_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "esp_webhook_receipts_connection_id_esp_feedback_connections_id_fk": { + "name": "esp_webhook_receipts_connection_id_esp_feedback_connections_id_fk", + "tableFrom": "esp_webhook_receipts", + "tableTo": "esp_feedback_connections", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "esp_webhook_receipts_team_id_teams_id_fk": { + "name": "esp_webhook_receipts_team_id_teams_id_fk", + "tableFrom": "esp_webhook_receipts", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "esp_webhook_receipts_receipt_id_unique": { + "name": "esp_webhook_receipts_receipt_id_unique", + "nullsNotDistinct": false, + "columns": [ + "receipt_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "esp_webhook_receipts_receipt_id_check": { + "name": "esp_webhook_receipts_receipt_id_check", + "value": "\"esp_webhook_receipts\".\"receipt_id\" ~ '^whr_'" + } + }, + "isRLSEnabled": false + }, + "public.event_definitions": { + "name": "event_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "properties": { + "name": "properties", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "last_properties": { + "name": "last_properties", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "event_definitions_team_id_name_idx": { + "name": "event_definitions_team_id_name_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "event_definitions_team_id_teams_id_fk": { + "name": "event_definitions_team_id_teams_id_fk", + "tableFrom": "event_definitions", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.jwks": { + "name": "jwks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "alg": { + "name": "alg", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "crv": { + "name": "crv", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail_dispatch_outbox": { + "name": "mail_dispatch_outbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "dispatch_id": { + "name": "dispatch_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outbound_message_id": { + "name": "outbound_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "queue_name": { + "name": "queue_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "job_name": { + "name": "job_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "available_at": { + "name": "available_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "publish_attempts": { + "name": "publish_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mail_dispatch_outbox_due_idx": { + "name": "mail_dispatch_outbox_due_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mail_dispatch_outbox_outbound_message_id_outbound_messages_id_fk": { + "name": "mail_dispatch_outbox_outbound_message_id_outbound_messages_id_fk", + "tableFrom": "mail_dispatch_outbox", + "tableTo": "outbound_messages", + "columnsFrom": [ + "outbound_message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mail_dispatch_outbox_dispatch_id_unique": { + "name": "mail_dispatch_outbox_dispatch_id_unique", + "nullsNotDistinct": false, + "columns": [ + "dispatch_id" + ] + }, + "mail_dispatch_outbox_outbound_message_id_unique": { + "name": "mail_dispatch_outbox_outbound_message_id_unique", + "nullsNotDistinct": false, + "columns": [ + "outbound_message_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "mail_dispatch_outbox_dispatch_id_check": { + "name": "mail_dispatch_outbox_dispatch_id_check", + "value": "\"mail_dispatch_outbox\".\"dispatch_id\" ~ '^mdj_'" + }, + "mail_dispatch_outbox_state_check": { + "name": "mail_dispatch_outbox_state_check", + "value": "\"mail_dispatch_outbox\".\"state\" IN ('pending', 'publishing', 'published', 'cancelled')" + } + }, + "isRLSEnabled": false + }, + "public.media": { + "name": "media", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "media_id": { + "name": "media_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "media_lit_id": { + "name": "media_lit_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "thumbnail_url": { + "name": "thumbnail_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "alt": { + "name": "alt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "caption": { + "name": "caption", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "media_team_id_media_lit_id_idx": { + "name": "media_team_id_media_lit_id_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "media_lit_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_team_id_created_at_idx": { + "name": "media_team_id_created_at_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "media_team_id_teams_id_fk": { + "name": "media_team_id_teams_id_fk", + "tableFrom": "media", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "media_media_id_unique": { + "name": "media_media_id_unique", + "nullsNotDistinct": false, + "columns": [ + "media_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "media_media_id_check": { + "name": "media_media_id_check", + "value": "\"media\".\"media_id\" ~ '^med_'" + } + }, + "isRLSEnabled": false + }, + "public.media_references": { + "name": "media_references", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "media_id": { + "name": "media_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_internal_id": { + "name": "resource_internal_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "resource_public_id": { + "name": "resource_public_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_resource_internal_id": { + "name": "parent_resource_internal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "parent_resource_public_id": { + "name": "parent_resource_public_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "media_references_resource_idx": { + "name": "media_references_resource_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_internal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_references_media_id_idx": { + "name": "media_references_media_id_idx", + "columns": [ + { + "expression": "media_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_references_resource_media_idx": { + "name": "media_references_resource_media_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_internal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "media_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "media_references_team_id_teams_id_fk": { + "name": "media_references_team_id_teams_id_fk", + "tableFrom": "media_references", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "media_references_media_id_media_id_fk": { + "name": "media_references_media_id_media_id_fk", + "tableFrom": "media_references", + "tableTo": "media", + "columnsFrom": [ + "media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_access_token": { + "name": "oauth_access_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorization_code_id": { + "name": "authorization_code_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resources": { + "name": "resources", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "requested_user_info_claims": { + "name": "requested_user_info_claims", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "refresh_id": { + "name": "refresh_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "revoked": { + "name": "revoked", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "confirmation": { + "name": "confirmation", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "auth_oauth_access_token_client_id_idx": { + "name": "auth_oauth_access_token_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auth_oauth_access_token_session_id_idx": { + "name": "auth_oauth_access_token_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auth_oauth_access_token_user_id_idx": { + "name": "auth_oauth_access_token_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auth_oauth_access_token_authorization_code_id_idx": { + "name": "auth_oauth_access_token_authorization_code_id_idx", + "columns": [ + { + "expression": "authorization_code_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auth_oauth_access_token_refresh_id_idx": { + "name": "auth_oauth_access_token_refresh_id_idx", + "columns": [ + { + "expression": "refresh_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_access_token_client_id_oauth_client_client_id_fk": { + "name": "oauth_access_token_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "oauth_client", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_access_token_session_id_session_id_fk": { + "name": "oauth_access_token_session_id_session_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "session", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauth_access_token_user_id_user_id_fk": { + "name": "oauth_access_token_user_id_user_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_access_token_refresh_id_oauth_refresh_token_id_fk": { + "name": "oauth_access_token_refresh_id_oauth_refresh_token_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "oauth_refresh_token", + "columnsFrom": [ + "refresh_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_access_token_token_unique": { + "name": "oauth_access_token_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_client": { + "name": "oauth_client", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_discovery_id": { + "name": "client_discovery_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "skip_consent": { + "name": "skip_consent", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enable_end_session": { + "name": "enable_end_session", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "client_credentials_scopes": { + "name": "client_credentials_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contacts": { + "name": "contacts", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "tos": { + "name": "tos", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "policy": { + "name": "policy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_id": { + "name": "software_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_version": { + "name": "software_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_statement": { + "name": "software_statement", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "post_logout_redirect_uris": { + "name": "post_logout_redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "backchannel_logout_uri": { + "name": "backchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backchannel_logout_session_required": { + "name": "backchannel_logout_session_required", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "application_type": { + "name": "application_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "jwks": { + "name": "jwks", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "jwks_uri": { + "name": "jwks_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grant_types": { + "name": "grant_types", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "response_types": { + "name": "response_types", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "require_pkce": { + "name": "require_pkce", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "dpop_bound_access_tokens": { + "name": "dpop_bound_access_tokens", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "auth_oauth_client_user_id_idx": { + "name": "auth_oauth_client_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_client_user_id_user_id_fk": { + "name": "oauth_client_user_id_user_id_fk", + "tableFrom": "oauth_client", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_client_client_id_unique": { + "name": "oauth_client_client_id_unique", + "nullsNotDistinct": false, + "columns": [ + "client_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_client_assertion": { + "name": "oauth_client_assertion", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "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.oauth_client_resource": { + "name": "oauth_client_resource", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "auth_oauth_client_resource_client_id_idx": { + "name": "auth_oauth_client_resource_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auth_oauth_client_resource_resource_id_idx": { + "name": "auth_oauth_client_resource_resource_id_idx", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auth_oauth_client_resource_client_id_resource_id_idx": { + "name": "auth_oauth_client_resource_client_id_resource_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_client_resource_client_id_oauth_client_client_id_fk": { + "name": "oauth_client_resource_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_client_resource", + "tableTo": "oauth_client", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_client_resource_resource_id_oauth_resource_identifier_fk": { + "name": "oauth_client_resource_resource_id_oauth_resource_identifier_fk", + "tableFrom": "oauth_client_resource", + "tableTo": "oauth_resource", + "columnsFrom": [ + "resource_id" + ], + "columnsTo": [ + "identifier" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_consent": { + "name": "oauth_consent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resources": { + "name": "resources", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "requested_user_info_claims": { + "name": "requested_user_info_claims", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "auth_oauth_consent_client_id_idx": { + "name": "auth_oauth_consent_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auth_oauth_consent_user_id_idx": { + "name": "auth_oauth_consent_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_consent_client_id_oauth_client_client_id_fk": { + "name": "oauth_consent_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_consent", + "tableTo": "oauth_client", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_consent_user_id_user_id_fk": { + "name": "oauth_consent_user_id_user_id_fk", + "tableFrom": "oauth_consent", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_post_login_team_selections": { + "name": "oauth_post_login_team_selections", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "oauth_post_login_team_selections_session_id_session_id_fk": { + "name": "oauth_post_login_team_selections_session_id_session_id_fk", + "tableFrom": "oauth_post_login_team_selections", + "tableTo": "session", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_post_login_team_selections_team_id_teams_id_fk": { + "name": "oauth_post_login_team_selections_team_id_teams_id_fk", + "tableFrom": "oauth_post_login_team_selections", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_refresh_token": { + "name": "oauth_refresh_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorization_code_id": { + "name": "authorization_code_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resources": { + "name": "resources", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "requested_user_info_claims": { + "name": "requested_user_info_claims", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "revoked": { + "name": "revoked", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rotated_at": { + "name": "rotated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rotation_replay_response": { + "name": "rotation_replay_response", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rotation_replay_expires_at": { + "name": "rotation_replay_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auth_time": { + "name": "auth_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "confirmation": { + "name": "confirmation", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "auth_oauth_refresh_token_client_id_idx": { + "name": "auth_oauth_refresh_token_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auth_oauth_refresh_token_authorization_code_id_idx": { + "name": "auth_oauth_refresh_token_authorization_code_id_idx", + "columns": [ + { + "expression": "authorization_code_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auth_oauth_refresh_token_session_id_idx": { + "name": "auth_oauth_refresh_token_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auth_oauth_refresh_token_user_id_idx": { + "name": "auth_oauth_refresh_token_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_refresh_token_client_id_oauth_client_client_id_fk": { + "name": "oauth_refresh_token_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "oauth_client", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_refresh_token_session_id_session_id_fk": { + "name": "oauth_refresh_token_session_id_session_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "session", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauth_refresh_token_user_id_user_id_fk": { + "name": "oauth_refresh_token_user_id_user_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_refresh_token_token_unique": { + "name": "oauth_refresh_token_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_resource": { + "name": "oauth_resource", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_ttl": { + "name": "access_token_ttl", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refresh_token_ttl": { + "name": "refresh_token_ttl", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "signing_algorithm": { + "name": "signing_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "signing_key_id": { + "name": "signing_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_scopes": { + "name": "allowed_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "custom_claims": { + "name": "custom_claims", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "dpop_bound_access_tokens_required": { + "name": "dpop_bound_access_tokens_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "policy_version": { + "name": "policy_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_resource_identifier_unique": { + "name": "oauth_resource_identifier_unique", + "nullsNotDistinct": false, + "columns": [ + "identifier" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ongoing_sequences": { + "name": "ongoing_sequences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sequence_id": { + "name": "sequence_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "next_email_scheduled_time": { + "name": "next_email_scheduled_time", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "retry_count": { + "name": "retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "priority": { + "name": "priority", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sent_email_ids": { + "name": "sent_email_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "trigger_event": { + "name": "trigger_event", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "ongoing_sequences_sequence_id_contact_id_idx": { + "name": "ongoing_sequences_sequence_id_contact_id_idx", + "columns": [ + { + "expression": "sequence_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "contact_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ongoing_sequences_next_email_scheduled_time_idx": { + "name": "ongoing_sequences_next_email_scheduled_time_idx", + "columns": [ + { + "expression": "next_email_scheduled_time", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ongoing_sequences_team_id_teams_id_fk": { + "name": "ongoing_sequences_team_id_teams_id_fk", + "tableFrom": "ongoing_sequences", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ongoing_sequences_sequence_id_sequences_id_fk": { + "name": "ongoing_sequences_sequence_id_sequences_id_fk", + "tableFrom": "ongoing_sequences", + "tableTo": "sequences", + "columnsFrom": [ + "sequence_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ongoing_sequences_contact_id_contacts_id_fk": { + "name": "ongoing_sequences_contact_id_contacts_id_fk", + "tableFrom": "ongoing_sequences", + "tableTo": "contacts", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_api_keys": { + "name": "organization_api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organization_api_key_id": { + "name": "organization_api_key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_prefix": { + "name": "key_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organization_api_keys_organization_id_idx": { + "name": "organization_api_keys_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_api_keys_organization_id_organizations_id_fk": { + "name": "organization_api_keys_organization_id_organizations_id_fk", + "tableFrom": "organization_api_keys", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_api_keys_created_by_user_id_user_id_fk": { + "name": "organization_api_keys_created_by_user_id_user_id_fk", + "tableFrom": "organization_api_keys", + "tableTo": "user", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_api_keys_organization_api_key_id_unique": { + "name": "organization_api_keys_organization_api_key_id_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_api_key_id" + ] + }, + "organization_api_keys_key_hash_unique": { + "name": "organization_api_keys_key_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "key_hash" + ] + } + }, + "policies": {}, + "checkConstraints": { + "organization_api_keys_public_id_check": { + "name": "organization_api_keys_public_id_check", + "value": "\"organization_api_keys\".\"organization_api_key_id\" ~ '^oak_'" + } + }, + "isRLSEnabled": false + }, + "public.organization_audit_events": { + "name": "organization_audit_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "esp_config_id": { + "name": "esp_config_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "esp_grant_id": { + "name": "esp_grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "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": { + "organization_audit_events_organization_id_created_at_idx": { + "name": "organization_audit_events_organization_id_created_at_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "organization_audit_events_team_id_created_at_idx": { + "name": "organization_audit_events_team_id_created_at_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_audit_events_organization_id_organizations_id_fk": { + "name": "organization_audit_events_organization_id_organizations_id_fk", + "tableFrom": "organization_audit_events", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_delivery_policies": { + "name": "organization_delivery_policies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "default_esp_config_id": { + "name": "default_esp_config_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "auto_grant_default_esp": { + "name": "auto_grant_default_esp", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "default_daily_limit": { + "name": "default_daily_limit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "default_monthly_limit": { + "name": "default_monthly_limit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "aggregate_daily_limit": { + "name": "aggregate_daily_limit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "aggregate_monthly_limit": { + "name": "aggregate_monthly_limit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "team_esp_enabled_by_default": { + "name": "team_esp_enabled_by_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "team_can_change_default": { + "name": "team_can_change_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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()" + } + }, + "indexes": {}, + "foreignKeys": { + "organization_delivery_policies_organization_id_organizations_id_fk": { + "name": "organization_delivery_policies_organization_id_organizations_id_fk", + "tableFrom": "organization_delivery_policies", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_delivery_policies_default_esp_fk": { + "name": "organization_delivery_policies_default_esp_fk", + "tableFrom": "organization_delivery_policies", + "tableTo": "esp_configs", + "columnsFrom": [ + "default_esp_config_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_delivery_policies_organization_id_unique": { + "name": "organization_delivery_policies_organization_id_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "organization_delivery_policies_limit_check": { + "name": "organization_delivery_policies_limit_check", + "value": "(\"organization_delivery_policies\".\"default_daily_limit\" IS NULL OR \"organization_delivery_policies\".\"default_daily_limit\" >= 0)\n AND (\"organization_delivery_policies\".\"default_monthly_limit\" IS NULL OR \"organization_delivery_policies\".\"default_monthly_limit\" >= 0)\n AND (\"organization_delivery_policies\".\"aggregate_daily_limit\" IS NULL OR \"organization_delivery_policies\".\"aggregate_daily_limit\" >= 0)\n AND (\"organization_delivery_policies\".\"aggregate_monthly_limit\" IS NULL OR \"organization_delivery_policies\".\"aggregate_monthly_limit\" >= 0)" + } + }, + "isRLSEnabled": false + }, + "public.organization_esp_quota_reservations": { + "name": "organization_esp_quota_reservations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "reservation_id": { + "name": "reservation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outbound_message_id": { + "name": "outbound_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "grant_id": { + "name": "grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "day_period_start": { + "name": "day_period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "month_period_start": { + "name": "month_period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'reserved'" + }, + "release_reason": { + "name": "release_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "committed_at": { + "name": "committed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "released_at": { + "name": "released_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "organization_esp_quota_reservations_outbound_message_id_outbound_messages_id_fk": { + "name": "organization_esp_quota_reservations_outbound_message_id_outbound_messages_id_fk", + "tableFrom": "organization_esp_quota_reservations", + "tableTo": "outbound_messages", + "columnsFrom": [ + "outbound_message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "organization_esp_quota_reservations_grant_id_esp_config_team_grants_id_fk": { + "name": "organization_esp_quota_reservations_grant_id_esp_config_team_grants_id_fk", + "tableFrom": "organization_esp_quota_reservations", + "tableTo": "esp_config_team_grants", + "columnsFrom": [ + "grant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "organization_esp_quota_reservations_organization_id_organizations_id_fk": { + "name": "organization_esp_quota_reservations_organization_id_organizations_id_fk", + "tableFrom": "organization_esp_quota_reservations", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "organization_esp_quota_reservations_grant_organization_fk": { + "name": "organization_esp_quota_reservations_grant_organization_fk", + "tableFrom": "organization_esp_quota_reservations", + "tableTo": "esp_config_team_grants", + "columnsFrom": [ + "grant_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_esp_quota_reservations_reservation_id_unique": { + "name": "organization_esp_quota_reservations_reservation_id_unique", + "nullsNotDistinct": false, + "columns": [ + "reservation_id" + ] + }, + "organization_esp_quota_reservations_outbound_message_id_unique": { + "name": "organization_esp_quota_reservations_outbound_message_id_unique", + "nullsNotDistinct": false, + "columns": [ + "outbound_message_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "organization_esp_quota_reservations_reservation_id_check": { + "name": "organization_esp_quota_reservations_reservation_id_check", + "value": "\"organization_esp_quota_reservations\".\"reservation_id\" ~ '^qrs_'" + }, + "organization_esp_quota_reservations_state_check": { + "name": "organization_esp_quota_reservations_state_check", + "value": "\"organization_esp_quota_reservations\".\"state\" IN ('reserved', 'committed', 'released')" + } + }, + "isRLSEnabled": false + }, + "public.organization_esp_usage_buckets": { + "name": "organization_esp_usage_buckets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "bucket_scope": { + "name": "bucket_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "grant_id": { + "name": "grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "period_type": { + "name": "period_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "period_start": { + "name": "period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "reserved_count": { + "name": "reserved_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "accepted_count": { + "name": "accepted_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organization_esp_usage_buckets_grant_period_idx": { + "name": "organization_esp_usage_buckets_grant_period_idx", + "columns": [ + { + "expression": "grant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"organization_esp_usage_buckets\".\"grant_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "organization_esp_usage_buckets_organization_period_idx": { + "name": "organization_esp_usage_buckets_organization_period_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"organization_esp_usage_buckets\".\"bucket_scope\" = 'organization'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_esp_usage_buckets_organization_id_organizations_id_fk": { + "name": "organization_esp_usage_buckets_organization_id_organizations_id_fk", + "tableFrom": "organization_esp_usage_buckets", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "organization_esp_usage_buckets_grant_id_esp_config_team_grants_id_fk": { + "name": "organization_esp_usage_buckets_grant_id_esp_config_team_grants_id_fk", + "tableFrom": "organization_esp_usage_buckets", + "tableTo": "esp_config_team_grants", + "columnsFrom": [ + "grant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "organization_esp_usage_buckets_grant_organization_fk": { + "name": "organization_esp_usage_buckets_grant_organization_fk", + "tableFrom": "organization_esp_usage_buckets", + "tableTo": "esp_config_team_grants", + "columnsFrom": [ + "grant_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "organization_esp_usage_buckets_scope_check": { + "name": "organization_esp_usage_buckets_scope_check", + "value": "(\n \"organization_esp_usage_buckets\".\"bucket_scope\" = 'grant' AND \"organization_esp_usage_buckets\".\"grant_id\" IS NOT NULL\n ) OR (\n \"organization_esp_usage_buckets\".\"bucket_scope\" = 'organization' AND \"organization_esp_usage_buckets\".\"grant_id\" IS NULL\n )" + }, + "organization_esp_usage_buckets_period_check": { + "name": "organization_esp_usage_buckets_period_check", + "value": "\"organization_esp_usage_buckets\".\"period_type\" IN ('day', 'month')" + }, + "organization_esp_usage_buckets_count_check": { + "name": "organization_esp_usage_buckets_count_check", + "value": "\"organization_esp_usage_buckets\".\"reserved_count\" >= 0 AND \"organization_esp_usage_buckets\".\"accepted_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.organization_members": { + "name": "organization_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "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()" + } + }, + "indexes": { + "organization_members_organization_id_user_id_idx": { + "name": "organization_members_organization_id_user_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_members_organization_id_organizations_id_fk": { + "name": "organization_members_organization_id_organizations_id_fk", + "tableFrom": "organization_members", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_members_user_id_user_id_fk": { + "name": "organization_members_user_id_user_id_fk", + "tableFrom": "organization_members", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "organization_members_role_check": { + "name": "organization_members_role_check", + "value": "\"organization_members\".\"role\" IN ('owner', 'admin', 'member')" + } + }, + "isRLSEnabled": false + }, + "public.organization_plan_policies": { + "name": "organization_plan_policies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "teams_limit_override": { + "name": "teams_limit_override", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "contacts_limit_override": { + "name": "contacts_limit_override", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "marketing_ramp_stage": { + "name": "marketing_ramp_stage", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "marketing_ramp_clean_days": { + "name": "marketing_ramp_clean_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "marketing_ramp_evaluated_at": { + "name": "marketing_ramp_evaluated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "past_due_since": { + "name": "past_due_since", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "pacing_exempt": { + "name": "pacing_exempt", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "signup_risk_score": { + "name": "signup_risk_score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "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()" + } + }, + "indexes": {}, + "foreignKeys": { + "organization_plan_policies_organization_id_organizations_id_fk": { + "name": "organization_plan_policies_organization_id_organizations_id_fk", + "tableFrom": "organization_plan_policies", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_plan_policies_organization_id_unique": { + "name": "organization_plan_policies_organization_id_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "organization_plan_policies_overrides_check": { + "name": "organization_plan_policies_overrides_check", + "value": "(\"organization_plan_policies\".\"teams_limit_override\" IS NULL OR \"organization_plan_policies\".\"teams_limit_override\" > 0)\n AND (\"organization_plan_policies\".\"contacts_limit_override\" IS NULL OR \"organization_plan_policies\".\"contacts_limit_override\" > 0)" + }, + "organization_plan_policies_ramp_check": { + "name": "organization_plan_policies_ramp_check", + "value": "\"organization_plan_policies\".\"marketing_ramp_stage\" BETWEEN 0 AND 3 AND \"organization_plan_policies\".\"marketing_ramp_clean_days\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "pending_team_name": { + "name": "pending_team_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organizations_organization_id_unique": { + "name": "organizations_organization_id_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "organizations_organization_id_check": { + "name": "organizations_organization_id_check", + "value": "\"organizations\".\"organization_id\" ~ '^org_'" + }, + "organizations_status_check": { + "name": "organizations_status_check", + "value": "\"organizations\".\"status\" IN ('pending_payment', 'active', 'suspended', 'abandoned', 'closed')" + } + }, + "isRLSEnabled": false + }, + "public.outbound_messages": { + "name": "outbound_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "delivery_source_type": { + "name": "delivery_source_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "esp_config_id": { + "name": "esp_config_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "esp_grant_id": { + "name": "esp_grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "feedback_connection_id": { + "name": "feedback_connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "submission_key": { + "name": "submission_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "campaign_delivery_id": { + "name": "campaign_delivery_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "transactional_email_id": { + "name": "transactional_email_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "recipient_email": { + "name": "recipient_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "normalized_recipient": { + "name": "normalized_recipient", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rfc_message_id": { + "name": "rfc_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_message_id": { + "name": "provider_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "delivery_status": { + "name": "delivery_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "feedback_status": { + "name": "feedback_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "bounced_at": { + "name": "bounced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "complained_at": { + "name": "complained_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_event_at": { + "name": "last_event_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "outbound_messages_team_id_created_at_idx": { + "name": "outbound_messages_team_id_created_at_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbound_messages_connection_provider_msg_idx": { + "name": "outbound_messages_connection_provider_msg_idx", + "columns": [ + { + "expression": "feedback_connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbound_messages_team_id_recipient_created_at_idx": { + "name": "outbound_messages_team_id_recipient_created_at_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "normalized_recipient", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "outbound_messages_team_id_teams_id_fk": { + "name": "outbound_messages_team_id_teams_id_fk", + "tableFrom": "outbound_messages", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "outbound_messages_esp_config_id_esp_configs_id_fk": { + "name": "outbound_messages_esp_config_id_esp_configs_id_fk", + "tableFrom": "outbound_messages", + "tableTo": "esp_configs", + "columnsFrom": [ + "esp_config_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "outbound_messages_esp_grant_id_esp_config_team_grants_id_fk": { + "name": "outbound_messages_esp_grant_id_esp_config_team_grants_id_fk", + "tableFrom": "outbound_messages", + "tableTo": "esp_config_team_grants", + "columnsFrom": [ + "esp_grant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "outbound_messages_feedback_connection_id_esp_feedback_connections_id_fk": { + "name": "outbound_messages_feedback_connection_id_esp_feedback_connections_id_fk", + "tableFrom": "outbound_messages", + "tableTo": "esp_feedback_connections", + "columnsFrom": [ + "feedback_connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "outbound_messages_campaign_delivery_id_email_deliveries_id_fk": { + "name": "outbound_messages_campaign_delivery_id_email_deliveries_id_fk", + "tableFrom": "outbound_messages", + "tableTo": "email_deliveries", + "columnsFrom": [ + "campaign_delivery_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "outbound_messages_transactional_email_id_transactional_emails_id_fk": { + "name": "outbound_messages_transactional_email_id_transactional_emails_id_fk", + "tableFrom": "outbound_messages", + "tableTo": "transactional_emails", + "columnsFrom": [ + "transactional_email_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "outbound_messages_message_id_unique": { + "name": "outbound_messages_message_id_unique", + "nullsNotDistinct": false, + "columns": [ + "message_id" + ] + }, + "outbound_messages_submission_key_unique": { + "name": "outbound_messages_submission_key_unique", + "nullsNotDistinct": false, + "columns": [ + "submission_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "outbound_messages_message_id_check": { + "name": "outbound_messages_message_id_check", + "value": "\"outbound_messages\".\"message_id\" ~ '^msg_'" + }, + "outbound_messages_delivery_pin_check": { + "name": "outbound_messages_delivery_pin_check", + "value": "(\n \"outbound_messages\".\"delivery_source_type\" = 'team'\n AND \"outbound_messages\".\"esp_config_id\" IS NOT NULL\n AND \"outbound_messages\".\"esp_grant_id\" IS NULL\n ) OR (\n \"outbound_messages\".\"delivery_source_type\" = 'organization'\n AND \"outbound_messages\".\"esp_config_id\" IS NOT NULL\n AND \"outbound_messages\".\"esp_grant_id\" IS NOT NULL\n ) OR (\n \"outbound_messages\".\"delivery_source_type\" IN ('team', 'organization')\n AND \"outbound_messages\".\"esp_config_id\" IS NULL\n AND \"outbound_messages\".\"esp_grant_id\" IS NULL\n AND \"outbound_messages\".\"delivery_status\" <> 'queued'\n )" + } + }, + "isRLSEnabled": false + }, + "public.plan_send_reservations": { + "name": "plan_send_reservations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "outbound_message_id": { + "name": "outbound_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "bucket_id": { + "name": "bucket_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'reserved'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "committed_at": { + "name": "committed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "released_at": { + "name": "released_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plan_send_reservations_outbound_uidx": { + "name": "plan_send_reservations_outbound_uidx", + "columns": [ + { + "expression": "outbound_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plan_send_reservations_expiry_idx": { + "name": "plan_send_reservations_expiry_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plan_send_reservations_organization_id_organizations_id_fk": { + "name": "plan_send_reservations_organization_id_organizations_id_fk", + "tableFrom": "plan_send_reservations", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "plan_send_reservations_bucket_id_plan_send_usage_buckets_id_fk": { + "name": "plan_send_reservations_bucket_id_plan_send_usage_buckets_id_fk", + "tableFrom": "plan_send_reservations", + "tableTo": "plan_send_usage_buckets", + "columnsFrom": [ + "bucket_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "plan_send_reservations_amount_check": { + "name": "plan_send_reservations_amount_check", + "value": "\"plan_send_reservations\".\"amount\" > 0" + }, + "plan_send_reservations_state_check": { + "name": "plan_send_reservations_state_check", + "value": "\"plan_send_reservations\".\"state\" IN ('reserved', 'committed', 'released')" + } + }, + "isRLSEnabled": false + }, + "public.plan_send_usage_buckets": { + "name": "plan_send_usage_buckets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "bucket_month": { + "name": "bucket_month", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "committed": { + "name": "committed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reserved": { + "name": "reserved", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "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()" + } + }, + "indexes": { + "plan_send_usage_buckets_organization_month_uidx": { + "name": "plan_send_usage_buckets_organization_month_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "bucket_month", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plan_send_usage_buckets_organization_id_organizations_id_fk": { + "name": "plan_send_usage_buckets_organization_id_organizations_id_fk", + "tableFrom": "plan_send_usage_buckets", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "plan_send_usage_buckets_count_check": { + "name": "plan_send_usage_buckets_count_check", + "value": "\"plan_send_usage_buckets\".\"committed\" >= 0 AND \"plan_send_usage_buckets\".\"reserved\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.reputation_connections": { + "name": "reputation_connections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_secret": { + "name": "encrypted_secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_label": { + "name": "account_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "alert_state": { + "name": "alert_state", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "subject_state": { + "name": "subject_state", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "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()" + } + }, + "indexes": { + "reputation_connections_team_id_provider_idx": { + "name": "reputation_connections_team_id_provider_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reputation_connections_next_sync_at_idx": { + "name": "reputation_connections_next_sync_at_idx", + "columns": [ + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reputation_connections_team_id_teams_id_fk": { + "name": "reputation_connections_team_id_teams_id_fk", + "tableFrom": "reputation_connections", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "reputation_connections_provider_check": { + "name": "reputation_connections_provider_check", + "value": "\"reputation_connections\".\"provider\" IN ('google_postmaster', 'microsoft_snds')" + }, + "reputation_connections_status_check": { + "name": "reputation_connections_status_check", + "value": "\"reputation_connections\".\"status\" IN ('active', 'error')" + } + }, + "isRLSEnabled": false + }, + "public.reputation_daily_stats": { + "name": "reputation_daily_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "day": { + "name": "day", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "metrics": { + "name": "metrics", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "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()" + } + }, + "indexes": { + "reputation_daily_stats_connection_subject_day_idx": { + "name": "reputation_daily_stats_connection_subject_day_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "day", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reputation_daily_stats_connection_id_reputation_connections_id_fk": { + "name": "reputation_daily_stats_connection_id_reputation_connections_id_fk", + "tableFrom": "reputation_daily_stats", + "tableTo": "reputation_connections", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rules": { + "name": "rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sequence_id": { + "name": "sequence_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_date_in_millis": { + "name": "event_date_in_millis", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "event_data": { + "name": "event_data", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "event_filter": { + "name": "event_filter", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "rules_team_id_event_event_data_idx": { + "name": "rules_team_id_event_event_data_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_data", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "rules_team_id_teams_id_fk": { + "name": "rules_team_id_teams_id_fk", + "tableFrom": "rules", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "rules_sequence_id_sequences_id_fk": { + "name": "rules_sequence_id_sequences_id_fk", + "tableFrom": "rules", + "tableTo": "sequences", + "columnsFrom": [ + "sequence_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "rules_rule_id_unique": { + "name": "rules_rule_id_unique", + "nullsNotDistinct": false, + "columns": [ + "rule_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "rules_rule_id_check": { + "name": "rules_rule_id_check", + "value": "\"rules\".\"rule_id\" ~ '^rule_'" + } + }, + "isRLSEnabled": false + }, + "public.segments": { + "name": "segments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "segment_id": { + "name": "segment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filter": { + "name": "filter", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "segments_team_id_name_idx": { + "name": "segments_team_id_name_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "segments_team_id_teams_id_fk": { + "name": "segments_team_id_teams_id_fk", + "tableFrom": "segments", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "segments_segment_id_unique": { + "name": "segments_segment_id_unique", + "nullsNotDistinct": false, + "columns": [ + "segment_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "segments_segment_id_check": { + "name": "segments_segment_id_check", + "value": "\"segments\".\"segment_id\" ~ '^seg_'" + } + }, + "isRLSEnabled": false + }, + "public.sending_domains": { + "name": "sending_domains", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "domain_id": { + "name": "domain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "challenge_token_hash": { + "name": "challenge_token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_check_at": { + "name": "next_check_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failed_check_count": { + "name": "failed_check_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "first_failed_at": { + "name": "first_failed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sending_domains_organization_domain_uidx": { + "name": "sending_domains_organization_domain_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sending_domains_organization_id_organizations_id_fk": { + "name": "sending_domains_organization_id_organizations_id_fk", + "tableFrom": "sending_domains", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sending_domains_domain_id_unique": { + "name": "sending_domains_domain_id_unique", + "nullsNotDistinct": false, + "columns": [ + "domain_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "sending_domains_domain_id_check": { + "name": "sending_domains_domain_id_check", + "value": "\"sending_domains\".\"domain_id\" ~ '^domain_'" + }, + "sending_domains_status_check": { + "name": "sending_domains_status_check", + "value": "\"sending_domains\".\"status\" IN ('pending', 'verified', 'revoked', 'failed')" + }, + "sending_domains_failed_check_count_check": { + "name": "sending_domains_failed_check_count_check", + "value": "\"sending_domains\".\"failed_check_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.sequence_emails": { + "name": "sequence_emails", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "sequence_id": { + "name": "sequence_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "email_id": { + "name": "email_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "delay_in_millis": { + "name": "delay_in_millis", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 86400000 + }, + "published": { + "name": "published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action_type": { + "name": "action_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action_data": { + "name": "action_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_test_sent_at": { + "name": "last_test_sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "content_scan_verdict": { + "name": "content_scan_verdict", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_scan_hash": { + "name": "content_scan_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_scan_reasons": { + "name": "content_scan_reasons", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "content_scanned_at": { + "name": "content_scanned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "sequence_emails_sequence_id_email_id_idx": { + "name": "sequence_emails_sequence_id_email_id_idx", + "columns": [ + { + "expression": "sequence_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sequence_emails_sequence_id_sequences_id_fk": { + "name": "sequence_emails_sequence_id_sequences_id_fk", + "tableFrom": "sequence_emails", + "tableTo": "sequences", + "columnsFrom": [ + "sequence_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "sequence_emails_email_id_check": { + "name": "sequence_emails_email_id_check", + "value": "\"sequence_emails\".\"email_id\" ~ '^email_'" + } + }, + "isRLSEnabled": false + }, + "public.sequence_entries": { + "name": "sequence_entries", + "schema": "", + "columns": { + "sequence_id": { + "name": "sequence_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "first_entered_at": { + "name": "first_entered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_entered_at": { + "name": "last_entered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "entry_count": { + "name": "entry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + } + }, + "indexes": {}, + "foreignKeys": { + "sequence_entries_sequence_id_sequences_id_fk": { + "name": "sequence_entries_sequence_id_sequences_id_fk", + "tableFrom": "sequence_entries", + "tableTo": "sequences", + "columnsFrom": [ + "sequence_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sequence_entries_contact_id_contacts_id_fk": { + "name": "sequence_entries_contact_id_contacts_id_fk", + "tableFrom": "sequence_entries", + "tableTo": "contacts", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sequence_entries_sequence_id_contact_id_pk": { + "name": "sequence_entries_sequence_id_contact_id_pk", + "columns": [ + "sequence_id", + "contact_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sequences": { + "name": "sequences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sequence_id": { + "name": "sequence_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "delivery_source_intent": { + "name": "delivery_source_intent", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "delivery_source_type": { + "name": "delivery_source_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outbox_id": { + "name": "outbox_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "esp_grant_id": { + "name": "esp_grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trigger_data": { + "name": "trigger_data", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trigger_filter": { + "name": "trigger_filter", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "reentry": { + "name": "reentry", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'every_time'" + }, + "reentry_period_days": { + "name": "reentry_period_days", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "filter": { + "name": "filter", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "exclude_filter": { + "name": "exclude_filter", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "emails_order": { + "name": "emails_order", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "entrants": { + "name": "entrants", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "report": { + "name": "report", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "view_in_browser": { + "name": "view_in_browser", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "web_published": { + "name": "web_published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "web_slug": { + "name": "web_slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "web_seo_title": { + "name": "web_seo_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "web_seo_description": { + "name": "web_seo_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "web_og_image": { + "name": "web_og_image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "web_noindex": { + "name": "web_noindex", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "web_canonical_url": { + "name": "web_canonical_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "sequences_team_id_web_slug_idx": { + "name": "sequences_team_id_web_slug_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "web_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"sequences\".\"web_slug\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sequences_team_id_teams_id_fk": { + "name": "sequences_team_id_teams_id_fk", + "tableFrom": "sequences", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sequences_outbox_id_esp_configs_id_fk": { + "name": "sequences_outbox_id_esp_configs_id_fk", + "tableFrom": "sequences", + "tableTo": "esp_configs", + "columnsFrom": [ + "outbox_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "sequences_esp_grant_id_esp_config_team_grants_id_fk": { + "name": "sequences_esp_grant_id_esp_config_team_grants_id_fk", + "tableFrom": "sequences", + "tableTo": "esp_config_team_grants", + "columnsFrom": [ + "esp_grant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sequences_sequence_id_unique": { + "name": "sequences_sequence_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sequence_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "sequences_sequence_id_check": { + "name": "sequences_sequence_id_check", + "value": "\"sequences\".\"sequence_id\" ~ '^seq_'" + }, + "sequences_delivery_pin_check": { + "name": "sequences_delivery_pin_check", + "value": "(\n \"sequences\".\"delivery_source_type\" IS NULL\n AND \"sequences\".\"outbox_id\" IS NULL\n AND \"sequences\".\"esp_grant_id\" IS NULL\n ) OR (\n \"sequences\".\"delivery_source_type\" = 'team'\n AND \"sequences\".\"outbox_id\" IS NOT NULL\n AND \"sequences\".\"esp_grant_id\" IS NULL\n ) OR (\n \"sequences\".\"delivery_source_type\" = 'organization'\n AND \"sequences\".\"outbox_id\" IS NOT NULL\n AND \"sequences\".\"esp_grant_id\" IS NOT NULL\n )" + } + }, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "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": { + "auth_session_user_id_idx": { + "name": "auth_session_user_id_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.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "mailing_address": { + "name": "mailing_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "web_enabled": { + "name": "web_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "web_handle": { + "name": "web_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "web_title": { + "name": "web_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "web_description": { + "name": "web_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "web_signup_form_id": { + "name": "web_signup_form_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tracking_domain": { + "name": "tracking_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tracking_domain_status": { + "name": "tracking_domain_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tracking_domain_error": { + "name": "tracking_domain_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tracking_domain_verified_at": { + "name": "tracking_domain_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "tracking_domain_checked_at": { + "name": "tracking_domain_checked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "tracking_domain_failures": { + "name": "tracking_domain_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "retired_tracking_domains": { + "name": "retired_tracking_domains", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "settings_team_id_teams_id_fk": { + "name": "settings_team_id_teams_id_fk", + "tableFrom": "settings", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "settings_web_signup_form_id_signup_forms_id_fk": { + "name": "settings_web_signup_form_id_signup_forms_id_fk", + "tableFrom": "settings", + "tableTo": "signup_forms", + "columnsFrom": [ + "web_signup_form_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_team_id_unique": { + "name": "settings_team_id_unique", + "nullsNotDistinct": false, + "columns": [ + "team_id" + ] + }, + "settings_web_handle_unique": { + "name": "settings_web_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "web_handle" + ] + }, + "settings_tracking_domain_unique": { + "name": "settings_tracking_domain_unique", + "nullsNotDistinct": false, + "columns": [ + "tracking_domain" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.signup_confirmations": { + "name": "signup_confirmations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "form_id": { + "name": "form_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "confirmed_at": { + "name": "confirmed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "signup_confirmations_form_id_email_idx": { + "name": "signup_confirmations_form_id_email_idx", + "columns": [ + { + "expression": "form_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "signup_confirmations_form_id_signup_forms_id_fk": { + "name": "signup_confirmations_form_id_signup_forms_id_fk", + "tableFrom": "signup_confirmations", + "tableTo": "signup_forms", + "columnsFrom": [ + "form_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "signup_confirmations_token_unique": { + "name": "signup_confirmations_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.signup_forms": { + "name": "signup_forms", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "form_id": { + "name": "form_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "double_opt_in": { + "name": "double_opt_in", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "redirect_url": { + "name": "redirect_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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()" + } + }, + "indexes": { + "signup_forms_team_id_idx": { + "name": "signup_forms_team_id_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "signup_forms_team_id_teams_id_fk": { + "name": "signup_forms_team_id_teams_id_fk", + "tableFrom": "signup_forms", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "signup_forms_form_id_unique": { + "name": "signup_forms_form_id_unique", + "nullsNotDistinct": false, + "columns": [ + "form_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "signup_forms_form_id_check": { + "name": "signup_forms_form_id_check", + "value": "\"signup_forms\".\"form_id\" ~ '^frm_'" + } + }, + "isRLSEnabled": false + }, + "public.team_api_keys": { + "name": "team_api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_api_key_id": { + "name": "team_api_key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_prefix": { + "name": "key_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_type": { + "name": "created_by_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "created_by_id": { + "name": "created_by_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "team_api_keys_team_id_idx": { + "name": "team_api_keys_team_id_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_api_keys_team_id_teams_id_fk": { + "name": "team_api_keys_team_id_teams_id_fk", + "tableFrom": "team_api_keys", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "team_api_keys_team_api_key_id_unique": { + "name": "team_api_keys_team_api_key_id_unique", + "nullsNotDistinct": false, + "columns": [ + "team_api_key_id" + ] + }, + "team_api_keys_key_hash_unique": { + "name": "team_api_keys_key_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "key_hash" + ] + } + }, + "policies": {}, + "checkConstraints": { + "team_api_keys_public_id_check": { + "name": "team_api_keys_public_id_check", + "value": "\"team_api_keys\".\"team_api_key_id\" ~ '^tak_'" + }, + "team_api_keys_created_by_type_check": { + "name": "team_api_keys_created_by_type_check", + "value": "\"team_api_keys\".\"created_by_type\" IN ('user', 'organization_key', 'system')" + } + }, + "isRLSEnabled": false + }, + "public.team_delivery_settings": { + "name": "team_delivery_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_esp_enabled": { + "name": "team_esp_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "team_can_change_default": { + "name": "team_can_change_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "default_source": { + "name": "default_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_team_esp_config_id": { + "name": "default_team_esp_config_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "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()" + } + }, + "indexes": {}, + "foreignKeys": { + "team_delivery_settings_team_id_teams_id_fk": { + "name": "team_delivery_settings_team_id_teams_id_fk", + "tableFrom": "team_delivery_settings", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_delivery_settings_default_team_esp_fk": { + "name": "team_delivery_settings_default_team_esp_fk", + "tableFrom": "team_delivery_settings", + "tableTo": "esp_configs", + "columnsFrom": [ + "default_team_esp_config_id", + "team_id" + ], + "columnsTo": [ + "id", + "team_id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "team_delivery_settings_team_id_unique": { + "name": "team_delivery_settings_team_id_unique", + "nullsNotDistinct": false, + "columns": [ + "team_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "team_delivery_settings_default_source_check": { + "name": "team_delivery_settings_default_source_check", + "value": "\"team_delivery_settings\".\"default_source\" IS NULL OR \"team_delivery_settings\".\"default_source\" IN ('organization', 'team')" + } + }, + "isRLSEnabled": false + }, + "public.team_members": { + "name": "team_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "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()" + } + }, + "indexes": { + "team_members_team_id_user_id_idx": { + "name": "team_members_team_id_user_id_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_members_team_id_teams_id_fk": { + "name": "team_members_team_id_teams_id_fk", + "tableFrom": "team_members", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_members_user_id_user_id_fk": { + "name": "team_members_user_id_user_id_fk", + "tableFrom": "team_members", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "team_members_role_check": { + "name": "team_members_role_check", + "value": "\"team_members\".\"role\" IN ('admin', 'member')" + } + }, + "isRLSEnabled": false + }, + "public.team_sending_controls": { + "name": "team_sending_controls", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'normal'" + }, + "reason_code": { + "name": "reason_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'automatic'" + }, + "entered_at": { + "name": "entered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "evaluated_at": { + "name": "evaluated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "minimum_hold_until": { + "name": "minimum_hold_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "operator_user_id": { + "name": "operator_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "operator_reason": { + "name": "operator_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "overridden_at": { + "name": "overridden_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "clean_evaluation_days": { + "name": "clean_evaluation_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "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()" + } + }, + "indexes": {}, + "foreignKeys": { + "team_sending_controls_team_id_teams_id_fk": { + "name": "team_sending_controls_team_id_teams_id_fk", + "tableFrom": "team_sending_controls", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "team_sending_controls_operator_user_id_user_id_fk": { + "name": "team_sending_controls_operator_user_id_user_id_fk", + "tableFrom": "team_sending_controls", + "tableTo": "user", + "columnsFrom": [ + "operator_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "team_sending_controls_team_id_unique": { + "name": "team_sending_controls_team_id_unique", + "nullsNotDistinct": false, + "columns": [ + "team_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "team_sending_controls_status_check": { + "name": "team_sending_controls_status_check", + "value": "\"team_sending_controls\".\"status\" IN ('normal', 'warned', 'marketing_paused', 'all_paused')" + }, + "team_sending_controls_source_check": { + "name": "team_sending_controls_source_check", + "value": "\"team_sending_controls\".\"source\" IN ('automatic', 'operator', 'abuse')" + }, + "team_sending_controls_clean_days_check": { + "name": "team_sending_controls_clean_days_check", + "value": "\"team_sending_controls\".\"clean_evaluation_days\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.teams": { + "name": "teams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provisioning_request_hash": { + "name": "provisioning_request_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "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()" + } + }, + "indexes": { + "teams_organization_id_external_id_idx": { + "name": "teams_organization_id_external_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"teams\".\"external_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "teams_organization_id_organizations_id_fk": { + "name": "teams_organization_id_organizations_id_fk", + "tableFrom": "teams", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "teams_team_id_unique": { + "name": "teams_team_id_unique", + "nullsNotDistinct": false, + "columns": [ + "team_id" + ] + }, + "teams_id_organization_id_unique": { + "name": "teams_id_organization_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "teams_team_id_check": { + "name": "teams_team_id_check", + "value": "\"teams\".\"team_id\" ~ '^team_'" + }, + "teams_status_check": { + "name": "teams_status_check", + "value": "\"teams\".\"status\" IN ('active', 'sending_suspended', 'archived')" + } + }, + "isRLSEnabled": false + }, + "public.transactional_emails": { + "name": "transactional_emails", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "txe_id": { + "name": "txe_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delivery_source_type": { + "name": "delivery_source_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outbox_id": { + "name": "outbox_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "esp_grant_id": { + "name": "esp_grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "to_email": { + "name": "to_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_email": { + "name": "from_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reply_to": { + "name": "reply_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "html": { + "name": "html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "headers": { + "name": "headers", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "contact_id": { + "name": "contact_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "track_opens": { + "name": "track_opens", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "track_clicks": { + "name": "track_clicks", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "open_count": { + "name": "open_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "click_count": { + "name": "click_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "transactional_emails_team_id_idempotency_key_idx": { + "name": "transactional_emails_team_id_idempotency_key_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"transactional_emails\".\"idempotency_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "transactional_emails_team_id_created_at_idx": { + "name": "transactional_emails_team_id_created_at_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "transactional_emails_team_id_status_idx": { + "name": "transactional_emails_team_id_status_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "transactional_emails_team_id_teams_id_fk": { + "name": "transactional_emails_team_id_teams_id_fk", + "tableFrom": "transactional_emails", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transactional_emails_outbox_id_esp_configs_id_fk": { + "name": "transactional_emails_outbox_id_esp_configs_id_fk", + "tableFrom": "transactional_emails", + "tableTo": "esp_configs", + "columnsFrom": [ + "outbox_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "transactional_emails_esp_grant_id_esp_config_team_grants_id_fk": { + "name": "transactional_emails_esp_grant_id_esp_config_team_grants_id_fk", + "tableFrom": "transactional_emails", + "tableTo": "esp_config_team_grants", + "columnsFrom": [ + "esp_grant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "transactional_emails_contact_id_contacts_id_fk": { + "name": "transactional_emails_contact_id_contacts_id_fk", + "tableFrom": "transactional_emails", + "tableTo": "contacts", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "transactional_emails_txe_id_unique": { + "name": "transactional_emails_txe_id_unique", + "nullsNotDistinct": false, + "columns": [ + "txe_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "transactional_emails_txe_id_check": { + "name": "transactional_emails_txe_id_check", + "value": "\"transactional_emails\".\"txe_id\" ~ '^txe_'" + }, + "transactional_emails_delivery_pin_check": { + "name": "transactional_emails_delivery_pin_check", + "value": "(\n \"transactional_emails\".\"delivery_source_type\" = 'team'\n AND \"transactional_emails\".\"outbox_id\" IS NOT NULL\n AND \"transactional_emails\".\"esp_grant_id\" IS NULL\n ) OR (\n \"transactional_emails\".\"delivery_source_type\" = 'organization'\n AND \"transactional_emails\".\"outbox_id\" IS NOT NULL\n AND \"transactional_emails\".\"esp_grant_id\" IS NOT NULL\n )" + } + }, + "isRLSEnabled": false + }, + "public.trial_claims": { + "name": "trial_claims", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verified_email_fingerprint": { + "name": "verified_email_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fingerprint_key_version": { + "name": "fingerprint_key_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trial_key": { + "name": "trial_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkout_attempt_id": { + "name": "checkout_attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'reserved'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "redeemed_at": { + "name": "redeemed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "trial_claims_user_trial_uidx": { + "name": "trial_claims_user_trial_uidx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "trial_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"trial_claims\".\"status\" <> 'released'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "trial_claims_email_trial_uidx": { + "name": "trial_claims_email_trial_uidx", + "columns": [ + { + "expression": "verified_email_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "trial_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"trial_claims\".\"status\" <> 'released'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "trial_claims_checkout_attempt_idx": { + "name": "trial_claims_checkout_attempt_idx", + "columns": [ + { + "expression": "checkout_attempt_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "trial_claims_user_id_user_id_fk": { + "name": "trial_claims_user_id_user_id_fk", + "tableFrom": "trial_claims", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "trial_claims_organization_id_organizations_id_fk": { + "name": "trial_claims_organization_id_organizations_id_fk", + "tableFrom": "trial_claims", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "trial_claims_status_check": { + "name": "trial_claims_status_check", + "value": "\"trial_claims\".\"status\" IN ('reserved', 'redeemed', 'released')" + } + }, + "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 + }, + "default_organization_id": { + "name": "default_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "user_default_organization_id_organizations_id_fk": { + "name": "user_default_organization_id_organizations_id_fk", + "tableFrom": "user", + "tableTo": "organizations", + "columnsFrom": [ + "default_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "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 with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "auth_verification_identifier_idx": { + "name": "auth_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.billing_catalog_revision_items": { + "name": "billing_catalog_revision_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "catalog_revision_id": { + "name": "catalog_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "offer_key": { + "name": "offer_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "billing_price_entry_id": { + "name": "billing_price_entry_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "billing_catalog_revision_items_revision_key_uidx": { + "name": "billing_catalog_revision_items_revision_key_uidx", + "columns": [ + { + "expression": "catalog_revision_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "offer_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_catalog_revision_items_revision_price_uidx": { + "name": "billing_catalog_revision_items_revision_price_uidx", + "columns": [ + { + "expression": "catalog_revision_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_price_entry_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_catalog_revision_items_catalog_revision_id_billing_catalog_revisions_id_fk": { + "name": "billing_catalog_revision_items_catalog_revision_id_billing_catalog_revisions_id_fk", + "tableFrom": "billing_catalog_revision_items", + "tableTo": "billing_catalog_revisions", + "columnsFrom": [ + "catalog_revision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "billing_catalog_revision_items_billing_price_entry_id_billing_price_entries_id_fk": { + "name": "billing_catalog_revision_items_billing_price_entry_id_billing_price_entries_id_fk", + "tableFrom": "billing_catalog_revision_items", + "tableTo": "billing_price_entries", + "columnsFrom": [ + "billing_price_entry_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_catalog_revisions": { + "name": "billing_catalog_revisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "checkout_provider": { + "name": "checkout_provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending_verification'" + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "activated_at": { + "name": "activated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "retired_at": { + "name": "retired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "billing_catalog_revisions_active_provider_uidx": { + "name": "billing_catalog_revisions_active_provider_uidx", + "columns": [ + { + "expression": "checkout_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"billing_catalog_revisions\".\"status\" = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "billing_catalog_revisions_revision_unique": { + "name": "billing_catalog_revisions_revision_unique", + "nullsNotDistinct": false, + "columns": [ + "revision" + ] + } + }, + "policies": {}, + "checkConstraints": { + "billing_catalog_revisions_status_check": { + "name": "billing_catalog_revisions_status_check", + "value": "\"billing_catalog_revisions\".\"status\" IN ('pending_verification', 'active', 'retired', 'invalid', 'abandoned')" + }, + "billing_catalog_revisions_revision_check": { + "name": "billing_catalog_revisions_revision_check", + "value": "\"billing_catalog_revisions\".\"revision\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.billing_checkout_attempts": { + "name": "billing_checkout_attempts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "attempt_id": { + "name": "attempt_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "billable_entity_id": { + "name": "billable_entity_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "payer_id": { + "name": "payer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payer_email": { + "name": "payer_email", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "return_url": { + "name": "return_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "catalog_revision": { + "name": "catalog_revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "offer_key": { + "name": "offer_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_plan": { + "name": "requested_plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_interval": { + "name": "requested_interval", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "billing_price_entry_id": { + "name": "billing_price_entry_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "quoted_amount_minor": { + "name": "quoted_amount_minor", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "quoted_currency": { + "name": "quoted_currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "billing_customer_id": { + "name": "billing_customer_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider_checkout_session_id": { + "name": "provider_checkout_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "checkout_url_encrypted": { + "name": "checkout_url_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'creating'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "billing_checkout_attempts_provider_session_uidx": { + "name": "billing_checkout_attempts_provider_session_uidx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_checkout_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"billing_checkout_attempts\".\"provider_checkout_session_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_checkout_attempts_idempotency_uidx": { + "name": "billing_checkout_attempts_idempotency_uidx", + "columns": [ + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_checkout_attempts_entity_nonterminal_uidx": { + "name": "billing_checkout_attempts_entity_nonterminal_uidx", + "columns": [ + { + "expression": "billable_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"billing_checkout_attempts\".\"status\" IN ('creating', 'open')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_checkout_attempts_billable_entity_id_organizations_id_fk": { + "name": "billing_checkout_attempts_billable_entity_id_organizations_id_fk", + "tableFrom": "billing_checkout_attempts", + "tableTo": "organizations", + "columnsFrom": [ + "billable_entity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "billing_checkout_attempts_payer_id_user_id_fk": { + "name": "billing_checkout_attempts_payer_id_user_id_fk", + "tableFrom": "billing_checkout_attempts", + "tableTo": "user", + "columnsFrom": [ + "payer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "billing_checkout_attempts_billing_price_entry_id_billing_price_entries_id_fk": { + "name": "billing_checkout_attempts_billing_price_entry_id_billing_price_entries_id_fk", + "tableFrom": "billing_checkout_attempts", + "tableTo": "billing_price_entries", + "columnsFrom": [ + "billing_price_entry_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "billing_checkout_attempts_billing_customer_id_billing_provider_customers_id_fk": { + "name": "billing_checkout_attempts_billing_customer_id_billing_provider_customers_id_fk", + "tableFrom": "billing_checkout_attempts", + "tableTo": "billing_provider_customers", + "columnsFrom": [ + "billing_customer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "billing_checkout_attempts_attempt_id_unique": { + "name": "billing_checkout_attempts_attempt_id_unique", + "nullsNotDistinct": false, + "columns": [ + "attempt_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "billing_checkout_attempts_status_check": { + "name": "billing_checkout_attempts_status_check", + "value": "\"billing_checkout_attempts\".\"status\" IN ('creating', 'open', 'completed', 'expired', 'abandoned', 'conflicted')" + }, + "billing_checkout_attempts_amount_check": { + "name": "billing_checkout_attempts_amount_check", + "value": "\"billing_checkout_attempts\".\"quoted_amount_minor\" > 0" + }, + "billing_checkout_attempts_plan_check": { + "name": "billing_checkout_attempts_plan_check", + "value": "\"billing_checkout_attempts\".\"requested_plan\" IN ('starter', 'pro', 'business')" + }, + "billing_checkout_attempts_interval_check": { + "name": "billing_checkout_attempts_interval_check", + "value": "\"billing_checkout_attempts\".\"requested_interval\" IN ('month', 'year')" + } + }, + "isRLSEnabled": false + }, + "public.billing_plan_change_attempts": { + "name": "billing_plan_change_attempts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "change_id": { + "name": "change_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "billable_entity_id": { + "name": "billable_entity_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "subscription_id": { + "name": "subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payer_id": { + "name": "payer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_catalog_revision": { + "name": "current_catalog_revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "current_billing_price_entry_id": { + "name": "current_billing_price_entry_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "current_plan": { + "name": "current_plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_interval": { + "name": "current_interval", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_catalog_revision": { + "name": "target_catalog_revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_billing_price_entry_id": { + "name": "target_billing_price_entry_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_plan": { + "name": "target_plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_interval": { + "name": "target_interval", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_offer_key": { + "name": "target_offer_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "effective_at": { + "name": "effective_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "proration_mode": { + "name": "proration_mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_payment_id": { + "name": "provider_payment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payment_url_encrypted": { + "name": "payment_url_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'creating'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "billing_plan_change_attempts_idempotency_uidx": { + "name": "billing_plan_change_attempts_idempotency_uidx", + "columns": [ + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_plan_change_attempts_entity_nonterminal_uidx": { + "name": "billing_plan_change_attempts_entity_nonterminal_uidx", + "columns": [ + { + "expression": "billable_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"billing_plan_change_attempts\".\"status\" IN ('creating', 'pending')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_plan_change_attempts_billable_entity_id_organizations_id_fk": { + "name": "billing_plan_change_attempts_billable_entity_id_organizations_id_fk", + "tableFrom": "billing_plan_change_attempts", + "tableTo": "organizations", + "columnsFrom": [ + "billable_entity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "billing_plan_change_attempts_subscription_id_billing_subscriptions_id_fk": { + "name": "billing_plan_change_attempts_subscription_id_billing_subscriptions_id_fk", + "tableFrom": "billing_plan_change_attempts", + "tableTo": "billing_subscriptions", + "columnsFrom": [ + "subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "billing_plan_change_attempts_payer_id_user_id_fk": { + "name": "billing_plan_change_attempts_payer_id_user_id_fk", + "tableFrom": "billing_plan_change_attempts", + "tableTo": "user", + "columnsFrom": [ + "payer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "billing_plan_change_attempts_current_billing_price_entry_id_billing_price_entries_id_fk": { + "name": "billing_plan_change_attempts_current_billing_price_entry_id_billing_price_entries_id_fk", + "tableFrom": "billing_plan_change_attempts", + "tableTo": "billing_price_entries", + "columnsFrom": [ + "current_billing_price_entry_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "billing_plan_change_attempts_target_billing_price_entry_id_billing_price_entries_id_fk": { + "name": "billing_plan_change_attempts_target_billing_price_entry_id_billing_price_entries_id_fk", + "tableFrom": "billing_plan_change_attempts", + "tableTo": "billing_price_entries", + "columnsFrom": [ + "target_billing_price_entry_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "billing_plan_change_attempts_change_id_unique": { + "name": "billing_plan_change_attempts_change_id_unique", + "nullsNotDistinct": false, + "columns": [ + "change_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "billing_plan_change_attempts_status_check": { + "name": "billing_plan_change_attempts_status_check", + "value": "\"billing_plan_change_attempts\".\"status\" IN ('creating', 'pending', 'succeeded', 'failed', 'conflicted')" + }, + "billing_plan_change_attempts_effective_at_check": { + "name": "billing_plan_change_attempts_effective_at_check", + "value": "\"billing_plan_change_attempts\".\"effective_at\" IN ('immediately', 'next_billing_date')" + }, + "billing_plan_change_attempts_proration_mode_check": { + "name": "billing_plan_change_attempts_proration_mode_check", + "value": "\"billing_plan_change_attempts\".\"proration_mode\" IN ('prorated_immediately', 'do_not_bill')" + }, + "billing_plan_change_attempts_current_plan_check": { + "name": "billing_plan_change_attempts_current_plan_check", + "value": "\"billing_plan_change_attempts\".\"current_plan\" IN ('starter', 'pro', 'business')" + }, + "billing_plan_change_attempts_target_plan_check": { + "name": "billing_plan_change_attempts_target_plan_check", + "value": "\"billing_plan_change_attempts\".\"target_plan\" IN ('starter', 'pro', 'business')" + } + }, + "isRLSEnabled": false + }, + "public.billing_plan_states": { + "name": "billing_plan_states", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "billable_entity_id": { + "name": "billable_entity_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "active_subscription_id": { + "name": "active_subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "projection_version": { + "name": "projection_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "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()" + } + }, + "indexes": {}, + "foreignKeys": { + "billing_plan_states_billable_entity_id_organizations_id_fk": { + "name": "billing_plan_states_billable_entity_id_organizations_id_fk", + "tableFrom": "billing_plan_states", + "tableTo": "organizations", + "columnsFrom": [ + "billable_entity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "billing_plan_states_active_subscription_id_billing_subscriptions_id_fk": { + "name": "billing_plan_states_active_subscription_id_billing_subscriptions_id_fk", + "tableFrom": "billing_plan_states", + "tableTo": "billing_subscriptions", + "columnsFrom": [ + "active_subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "billing_plan_states_billable_entity_id_unique": { + "name": "billing_plan_states_billable_entity_id_unique", + "nullsNotDistinct": false, + "columns": [ + "billable_entity_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_price_entries": { + "name": "billing_price_entries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "offer_key": { + "name": "offer_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "billing_interval": { + "name": "billing_interval", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_minor": { + "name": "amount_minor", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "provider_trial_days": { + "name": "provider_trial_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_product_id": { + "name": "provider_product_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "billing_price_entries_provider_product_uidx": { + "name": "billing_price_entries_provider_product_uidx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_product_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_price_entries_offer_key_idx": { + "name": "billing_price_entries_offer_key_idx", + "columns": [ + { + "expression": "offer_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "billing_price_entries_amount_check": { + "name": "billing_price_entries_amount_check", + "value": "\"billing_price_entries\".\"amount_minor\" > 0" + }, + "billing_price_entries_trial_days_check": { + "name": "billing_price_entries_trial_days_check", + "value": "\"billing_price_entries\".\"provider_trial_days\" >= 0" + }, + "billing_price_entries_currency_check": { + "name": "billing_price_entries_currency_check", + "value": "\"billing_price_entries\".\"currency\" ~ '^[A-Z]{3}$'" + }, + "billing_price_entries_plan_check": { + "name": "billing_price_entries_plan_check", + "value": "\"billing_price_entries\".\"plan\" IN ('starter', 'pro', 'business')" + }, + "billing_price_entries_interval_check": { + "name": "billing_price_entries_interval_check", + "value": "\"billing_price_entries\".\"billing_interval\" IN ('month', 'year')" + } + }, + "isRLSEnabled": false + }, + "public.billing_provider_customers": { + "name": "billing_provider_customers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payer_id": { + "name": "payer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payer_email": { + "name": "payer_email", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "provider_customer_id": { + "name": "provider_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'creating'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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()" + } + }, + "indexes": { + "billing_provider_customers_provider_payer_uidx": { + "name": "billing_provider_customers_provider_payer_uidx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "payer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_provider_customers_provider_customer_uidx": { + "name": "billing_provider_customers_provider_customer_uidx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"billing_provider_customers\".\"provider_customer_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_provider_customers_idempotency_uidx": { + "name": "billing_provider_customers_idempotency_uidx", + "columns": [ + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_provider_customers_payer_id_user_id_fk": { + "name": "billing_provider_customers_payer_id_user_id_fk", + "tableFrom": "billing_provider_customers", + "tableTo": "user", + "columnsFrom": [ + "payer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "billing_provider_customers_status_check": { + "name": "billing_provider_customers_status_check", + "value": "\"billing_provider_customers\".\"status\" IN ('creating', 'active', 'conflicted')" + } + }, + "isRLSEnabled": false + }, + "public.billing_reconciliation_jobs": { + "name": "billing_reconciliation_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "checkout_attempt_id": { + "name": "checkout_attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "plan_change_attempt_id": { + "name": "plan_change_attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "subscription_id": { + "name": "subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider_customer_id": { + "name": "provider_customer_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'reconcile'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "available_at": { + "name": "available_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "worker_id": { + "name": "worker_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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()" + } + }, + "indexes": { + "billing_reconciliation_jobs_live_checkout_uidx": { + "name": "billing_reconciliation_jobs_live_checkout_uidx", + "columns": [ + { + "expression": "checkout_attempt_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"billing_reconciliation_jobs\".\"checkout_attempt_id\" IS NOT NULL AND \"billing_reconciliation_jobs\".\"status\" IN ('pending', 'processing', 'failed')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_reconciliation_jobs_live_plan_change_uidx": { + "name": "billing_reconciliation_jobs_live_plan_change_uidx", + "columns": [ + { + "expression": "plan_change_attempt_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"billing_reconciliation_jobs\".\"plan_change_attempt_id\" IS NOT NULL AND \"billing_reconciliation_jobs\".\"status\" IN ('pending', 'processing', 'failed')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_reconciliation_jobs_live_subscription_uidx": { + "name": "billing_reconciliation_jobs_live_subscription_uidx", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"billing_reconciliation_jobs\".\"subscription_id\" IS NOT NULL AND \"billing_reconciliation_jobs\".\"status\" IN ('pending', 'processing', 'failed')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_reconciliation_jobs_live_customer_uidx": { + "name": "billing_reconciliation_jobs_live_customer_uidx", + "columns": [ + { + "expression": "provider_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"billing_reconciliation_jobs\".\"provider_customer_id\" IS NOT NULL AND \"billing_reconciliation_jobs\".\"status\" IN ('pending', 'processing', 'failed')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_reconciliation_jobs_checkout_attempt_id_billing_checkout_attempts_id_fk": { + "name": "billing_reconciliation_jobs_checkout_attempt_id_billing_checkout_attempts_id_fk", + "tableFrom": "billing_reconciliation_jobs", + "tableTo": "billing_checkout_attempts", + "columnsFrom": [ + "checkout_attempt_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "billing_reconciliation_jobs_plan_change_attempt_id_billing_plan_change_attempts_id_fk": { + "name": "billing_reconciliation_jobs_plan_change_attempt_id_billing_plan_change_attempts_id_fk", + "tableFrom": "billing_reconciliation_jobs", + "tableTo": "billing_plan_change_attempts", + "columnsFrom": [ + "plan_change_attempt_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "billing_reconciliation_jobs_subscription_id_billing_subscriptions_id_fk": { + "name": "billing_reconciliation_jobs_subscription_id_billing_subscriptions_id_fk", + "tableFrom": "billing_reconciliation_jobs", + "tableTo": "billing_subscriptions", + "columnsFrom": [ + "subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "billing_reconciliation_jobs_provider_customer_id_billing_provider_customers_id_fk": { + "name": "billing_reconciliation_jobs_provider_customer_id_billing_provider_customers_id_fk", + "tableFrom": "billing_reconciliation_jobs", + "tableTo": "billing_provider_customers", + "columnsFrom": [ + "provider_customer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "billing_reconciliation_jobs_exactly_one_subject": { + "name": "billing_reconciliation_jobs_exactly_one_subject", + "value": "((CASE WHEN \"billing_reconciliation_jobs\".\"checkout_attempt_id\" IS NOT NULL THEN 1 ELSE 0 END) + (CASE WHEN \"billing_reconciliation_jobs\".\"plan_change_attempt_id\" IS NOT NULL THEN 1 ELSE 0 END) + (CASE WHEN \"billing_reconciliation_jobs\".\"subscription_id\" IS NOT NULL THEN 1 ELSE 0 END) + (CASE WHEN \"billing_reconciliation_jobs\".\"provider_customer_id\" IS NOT NULL THEN 1 ELSE 0 END)) = 1" + }, + "billing_reconciliation_jobs_status_check": { + "name": "billing_reconciliation_jobs_status_check", + "value": "\"billing_reconciliation_jobs\".\"status\" IN ('pending', 'processing', 'failed', 'completed', 'quarantined')" + }, + "billing_reconciliation_jobs_operation_check": { + "name": "billing_reconciliation_jobs_operation_check", + "value": "\"billing_reconciliation_jobs\".\"operation\" = 'reconcile' OR (\"billing_reconciliation_jobs\".\"operation\" = 'cancellation' AND \"billing_reconciliation_jobs\".\"subscription_id\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.billing_subscriptions": { + "name": "billing_subscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "billable_entity_id": { + "name": "billable_entity_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "billing_customer_id": { + "name": "billing_customer_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "payer_id": { + "name": "payer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "origin_checkout_attempt_id": { + "name": "origin_checkout_attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_subscription_id": { + "name": "provider_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_product_id": { + "name": "provider_product_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "billing_price_entry_id": { + "name": "billing_price_entry_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "catalog_revision": { + "name": "catalog_revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "offer_key": { + "name": "offer_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "billing_interval": { + "name": "billing_interval", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "current_period_starts_at": { + "name": "current_period_starts_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "current_period_ends_at": { + "name": "current_period_ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "paid_through_at": { + "name": "paid_through_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "trial_ends_at": { + "name": "trial_ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_entitlement_source": { + "name": "is_entitlement_source", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "provider_occurred_at": { + "name": "provider_occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "provider_version": { + "name": "provider_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_observed_at": { + "name": "last_observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_reconciled_at": { + "name": "last_reconciled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "billing_subscriptions_provider_subscription_uidx": { + "name": "billing_subscriptions_provider_subscription_uidx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_subscriptions_entity_source_uidx": { + "name": "billing_subscriptions_entity_source_uidx", + "columns": [ + { + "expression": "billable_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"billing_subscriptions\".\"is_entitlement_source\" = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_subscriptions_billable_entity_id_organizations_id_fk": { + "name": "billing_subscriptions_billable_entity_id_organizations_id_fk", + "tableFrom": "billing_subscriptions", + "tableTo": "organizations", + "columnsFrom": [ + "billable_entity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "billing_subscriptions_billing_customer_id_billing_provider_customers_id_fk": { + "name": "billing_subscriptions_billing_customer_id_billing_provider_customers_id_fk", + "tableFrom": "billing_subscriptions", + "tableTo": "billing_provider_customers", + "columnsFrom": [ + "billing_customer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "billing_subscriptions_payer_id_user_id_fk": { + "name": "billing_subscriptions_payer_id_user_id_fk", + "tableFrom": "billing_subscriptions", + "tableTo": "user", + "columnsFrom": [ + "payer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "billing_subscriptions_origin_checkout_attempt_id_billing_checkout_attempts_id_fk": { + "name": "billing_subscriptions_origin_checkout_attempt_id_billing_checkout_attempts_id_fk", + "tableFrom": "billing_subscriptions", + "tableTo": "billing_checkout_attempts", + "columnsFrom": [ + "origin_checkout_attempt_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "billing_subscriptions_billing_price_entry_id_billing_price_entries_id_fk": { + "name": "billing_subscriptions_billing_price_entry_id_billing_price_entries_id_fk", + "tableFrom": "billing_subscriptions", + "tableTo": "billing_price_entries", + "columnsFrom": [ + "billing_price_entry_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "billing_subscriptions_status_check": { + "name": "billing_subscriptions_status_check", + "value": "\"billing_subscriptions\".\"status\" IN ('pending', 'trialing', 'active', 'past_due', 'cancelled', 'expired')" + }, + "billing_subscriptions_plan_check": { + "name": "billing_subscriptions_plan_check", + "value": "\"billing_subscriptions\".\"plan\" IN ('starter', 'pro', 'business')" + }, + "billing_subscriptions_interval_check": { + "name": "billing_subscriptions_interval_check", + "value": "\"billing_subscriptions\".\"billing_interval\" IN ('month', 'year')" + } + }, + "isRLSEnabled": false + }, + "public.billing_webhook_events": { + "name": "billing_webhook_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_event_id": { + "name": "provider_event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "subscription_id": { + "name": "subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "checkout_attempt_id": { + "name": "checkout_attempt_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload_encrypted": { + "name": "payload_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload_key_version": { + "name": "payload_key_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verified_key_version": { + "name": "verified_key_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_attempts": { + "name": "processing_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "available_at": { + "name": "available_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "worker_id": { + "name": "worker_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "billing_webhook_events_provider_event_uidx": { + "name": "billing_webhook_events_provider_event_uidx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_webhook_events_queue_idx": { + "name": "billing_webhook_events_queue_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "billing_webhook_events_status_check": { + "name": "billing_webhook_events_status_check", + "value": "\"billing_webhook_events\".\"status\" IN ('pending', 'processing', 'processed', 'ignored', 'quarantined', 'failed')" + } + }, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/api/drizzle/meta/_journal.json b/apps/api/drizzle/meta/_journal.json index 6bb7cd7..cd5eebe 100644 --- a/apps/api/drizzle/meta/_journal.json +++ b/apps/api/drizzle/meta/_journal.json @@ -197,6 +197,13 @@ "when": 1793160384309, "tag": "0027_custom_events", "breakpoints": true + }, + { + "idx": 28, + "version": "7", + "when": 1793274088220, + "tag": "0028_broadcast_archive", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/api/src/config/strings.ts b/apps/api/src/config/strings.ts index c6cd97f..fc1e5f8 100644 --- a/apps/api/src/config/strings.ts +++ b/apps/api/src/config/strings.ts @@ -10,6 +10,7 @@ export const responses = { broadcast_no_recipients: "This broadcast has no matching recipients. Add contacts or widen the audience filter before sending.", mail_already_sent: "This broadcast has already been sent", + broadcast_archived: "This broadcast is archived. Unarchive it before resuming it", cannot_delete_last_email: "Cannot delete the last email in a sequence", mandatory_tags_missing: "Marketing email content must contain exactly one final managed footer", diff --git a/apps/api/src/db/schema-core.ts b/apps/api/src/db/schema-core.ts index 194cade..e306c28 100644 --- a/apps/api/src/db/schema-core.ts +++ b/apps/api/src/db/schema-core.ts @@ -1383,6 +1383,10 @@ export const sequences = pgTable( webNoindex: boolean("web_noindex").notNull().default(false), /** Where the issue really lives when a platform hosts it. */ webCanonicalUrl: text("web_canonical_url"), + /** When a sent broadcast was archived (#147): hidden from the default + * list and the public web archive, with its report and delivery + * history kept. `null` when not archived. */ + archivedAt: timestamp("archived_at", { withTimezone: true }), createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(), updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow(), }, diff --git a/apps/api/src/mcp/policy.ts b/apps/api/src/mcp/policy.ts index 3a40bd1..eaa1286 100644 --- a/apps/api/src/mcp/policy.ts +++ b/apps/api/src/mcp/policy.ts @@ -96,6 +96,8 @@ const toolPolicies = { delete_sequence_email: MCP_SCOPES.sequencesWrite, delete_sequence: MCP_SCOPES.sequencesWrite, preview_sequence_deletion: MCP_SCOPES.sequencesRead, + archive_broadcast: MCP_SCOPES.sequencesWrite, + unarchive_broadcast: MCP_SCOPES.sequencesWrite, start_sequence: MCP_SCOPES.sequencesWrite, pause_sequence: MCP_SCOPES.sequencesWrite, diff --git a/apps/api/src/mcp/server.test.ts b/apps/api/src/mcp/server.test.ts index 4d8c86c..1a9f6b6 100644 --- a/apps/api/src/mcp/server.test.ts +++ b/apps/api/src/mcp/server.test.ts @@ -67,11 +67,11 @@ describe("MCP server", () => { expect(client.getProtocolEra()).toBe("modern"); const result = await client.listTools(); - expect(result.tools).toHaveLength(111); + expect(result.tools).toHaveLength(113); expect(result.tools.map((tool) => tool.name).sort()).toEqual( Object.keys(listMcpToolPolicies()).sort(), ); - expect(new Set(result.tools.map((tool) => tool.name)).size).toBe(111); + expect(new Set(result.tools.map((tool) => tool.name)).size).toBe(113); expect(result.ttlMs).toBe(300_000); expect(result.cacheScope).toBe("private"); for (const tool of result.tools) { diff --git a/apps/api/src/mcp/tools/sequences.ts b/apps/api/src/mcp/tools/sequences.ts index a56ce03..2a8e88c 100644 --- a/apps/api/src/mcp/tools/sequences.ts +++ b/apps/api/src/mcp/tools/sequences.ts @@ -1,4 +1,5 @@ import { + archivedFilters, contactFilterSchema, emailContentInputSchema, sequenceDeletionPreviewSchema, @@ -9,7 +10,10 @@ import { z } from "zod"; import { broadcastPacing } from "../../abuse/pacing-status"; import { sequenceDeliveryRates } from "../../automation/sending-health"; import { emailActionTypes, mailTypes } from "../../config/constants"; -import { sequenceDeleteBlockerMessage } from "../../sequences/delete-blockers"; +import { + broadcastArchiveErrors, + sequenceDeleteBlockerMessage, +} from "../../sequences/delete-blockers"; import type { HydratedSequence } from "../../sequences/queries"; import { addMailToSequence, @@ -26,6 +30,7 @@ import { getSubscribersCount, listSequences, pauseSequence, + setBroadcastArchived, startSequence, updateMailInSequence, updateSequence, @@ -72,9 +77,13 @@ export function registerSequenceTools(server: McpToolRegistrar): void { "list_sequences", { description: - "Returns a paginated list of broadcasts (type=broadcast, one-off) or sequences (type=sequence, multi-step, event-triggered).", + "Returns a paginated list of broadcasts (type=broadcast, one-off) or sequences (type=sequence, multi-step, event-triggered). Archived broadcasts are left out unless archived is only or include.", inputSchema: { type: z.enum(mailTypes), + archived: z + .enum(archivedFilters) + .optional() + .describe("exclude (default), only or include archived broadcasts"), offset: z.number().int().min(1).optional().describe("Page number (default: 1)"), itemsPerPage: z.number().int().min(1).max(50).optional(), }, @@ -95,8 +104,9 @@ export function registerSequenceTools(server: McpToolRegistrar): void { type: args.type, offset: args.offset, itemsPerPage: args.itemsPerPage, + archived: args.archived, }), - countSequences(teamId, args.type), + countSequences(teamId, args.type, args.archived), ]); return jsonResult({ items: items.map(toPublicSequence), @@ -404,7 +414,7 @@ export function registerSequenceTools(server: McpToolRegistrar): void { "preview_sequence_deletion", { description: - "Shows what delete_sequence would remove without changing anything: how many contacts are enrolled, how many emails it has sent, and whether it can be deleted now (blocker sequence_active means pause it first).", + "Shows what delete_sequence would remove without changing anything: how many contacts are enrolled, how many emails it has sent, and whether it can be deleted now (blocker sequence_active means pause it first; broadcast_sent means archive the broadcast with archive_broadcast instead).", inputSchema: { sequenceId: z.string() }, outputSchema: sequenceDeletionPreviewSchema, annotations: { @@ -426,7 +436,7 @@ export function registerSequenceTools(server: McpToolRegistrar): void { "delete_sequence", { description: - "Permanently deletes a broadcast or sequence with its emails and triggers. An active one must be paused first (pause_sequence). Contacts still enrolled leave it, and its delivery, open and click history (its stats) is deleted and can't be recovered; call preview_sequence_deletion first and confirm with the user. Images only it used are deleted; images in sent mail are kept.", + "Permanently deletes a broadcast or sequence with its emails and triggers. An active sequence or sending broadcast must be paused first (pause_sequence). A draft or scheduled broadcast can be deleted; a broadcast that has started sending can't (broadcast_sent): use archive_broadcast. Contacts still enrolled leave it, and its delivery, open and click history (its stats) is deleted and can't be recovered; call preview_sequence_deletion first and confirm with the user. Images only it used are deleted; images in sent mail are kept.", inputSchema: { sequenceId: z.string() }, outputSchema: successMessageSchema, annotations: { @@ -448,6 +458,42 @@ export function registerSequenceTools(server: McpToolRegistrar): void { }, ); + for (const archived of [true, false]) { + server.registerTool( + archived ? "archive_broadcast" : "unarchive_broadcast", + { + description: archived + ? "Archives a sent broadcast: it leaves the broadcasts list (list_sequences with archived=only shows it) and the public web archive, feed and sitemap. Its report, delivery history and images are kept. Drafts and scheduled broadcasts can't be archived; delete them with delete_sequence." + : "Restores an archived broadcast to the broadcasts list, and to the web archive when it is published there.", + inputSchema: { sequenceId: z.string() }, + outputSchema: sequenceSchema, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, + }, + async (args: any, extra: any) => { + const teamId = getTeamId(extra); + if (!teamId) return AUTH_ERROR; + try { + const sequence = await setBroadcastArchived({ + teamId, + sequenceId: args.sequenceId, + archived, + }); + if (!sequence) return NOT_FOUND; + return jsonResult(toPublicSequence(sequence)); + } catch (err: any) { + const message = + broadcastArchiveErrors[err.message as keyof typeof broadcastArchiveErrors]; + return errorResult(message ?? err.message); + } + }, + ); + } + server.registerTool( "start_sequence", { diff --git a/apps/api/src/mcp/tools/tools.test.ts b/apps/api/src/mcp/tools/tools.test.ts index ce40611..ae033f0 100644 --- a/apps/api/src/mcp/tools/tools.test.ts +++ b/apps/api/src/mcp/tools/tools.test.ts @@ -18,6 +18,7 @@ const mocks = { createSequence: vi.fn(), deleteSequence: vi.fn(), + setBroadcastArchived: vi.fn(), getSequenceDeletionImpact: vi.fn(), getSequenceBySequenceId: vi.fn(), getEmailSentCount: vi.fn(), @@ -150,6 +151,7 @@ mock.module("../../sequences/queries", () => ({ updateMailInSequence: mocks.updateMailInSequence, deleteMailFromSequence: mocks.deleteMailFromSequence, deleteSequence: mocks.deleteSequence, + setBroadcastArchived: mocks.setBroadcastArchived, getSequenceDeletionImpact: mocks.getSequenceDeletionImpact, startSequence: mocks.startSequence, pauseSequence: mocks.pauseSequence, @@ -530,6 +532,57 @@ describe("MCP sequence tools", () => { ).resolves.toEqual(NOT_FOUND); }); + it("explains why a sent broadcast can't be deleted", async () => { + const tools = makeToolRegistry(registerSequenceTools); + mocks.deleteSequence.mockRejectedValueOnce(new Error("broadcast_sent")); + + const result = await tools + .get("delete_sequence")! + .handler({ sequenceId: "seq-1" }, auth); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain("Archive it instead"); + }); + + it("archives and restores a broadcast of the caller's team", async () => { + const tools = makeToolRegistry(registerSequenceTools); + const archivedAt = new Date("2026-10-29T00:00:00Z"); + mocks.setBroadcastArchived + .mockResolvedValueOnce({ sequenceId: "seq-1", archivedAt, emails: [] }) + .mockResolvedValueOnce({ sequenceId: "seq-1", archivedAt: null, emails: [] }) + .mockResolvedValueOnce(null) + .mockRejectedValueOnce(new Error("broadcast_not_sent")); + + const archived = await tools + .get("archive_broadcast")! + .handler({ sequenceId: "seq-1" }, auth); + expect(archived.structuredContent).toMatchObject({ + archivedAt: archivedAt.toISOString(), + }); + expect(mocks.setBroadcastArchived).toHaveBeenLastCalledWith({ + teamId: "team-1", + sequenceId: "seq-1", + archived: true, + }); + const restored = await tools + .get("unarchive_broadcast")! + .handler({ sequenceId: "seq-1" }, auth); + expect(restored.structuredContent).toMatchObject({ archivedAt: null }); + expect(mocks.setBroadcastArchived).toHaveBeenLastCalledWith({ + teamId: "team-1", + sequenceId: "seq-1", + archived: false, + }); + await expect( + tools.get("archive_broadcast")!.handler({ sequenceId: "seq-2" }, auth), + ).resolves.toEqual(NOT_FOUND); + const draft = await tools + .get("archive_broadcast")! + .handler({ sequenceId: "seq-3" }, auth); + expect(draft.isError).toBe(true); + expect(draft.content[0].text).toContain("Only a sent broadcast can be archived"); + }); + it("previews a sequence deletion", async () => { const tools = makeToolRegistry(registerSequenceTools); const preview = { diff --git a/apps/api/src/sequences/delete-blockers.ts b/apps/api/src/sequences/delete-blockers.ts index 9bb23c7..47063c7 100644 --- a/apps/api/src/sequences/delete-blockers.ts +++ b/apps/api/src/sequences/delete-blockers.ts @@ -1,13 +1,17 @@ /** * Why a broadcast or sequence can't be deleted. Each code tells the caller * what to do instead, so the REST API, MCP and dashboard explain it the same - * way. Broadcast-only blockers (#147) belong here too. + * way. */ export const sequenceDeleteBlockerMessages = { // Contacts are moving through it (or a broadcast is sending), so deleting // it would drop them mid-flow. Pausing stops new sends first. sequence_active: "This sequence is active. Pause it before deleting it, so no contact is dropped mid-flow.", + // A broadcast that has started sending keeps its report (#147): archive + // it to hide it instead. + broadcast_sent: + "This broadcast has been sent, so deleting it would erase its report. Archive it instead: it leaves the list and the web archive, and its report is kept.", } as const; export type SequenceDeleteBlocker = keyof typeof sequenceDeleteBlockerMessages; @@ -23,9 +27,57 @@ export function sequenceDeleteBlockerMessage(code: unknown): string | undefined : undefined; } -/** The blocker for a sequence in this state, or `null` when it may be deleted. */ -export function sequenceDeleteBlocker(sequence: { +interface BlockerInput { status: string; -}): SequenceDeleteBlocker | null { + type?: string; + report?: unknown; + /** A broadcast's send time (epoch millis), `emails[0].delayInMillis`. */ + sendAt?: number | null; +} + +/** Whether a broadcast has started sending: its audience is locked. */ +export function broadcastStarted(report: unknown): boolean { + return Boolean( + (report as { broadcast?: { lockedAt?: number | null } } | null)?.broadcast + ?.lockedAt, + ); +} + +/** + * The blocker for a sequence in this state, or `null` when it may be + * deleted. Broadcasts: a draft or a scheduled one whose send time hasn't + * come can be deleted; a sending one is `sequence_active`; once it has + * started sending (sent, or paused mid-send) it is `broadcast_sent`. + */ +export function sequenceDeleteBlocker( + sequence: BlockerInput, + now = Date.now(), +): SequenceDeleteBlocker | null { + if (sequence.type === "broadcast") { + const started = broadcastStarted(sequence.report); + if (sequence.status === "active") { + const scheduled = !started && (sequence.sendAt ?? 0) > now; + return scheduled ? null : "sequence_active"; + } + return started ? "broadcast_sent" : null; + } return sequence.status === "active" ? "sequence_active" : null; } + +export const broadcastArchiveErrors = { + broadcast_not_sent: + "Only a sent broadcast can be archived. Delete a draft or scheduled broadcast instead.", + sequence_active: sequenceDeleteBlockerMessages.sequence_active, +} as const; + +/** Why a broadcast can't be archived, or `null` when it can: archive is + * for broadcasts that have sent mail and aren't sending now. */ +export function broadcastArchiveBlocker( + sequence: BlockerInput, + now = Date.now(), +): keyof typeof broadcastArchiveErrors | null { + const blocker = sequenceDeleteBlocker(sequence, now); + if (blocker === "broadcast_sent") return null; + if (blocker === "sequence_active") return "sequence_active"; + return "broadcast_not_sent"; +} diff --git a/apps/api/src/sequences/queries.test.ts b/apps/api/src/sequences/queries.test.ts index bc2dfdd..60ed53a 100644 --- a/apps/api/src/sequences/queries.test.ts +++ b/apps/api/src/sequences/queries.test.ts @@ -42,6 +42,7 @@ type TestDb = import("../test/db").TestDb; const { defaultEmailContent } = await import("./helpers"); const { addMailToSequence, + countSequences, createSequence, deleteMailFromSequence, deleteSequence, @@ -53,6 +54,7 @@ const { getSubscribersCount, listSequences, pauseSequence, + setBroadcastArchived, startSequence, updateMailInSequence, updateSequence, @@ -1004,3 +1006,211 @@ describe("deleteSequence guards and history", () => { expect(await tdb.select().from(mediaReferences)).toHaveLength(0); }); }); + +describe("broadcast delete and archive (#147)", () => { + /** A broadcast whose email shows `mediaId`, in the given state. */ + async function broadcast( + state: "draft" | "scheduled" | "sending" | "sent" | "paused_mid_send", + mediaId = "broadcast-media", + ) { + const { team, contact } = await seedTeamAndContact(tdb); + const template = await makeTemplate(team.id); + const created = await createSequence({ + teamId: team.id, + type: "broadcast", + templateId: template.templateId, + }); + await updateMailInSequence({ + teamId: team.id, + sequenceId: created.sequenceId, + emailId: created.emails[0].emailId, + content: emailWithImage(mediaId), + }); + const email = created.emails[0]; + const lockedAt = Date.now() - 60_000; + if (state === "scheduled") { + await tdb + .update(sequenceEmails) + .set({ delayInMillis: Date.now() + 86_400_000 }) + .where(eq(sequenceEmails.id, email.id)); + await tdb.insert(rules).values({ + teamId: team.id, + sequenceId: created.id, + event: EventType.DATE_OCCURRED, + eventDateInMillis: Date.now() + 86_400_000, + }); + } + const status = { + draft: "draft", + scheduled: "active", + sending: "active", + sent: "completed", + paused_mid_send: "paused", + }[state]; + const started = ["sending", "sent", "paused_mid_send"].includes(state); + await tdb + .update(sequences) + .set({ + status, + report: started ? { broadcast: { lockedAt, sentAt: null } } : {}, + }) + .where(eq(sequences.id, created.id)); + if (started) { + await tdb.insert(emailDeliveries).values({ + teamId: team.id, + sequenceId: created.id, + contactId: contact.id, + emailId: email.id, + }); + await tdb.insert(emailEvents).values({ + teamId: team.id, + sequenceId: created.id, + contactId: contact.id, + emailId: email.id, + action: EmailEventAction.OPEN, + }); + } + vi.clearAllMocks(); + return { team, sequence: created }; + } + + const mediaLitIds = async () => + (await tdb.select().from(media)).map((row) => row.mediaLitId); + + it("deletes a draft broadcast and its unused image", async () => { + const { team, sequence } = await broadcast("draft"); + expect(await mediaLitIds()).toEqual(["broadcast-media"]); + + await expect( + getSequenceDeletionImpact(team.id, sequence.sequenceId), + ).resolves.toMatchObject({ blocker: null, emailsSentCount: 0 }); + await expect( + deleteSequence({ teamId: team.id, sequenceId: sequence.sequenceId }), + ).resolves.toBe(true); + + expect(await tdb.select().from(sequences)).toHaveLength(0); + expect(deleteMedia).toHaveBeenCalledWith("broadcast-media"); + expect(await mediaLitIds()).toEqual([]); + expect(await tdb.select().from(mediaReferences)).toHaveLength(0); + }); + + it("deletes a scheduled broadcast with its schedule", async () => { + const { team, sequence } = await broadcast("scheduled"); + expect(await tdb.select().from(rules)).toHaveLength(1); + + await expect( + deleteSequence({ teamId: team.id, sequenceId: sequence.sequenceId }), + ).resolves.toBe(true); + + expect(await tdb.select().from(sequences)).toHaveLength(0); + expect(await tdb.select().from(rules)).toHaveLength(0); + expect(deleteMedia).toHaveBeenCalledWith("broadcast-media"); + }); + + it("refuses to delete or archive a broadcast while it is sending", async () => { + const { team, sequence } = await broadcast("sending"); + const ids = { teamId: team.id, sequenceId: sequence.sequenceId }; + + await expect(deleteSequence(ids)).rejects.toThrow("sequence_active"); + await expect(setBroadcastArchived({ ...ids, archived: true })).rejects.toThrow( + "sequence_active", + ); + expect(await tdb.select().from(emailDeliveries)).toHaveLength(1); + }); + + it("refuses to delete a sent broadcast and archives it with its report and images", async () => { + const { team, sequence } = await broadcast("sent"); + const ids = { teamId: team.id, sequenceId: sequence.sequenceId }; + + await expect(getSequenceDeletionImpact(team.id, ids.sequenceId)).resolves.toEqual({ + status: "completed", + blocker: "broadcast_sent", + enrolledCount: 0, + emailsSentCount: 1, + }); + await expect(deleteSequence(ids)).rejects.toThrow("broadcast_sent"); + + const archived = await setBroadcastArchived({ ...ids, archived: true }); + expect(archived?.archivedAt).toBeInstanceOf(Date); + expect(archived?.status).toBe("completed"); + expect(await tdb.select().from(emailDeliveries)).toHaveLength(1); + expect(await tdb.select().from(emailEvents)).toHaveLength(1); + expect(await getSequenceOpenRate(ids.sequenceId)).toBeGreaterThan(0); + expect(deleteMedia).not.toHaveBeenCalled(); + expect(await mediaLitIds()).toEqual(["broadcast-media"]); + expect(await tdb.select().from(mediaReferences)).toHaveLength(1); + + // Hidden from the default list, shown under "Archived". + const list = (archived: "exclude" | "only" | "include" = "exclude") => + listSequences({ teamId: team.id, type: "broadcast", archived }); + expect(await list()).toHaveLength(0); + expect((await list("only")).map((item) => item.sequenceId)).toEqual([ + ids.sequenceId, + ]); + expect(await list("include")).toHaveLength(1); + expect(await countSequences(team.id, "broadcast")).toBe(0); + expect(await countSequences(team.id, "broadcast", "only")).toBe(1); + + // Still can't be deleted once archived. + await expect(deleteSequence(ids)).rejects.toThrow("broadcast_sent"); + + const restored = await setBroadcastArchived({ ...ids, archived: false }); + expect(restored?.archivedAt).toBeNull(); + expect(await list()).toHaveLength(1); + expect(await list("only")).toHaveLength(0); + }); + + it("archives a broadcast paused mid-send, which can't resume until restored", async () => { + const { team, sequence } = await broadcast("paused_mid_send"); + const ids = { teamId: team.id, sequenceId: sequence.sequenceId }; + await expect(deleteSequence(ids)).rejects.toThrow("broadcast_sent"); + + await setBroadcastArchived({ ...ids, archived: true }); + await expect(startSequence(ids)).rejects.toThrow(responses.broadcast_archived); + }); + + it("only archives sent broadcasts", async () => { + const draft = await broadcast("draft"); + await expect( + setBroadcastArchived({ + teamId: draft.team.id, + sequenceId: draft.sequence.sequenceId, + archived: true, + }), + ).rejects.toThrow("broadcast_not_sent"); + + const { team } = await seedTeamAndContact(tdb); + const template = await makeTemplate(team.id); + const multiStep = await createSequence({ + teamId: team.id, + type: "sequence", + templateId: template.templateId, + }); + await expect( + setBroadcastArchived({ + teamId: team.id, + sequenceId: multiStep.sequenceId, + archived: true, + }), + ).resolves.toBeNull(); + }); + + it("never archives, restores or deletes another team's broadcast", async () => { + const { sequence } = await broadcast("sent"); + const other = await seedTeamAndContact(tdb); + const ids = { teamId: other.team.id, sequenceId: sequence.sequenceId }; + + await expect(setBroadcastArchived({ ...ids, archived: true })).resolves.toBeNull(); + await expect(setBroadcastArchived({ ...ids, archived: false })).resolves.toBeNull(); + await expect(deleteSequence(ids)).resolves.toBe(false); + const [row] = await tdb.select().from(sequences); + expect(row.archivedAt).toBeNull(); + expect( + await listSequences({ + teamId: other.team.id, + type: "broadcast", + archived: "include", + }), + ).toHaveLength(0); + }); +}); diff --git a/apps/api/src/sequences/queries.ts b/apps/api/src/sequences/queries.ts index 899e7b7..890d21b 100644 --- a/apps/api/src/sequences/queries.ts +++ b/apps/api/src/sequences/queries.ts @@ -1,5 +1,16 @@ import type { Email as EmailContent } from "@sendlit/email-editor"; -import { and, asc, count, eq, inArray, notInArray } from "drizzle-orm"; +import { + and, + asc, + count, + eq, + inArray, + isNotNull, + isNull, + or, + type SQL, + sql, +} from "drizzle-orm"; import { assertLargeBroadcastReviewed } from "../abuse/broadcast-review"; import { assertEmailContentAllowed } from "../abuse/content-scan"; import { assertNoImportUnderReview } from "../abuse/imports"; @@ -50,7 +61,11 @@ import { captureEvent } from "../observability"; import { assertMailingAddressConfigured } from "../settings/general/queries"; import { getTeam } from "../team/queries"; import { resolveStartingTemplate } from "../templates/queries"; -import { type SequenceDeleteBlocker, sequenceDeleteBlocker } from "./delete-blockers"; +import { + broadcastArchiveBlocker, + type SequenceDeleteBlocker, + sequenceDeleteBlocker, +} from "./delete-blockers"; import { addRule, defaultEmailContent, @@ -232,22 +247,34 @@ export async function getSequenceBySequenceId( return hydrate(row); } +/** Which broadcasts a list shows: archived ones are hidden by default. */ +export type ArchivedFilter = "exclude" | "only" | "include"; + +function listCondition(teamId: string, type: MailType, archived: ArchivedFilter) { + const conditions: SQL[] = [eq(sequences.teamId, teamId), eq(sequences.type, type)]; + if (archived === "exclude") conditions.push(isNull(sequences.archivedAt)); + if (archived === "only") conditions.push(isNotNull(sequences.archivedAt)); + return and(...conditions); +} + export async function listSequences({ teamId, type, offset = 1, itemsPerPage: perPage = itemsPerPage, + archived = "exclude", }: { teamId: string; type: MailType; offset?: number; itemsPerPage?: number; + archived?: ArchivedFilter; }): Promise { const pageSize = Math.min(Math.max(perPage, 1), maxItemsPerPage); const rows = await db .select() .from(sequences) - .where(and(eq(sequences.teamId, teamId), eq(sequences.type, type))) + .where(listCondition(teamId, type, archived)) .limit(pageSize) .offset((Math.max(offset, 1) - 1) * pageSize); if (rows.length === 0) return []; @@ -277,11 +304,15 @@ export async function listSequences({ ); } -export async function countSequences(teamId: string, type: MailType): Promise { +export async function countSequences( + teamId: string, + type: MailType, + archived: ArchivedFilter = "exclude", +): Promise { const [row] = await db .select({ value: count() }) .from(sequences) - .where(and(eq(sequences.teamId, teamId), eq(sequences.type, type))); + .where(listCondition(teamId, type, archived)); return row?.value ?? 0; } @@ -732,15 +763,35 @@ export interface SequenceDeletionImpact { emailsSentCount: number; } +function deleteBlockerOf(sequence: HydratedSequence) { + return sequenceDeleteBlocker({ + status: sequence.status, + type: sequence.type, + report: sequence.report, + sendAt: sequence.emails[0]?.delayInMillis, + }); +} + +/** + * Rows that may be deleted right now, repeated in the delete statement so a + * sequence started (or a broadcast that began sending) since the check is + * kept: not active, except a broadcast still waiting for its send time; + * and never a broadcast that has started sending. + */ +const deletableNow = () => { + const notStarted = sql`${sequences.report}->'broadcast'->>'lockedAt' IS NULL`; + return or( + and(eq(sequences.type, "sequence"), sql`${sequences.status} <> 'active'`), + and(eq(sequences.type, "broadcast"), notStarted), + ); +}; + /** What deleting a sequence would remove, shown before the user confirms. */ export async function getSequenceDeletionImpact( teamId: string, sequenceId: string, ): Promise { - const [sequence] = await db - .select({ id: sequences.id, status: sequences.status }) - .from(sequences) - .where(and(eq(sequences.teamId, teamId), eq(sequences.sequenceId, sequenceId))); + const sequence = await getSequenceBySequenceId(teamId, sequenceId); if (!sequence) return null; const [[enrolled], [sent]] = await Promise.all([ db @@ -754,7 +805,7 @@ export async function getSequenceDeletionImpact( ]); return { status: sequence.status, - blocker: sequenceDeleteBlocker(sequence), + blocker: deleteBlockerOf(sequence), enrolledCount: enrolled?.value ?? 0, emailsSentCount: sent?.value ?? 0, }; @@ -776,7 +827,7 @@ export async function deleteSequence({ }): Promise { const sequence = await getSequenceBySequenceId(teamId, sequenceId); if (!sequence) return false; - const blocker = sequenceDeleteBlocker(sequence); + const blocker = deleteBlockerOf(sequence); if (blocker) throw new Error(blocker); const impact = await getSequenceDeletionImpact(teamId, sequenceId); @@ -788,12 +839,12 @@ export async function deleteSequence({ // Guarded on status so a sequence started since the read above is kept. const removed = await db .delete(sequences) - .where(and(eq(sequences.id, sequence.id), notInArray(sequences.status, ["active"]))) + .where(and(eq(sequences.id, sequence.id), deletableNow())) .returning({ id: sequences.id }); if (!removed.length) { const current = await getSequenceBySequenceId(teamId, sequenceId); if (!current) return false; - throw new Error(sequenceDeleteBlocker(current) ?? "sequence_active"); + throw new Error(deleteBlockerOf(current) ?? "sequence_active"); } const mediaIds = await releaseEmailMedia(teamId, emailIds, sentIds); @@ -816,6 +867,52 @@ export async function deleteSequence({ return true; } +/** + * Archives (or, with `archived: false`, restores) a sent broadcast (#147). + * An archived broadcast is hidden from the default list and the public web + * archive; its report, delivery history and images are kept. Throws + * `broadcast_not_sent` for a draft or scheduled broadcast (delete it + * instead) and `sequence_active` while it is sending; returns null when + * there is no such broadcast. + */ +export async function setBroadcastArchived({ + teamId, + sequenceId, + archived, +}: { + teamId: string; + sequenceId: string; + archived: boolean; +}): Promise { + const sequence = await getSequenceBySequenceId(teamId, sequenceId); + if (sequence?.type !== "broadcast") return null; + if (archived) { + const blocker = broadcastArchiveBlocker({ + status: sequence.status, + type: sequence.type, + report: sequence.report, + sendAt: sequence.emails[0]?.delayInMillis, + }); + if (blocker) throw new Error(blocker); + } + if (Boolean(sequence.archivedAt) === archived) return sequence; + + const [row] = await db + .update(sequences) + .set({ archivedAt: archived ? new Date() : null, updatedAt: new Date() }) + .where(eq(sequences.id, sequence.id)) + .returning(); + + captureEvent({ + event: archived ? "broadcast_archived" : "broadcast_unarchived", + source: archived ? "sequences.archive" : "sequences.unarchive", + teamId, + properties: { sequence_id: sequence.sequenceId, sequence_status: sequence.status }, + }); + + return hydrate(row); +} + export async function startSequence({ teamId, sequenceId, @@ -831,6 +928,8 @@ export async function startSequence({ if (!["draft", "paused"].includes(sequence.status)) { throw new Error(responses.sequence_already_started); } + // An archived broadcast was paused mid-send; restore it before resuming. + if (sequence.archivedAt) throw new Error(responses.broadcast_archived); if (!sequence.emails.some((e) => e.published)) { throw new Error(responses.no_published_emails); } diff --git a/apps/api/src/sequences/routes-purpose.test.ts b/apps/api/src/sequences/routes-purpose.test.ts index b982f56..50d74f6 100644 --- a/apps/api/src/sequences/routes-purpose.test.ts +++ b/apps/api/src/sequences/routes-purpose.test.ts @@ -8,6 +8,9 @@ const mocks = { sendSequenceEmailTest: vi.fn(), deleteSequence: vi.fn(), getSequenceDeletionImpact: vi.fn(), + setBroadcastArchived: vi.fn(), + listSequences: vi.fn(), + countSequences: vi.fn(), }; mock.module("../auth/middleware", () => ({ @@ -35,8 +38,9 @@ mock.module("./test-send", () => ({ mock.module("./queries", () => ({ createSequence: mocks.createSequence, addMailToSequence: mocks.addMailToSequence, - countSequences: vi.fn(), + countSequences: mocks.countSequences, deleteSequence: mocks.deleteSequence, + setBroadcastArchived: mocks.setBroadcastArchived, getSequenceDeletionImpact: mocks.getSequenceDeletionImpact, deleteMailFromSequence: vi.fn(), getEmailSentCount: vi.fn(), @@ -45,7 +49,7 @@ mock.module("./queries", () => ({ getSequenceOpenRate: vi.fn(), getSubscribers: vi.fn(), getSubscribersCount: vi.fn(), - listSequences: vi.fn(), + listSequences: mocks.listSequences, pauseSequence: vi.fn(), startSequence: vi.fn(), updateMailInSequence: mocks.updateMailInSequence, @@ -199,3 +203,73 @@ describe("sequence delete route", () => { expect(mocks.getSequenceDeletionImpact).toHaveBeenCalledWith("team-1", "seq_1"); }); }); + +describe("broadcast archive routes", () => { + it("answers 409 broadcast_sent when deleting a sent broadcast", async () => { + mocks.deleteSequence.mockRejectedValue(new Error("broadcast_sent")); + + const response = await requestApp(app(), "/sequences/seq_1", { method: "DELETE" }); + + expect(response.status).toBe(409); + expect(response.json()).toEqual({ error: "broadcast_sent" }); + }); + + it("archives and restores, and refuses unsent broadcasts with 409", async () => { + mocks.setBroadcastArchived + .mockResolvedValueOnce({ + sequenceId: "seq_1", + archivedAt: new Date(0), + emails: [], + }) + .mockResolvedValueOnce({ sequenceId: "seq_1", archivedAt: null, emails: [] }) + .mockRejectedValueOnce(new Error("broadcast_not_sent")) + .mockResolvedValueOnce(null); + + const archived = await requestApp(app(), "/sequences/seq_1/archive", { + method: "POST", + }); + expect(archived.status).toBe(200); + expect(archived.json().archivedAt).toBe(new Date(0).toISOString()); + expect(mocks.setBroadcastArchived).toHaveBeenLastCalledWith({ + teamId: "team-1", + sequenceId: "seq_1", + archived: true, + }); + + const restored = await requestApp(app(), "/sequences/seq_1/unarchive", { + method: "POST", + }); + expect(restored.json().archivedAt).toBeNull(); + expect(mocks.setBroadcastArchived).toHaveBeenLastCalledWith({ + teamId: "team-1", + sequenceId: "seq_1", + archived: false, + }); + + const draft = await requestApp(app(), "/sequences/seq_1/archive", { + method: "POST", + }); + expect(draft.status).toBe(409); + expect(draft.json()).toEqual({ error: "broadcast_not_sent" }); + expect( + (await requestApp(app(), "/sequences/seq_9/archive", { method: "POST" })).status, + ).toBe(404); + }); + + it("passes the archived filter to the list", async () => { + mocks.listSequences.mockResolvedValue([]); + mocks.countSequences.mockResolvedValue(0); + + const response = await requestApp(app(), "/sequences?type=broadcast&archived=only"); + + expect(response.status).toBe(200); + expect(mocks.listSequences).toHaveBeenCalledWith( + expect.objectContaining({ + teamId: "team-1", + type: "broadcast", + archived: "only", + }), + ); + expect(mocks.countSequences).toHaveBeenCalledWith("team-1", "broadcast", "only"); + }); +}); diff --git a/apps/api/src/sequences/routes.ts b/apps/api/src/sequences/routes.ts index c2535fa..b9a0514 100644 --- a/apps/api/src/sequences/routes.ts +++ b/apps/api/src/sequences/routes.ts @@ -15,7 +15,7 @@ import { updateWebPublishing, WebPublishingError, } from "../web/queries"; -import { isSequenceDeleteBlocker } from "./delete-blockers"; +import { broadcastArchiveErrors, isSequenceDeleteBlocker } from "./delete-blockers"; import { addMailToSequence, countSequences, @@ -32,6 +32,7 @@ import { type HydratedSequence, listSequences, pauseSequence, + setBroadcastArchived, startSequence, updateMailInSequence, updateSequence, @@ -97,8 +98,9 @@ const impl = s.router(contract.sequences, { type: query.type, offset: query.offset, itemsPerPage: query.itemsPerPage, + archived: query.archived, }), - countSequences(teamId, query.type), + countSequences(teamId, query.type, query.archived), ]); return { status: 200, body: { items: items.map(toBody), total } }; }, @@ -233,6 +235,31 @@ const impl = s.router(contract.sequences, { if (!deleted) return { status: 404, body: { error: "Sequence not found" } }; return { status: 204, body: undefined }; }, + archive: async ({ params, req }) => { + try { + const sequence = await setBroadcastArchived({ + teamId: (req as any).teamId, + sequenceId: params.sequenceId, + archived: true, + }); + if (!sequence) return { status: 404, body: { error: "Broadcast not found" } }; + return { status: 200, body: toBody(sequence) }; + } catch (err: any) { + if (err.message in broadcastArchiveErrors) { + return { status: 409, body: { error: err.message } }; + } + throw err; + } + }, + unarchive: async ({ params, req }) => { + const sequence = await setBroadcastArchived({ + teamId: (req as any).teamId, + sequenceId: params.sequenceId, + archived: false, + }); + if (!sequence) return { status: 404, body: { error: "Broadcast not found" } }; + return { status: 200, body: toBody(sequence) }; + }, deletionPreview: async ({ params, req }) => { const preview = await getSequenceDeletionImpact( (req as any).teamId, diff --git a/apps/api/src/web/queries.ts b/apps/api/src/web/queries.ts index e002de7..751d6ef 100644 --- a/apps/api/src/web/queries.ts +++ b/apps/api/src/web/queries.ts @@ -1,5 +1,5 @@ import type { WebIssue, WebPublishing, WebSettings } from "@sendlit/api-contract"; -import { and, count, desc, eq, isNotNull, ne, sql } from "drizzle-orm"; +import { and, count, desc, eq, isNotNull, isNull, ne, sql } from "drizzle-orm"; import { TEST_UNSUBSCRIBE_TOKEN } from "../config/constants"; import { db } from "../db/client"; import { sequenceEmails, sequences, settings, signupForms, teams } from "../db/schema"; @@ -39,13 +39,15 @@ async function firstFree(base: string, taken: (candidate: string) => Promise and( eq(sequences.teamId, teamId), eq(sequences.type, "broadcast"), eq(sequences.webPublished, true), isNotNull(sequences.webSlug), + isNull(sequences.archivedAt), sql`${sequences.report}->'broadcast'->>'lockedAt' IS NOT NULL`, ); @@ -197,7 +199,9 @@ export async function toWebPublishing( ogImage: sequence.webOgImage, noindex: sequence.webNoindex, canonicalUrl: sequence.webCanonicalUrl, - live: Boolean(sequence.webPublished && sequence.webSlug && sent), + live: Boolean( + sequence.webPublished && sequence.webSlug && sent && !sequence.archivedAt, + ), publishedAt: sequence.webPublished && sent ? sent.toISOString() : null, url: issueUrl(row, sequence), }; diff --git a/apps/api/src/web/web.test.ts b/apps/api/src/web/web.test.ts index f70ddc3..9e2ed4d 100644 --- a/apps/api/src/web/web.test.ts +++ b/apps/api/src/web/web.test.ts @@ -258,6 +258,42 @@ describe("public web pages", () => { expect(sitemap.body).not.toContain("/p/elsewhere"); }); + it("takes archived broadcasts off the archive, feed and sitemap until restored", async () => { + const { team } = await setup(); + const kept = await seedBroadcast(team.id, "Kept"); + const archived = await seedBroadcast(team.id, "Archived issue"); + await updateWebPublishing(kept, { published: true }); + await updateWebPublishing(archived, { published: true }); + const setArchived = (archivedAt: Date | null) => + tdb.update(sequences).set({ archivedAt }).where(eq(sequences.id, archived.id)); + + await setArchived(new Date()); + const archive = await requestApp(app(), "/w/acme-weekly"); + expect(archive.body).toContain("Kept"); + expect(archive.body).not.toContain("Archived issue"); + expect((await requestApp(app(), "/w/acme-weekly/p/archived-issue")).status).toBe( + 404, + ); + expect((await requestApp(app(), "/w/acme-weekly/feed.xml")).body).not.toContain( + "Archived issue", + ); + expect((await requestApp(app(), "/w/acme-weekly/sitemap.xml")).body).not.toContain( + "/p/archived-issue", + ); + expect((await listIssues(team.id)).total).toBe(1); + const [row] = await tdb + .select() + .from(sequences) + .where(eq(sequences.id, archived.id)); + expect((await toWebPublishing(row)).live).toBe(false); + + await setArchived(null); + expect((await requestApp(app(), "/w/acme-weekly/p/archived-issue")).status).toBe( + 200, + ); + expect((await listIssues(team.id)).total).toBe(2); + }); + it("404s unpublished issues, and everything once hosting is off", async () => { const { team } = await setup(); const issue = await seedBroadcast(team.id, "Gone"); diff --git a/apps/docs/content/docs/developers/errors.mdx b/apps/docs/content/docs/developers/errors.mdx index e510378..dba569b 100644 --- a/apps/docs/content/docs/developers/errors.mdx +++ b/apps/docs/content/docs/developers/errors.mdx @@ -45,6 +45,8 @@ template or transactional delivery. | `409` | `event_definition_exists` | An event with that name is already registered. | | `409` | `event_definition_in_use` | The event can't be deleted while a sequence uses it as its trigger. | | `409` | `sequence_active` | An active sequence, or a sending broadcast, can't be deleted. Pause it first. | +| `409` | `broadcast_sent` | A broadcast that has started sending can't be deleted, so its report is kept. Archive it instead. | +| `409` | `broadcast_not_sent` | Only a sent broadcast can be archived. Delete a draft or scheduled broadcast instead. | | `404` | `event_definition_not_found` | No event with that name is registered. | | `400` | `invalid_trigger_type` | The trigger type isn't allowed for this broadcast or sequence. | | `400` | `invalid_trigger_data` | The trigger value is invalid, for example an event name over 128 characters or starting with `$`. | diff --git a/apps/docs/content/docs/developers/mcp.mdx b/apps/docs/content/docs/developers/mcp.mdx index f3c561c..f430fd9 100644 --- a/apps/docs/content/docs/developers/mcp.mdx +++ b/apps/docs/content/docs/developers/mcp.mdx @@ -119,7 +119,8 @@ Broadcasts use the sequence resource with one email and an audience filter. For - `list_sequences`, `get_sequence`, `create_sequence`, `update_sequence` - `add_sequence_email`, `update_sequence_email`, `delete_sequence_email` -- `preview_sequence_deletion`, `delete_sequence` (destructive: pause an active sequence first; its enrollments and stats are deleted) +- `preview_sequence_deletion`, `delete_sequence` (destructive: pause an active sequence first; its enrollments and stats are deleted). Draft and scheduled broadcasts can be deleted; sent ones can't (`broadcast_sent`) +- `archive_broadcast`, `unarchive_broadcast` hide a sent broadcast from the list and the web archive, keeping its report. `list_sequences` takes `archived` (`exclude` by default, `only`, `include`) - `send_sequence_email_test` sends a `[Test]` copy of one email to you (or up to five addresses) - `start_sequence`, `pause_sequence`, `get_sequence_stats`, `get_sequence_subscribers` - `get_broadcast_web_publishing`, `update_broadcast_web_publishing`, `list_web_issues`, `get_web_issue` ([web archive](/email-marketing/web-archive)) diff --git a/apps/docs/content/docs/email-marketing/broadcasts.mdx b/apps/docs/content/docs/email-marketing/broadcasts.mdx index 1ba3c95..f3f403f 100644 --- a/apps/docs/content/docs/email-marketing/broadcasts.mdx +++ b/apps/docs/content/docs/email-marketing/broadcasts.mdx @@ -113,3 +113,30 @@ Broadcast delivery uses the same sequence state machine as automations: - `completed` means sent. The first email's `delayInMillis` stores the absolute send timestamp for broadcasts. + +## Delete or archive a broadcast + +Use the **⋯** menu on the Broadcasts list, or the buttons on the broadcast +page. + +- **Drafts and scheduled broadcasts** can be deleted. Deleting a scheduled + broadcast cancels its send. Images only it used are deleted from the media + library. Deleting is permanent. +- **A broadcast that is sending** can't be deleted or archived. Pause it first. +- **A sent broadcast** can't be deleted, so its report is never lost (the API + answers `409 broadcast_sent`). Archive it instead. An archived broadcast: + - leaves the Broadcasts list. Pick **Archived** in the list's filter to see + it; + - keeps its report, delivery history and images, and the "view in browser" + links in the emails you sent keep working; + - leaves the [web archive](/email-marketing/web-archive), its RSS feed and + sitemap. + + **Unarchive** puts it back in the list, and back on the web archive if it + was published there. + +With the API, `DELETE /sequences/{sequenceId}` deletes, +`POST /sequences/{sequenceId}/archive` and `/unarchive` archive and restore, +and `GET /sequences?type=broadcast&archived=only` lists archived broadcasts. +MCP clients use `delete_sequence`, `archive_broadcast` and +`unarchive_broadcast`. diff --git a/apps/docs/content/docs/email-marketing/web-archive.mdx b/apps/docs/content/docs/email-marketing/web-archive.mdx index 60aad46..e4c5cc4 100644 --- a/apps/docs/content/docs/email-marketing/web-archive.mdx +++ b/apps/docs/content/docs/email-marketing/web-archive.mdx @@ -40,8 +40,10 @@ sent. You can also set: sitemap. Turn **Publish to the web** off to take the page down; it then returns 404 -and leaves the archive, the RSS feed and the sitemap. Deleting the broadcast -does the same. +and leaves the archive, the RSS feed and the sitemap. +[Archiving the broadcast](/email-marketing/broadcasts#delete-or-archive-a-broadcast) +does the same; unarchiving brings the page back. Sent broadcasts can't be +deleted. ## What the public page shows diff --git a/apps/web/app/(dashboard)/broadcasts/[sequenceId]/page.tsx b/apps/web/app/(dashboard)/broadcasts/[sequenceId]/page.tsx index bd77913..28d7a87 100644 --- a/apps/web/app/(dashboard)/broadcasts/[sequenceId]/page.tsx +++ b/apps/web/app/(dashboard)/broadcasts/[sequenceId]/page.tsx @@ -9,13 +9,25 @@ import { SequenceAnalytics, type SequenceStats, } from "@sendlit/email-blocks"; -import { CalendarClock, Check, Pencil, Send, X } from "lucide-react"; +import { + Archive, + ArchiveRestore, + CalendarClock, + Check, + Pencil, + Send, + Trash2, + X, +} from "lucide-react"; import Link from "next/link"; +import { useRouter } from "next/navigation"; import { use, useEffect, useState } from "react"; +import { ArchiveBroadcastDialog } from "@/components/dashboard/archive-broadcast-dialog"; import { AutoPauseBanner } from "@/components/dashboard/auto-pause-banner"; import { Banner } from "@/components/dashboard/banner"; import { useSetBreadcrumb } from "@/components/dashboard/breadcrumb-context"; import { BroadcastWebDialog } from "@/components/dashboard/broadcast-web-dialog"; +import { DeleteSequenceDialog } from "@/components/dashboard/delete-sequence-dialog"; import { DeliverySourcePicker } from "@/components/dashboard/delivery-source-picker"; import { MARKETING_EMAIL_EDITOR_BLOCKS, @@ -39,6 +51,11 @@ import { } from "@/components/ui/codelit/dialog"; import { Input } from "@/components/ui/codelit/input"; import { Label } from "@/components/ui/codelit/label"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/codelit/tooltip"; import { type DeliverySourceSelection, getSequence, @@ -48,11 +65,16 @@ import { pauseSequence, type SendingOption, startSequence, + unarchiveBroadcast, updateSequence, updateSequenceEmail, } from "@/lib/api"; import { ApiError } from "@/lib/api-client"; -import { broadcastScheduledFor, presentBroadcastStatus } from "@/lib/broadcast"; +import { + broadcastDeleteActions, + broadcastScheduledFor, + presentBroadcastStatus, +} from "@/lib/broadcast"; import { isDeletedDeliverySource, presentDeliverySourceError, @@ -104,6 +126,9 @@ export default function BroadcastEditorPage({ const [working, setWorking] = useState(false); const [audienceCount, setAudienceCount] = useState(); const [sendingOptions, setSendingOptions] = useState([]); + const [deleteOpen, setDeleteOpen] = useState(false); + const [archiveOpen, setArchiveOpen] = useState(false); + const router = useRouter(); const { segmentProps, clearSelection } = useSegments(setError); const exclusionSegments = useSegments(setError); @@ -255,6 +280,18 @@ export default function BroadcastEditorPage({ } } + async function unarchive() { + setWorking(true); + setError(null); + try { + setSequence(await unarchiveBroadcast(sequenceId)); + } catch (err) { + setError(err instanceof ApiError ? err.message : "Action failed"); + } finally { + setWorking(false); + } + } + function openSchedule() { setScheduleAt(toLocalInputValue(new Date(Date.now() + 60 * 60_000))); setScheduleOpen(true); @@ -284,6 +321,7 @@ export default function BroadcastEditorPage({ } const status = presentBroadcastStatus(sequence); + const removal = broadcastDeleteActions(sequence); const scheduledFor = broadcastScheduledFor(sequence); const scheduledForLabel = scheduledFor?.toLocaleString(); // A broadcast paused mid-send is resumed, not edited or re-sent. @@ -306,8 +344,43 @@ export default function BroadcastEditorPage({ {sequence.title || "Untitled broadcast"} {status.label} + {sequence.archivedAt && archived}
+ {sequence.archivedAt ? ( + + ) : removal.archive.allowed ? ( + + ) : null} + + + {/* A disabled button gets no pointer events; the span shows why. */} + + + + + {removal.delete.reason && ( + {removal.delete.reason} + )} + {editable && ( <> @@ -351,12 +424,35 @@ export default function BroadcastEditorPage({
+ router.push("/broadcasts")} + onArchiveInstead={() => { + setDeleteOpen(false); + setArchiveOpen(true); + }} + /> + + {error && ( )} + {sequence.archivedAt && ( + + This broadcast is archived. It's hidden from the broadcasts list and + your web archive; its report is kept. Unarchive it to bring it back. + + )} + {scheduledForLabel && ( = { + broadcast_not_sent: + "Only a sent broadcast can be archived. Delete a draft or scheduled broadcast instead.", + sequence_active: "This broadcast is sending. Pause it before archiving it.", +}; + +/** + * Confirms archiving a sent broadcast (ADR 0008): it leaves the list and the + * public web archive, and keeps its report. Unarchiving restores it. + */ +export function ArchiveBroadcastDialog({ + broadcast, + onOpenChange, + onArchived, +}: { + /** The broadcast to archive; the dialog is open while this is set. */ + broadcast: { sequenceId: string; title: string } | null; + onOpenChange: (open: boolean) => void; + onArchived: () => Promise | void; +}) { + const name = broadcast?.title || "Untitled broadcast"; + return ( + + error instanceof ApiError && error.message in ERRORS + ? ERRORS[error.message] + : "Unable to archive this broadcast. Please try again." + } + onConfirm={async () => { + if (!broadcast) return; + await archiveBroadcast(broadcast.sequenceId); + await onArchived(); + }} + > +
    +
  • Its report, delivery history and images are kept.
  • +
  • + "View in browser" links in the emails already sent keep working. +
  • +
  • + To find it again, pick{" "} + Archived in the + broadcasts list. +
  • +
+
+ ); +} diff --git a/apps/web/components/dashboard/delete-confirmation-dialog.tsx b/apps/web/components/dashboard/delete-confirmation-dialog.tsx index be248eb..769e1ef 100644 --- a/apps/web/components/dashboard/delete-confirmation-dialog.tsx +++ b/apps/web/components/dashboard/delete-confirmation-dialog.tsx @@ -20,6 +20,8 @@ export function DeleteConfirmationDialog({ description = "This action cannot be undone.", confirmLabel = "Delete", confirmDisabled = false, + confirmVariant = "destructive", + pendingLabel = "Deleting…", errorMessage, children, }: { @@ -31,6 +33,10 @@ export function DeleteConfirmationDialog({ confirmLabel?: string; /** Holds the confirm button, e.g. while the details below load. */ confirmDisabled?: boolean; + /** `primary` for reversible actions such as archiving. */ + confirmVariant?: "destructive" | "primary"; + /** Shown on the confirm button while `onConfirm` runs. */ + pendingLabel?: string; /** Explains a failed delete; defaults to a generic retry message. */ errorMessage?: (error: unknown) => ReactNode; /** Details shown between the description and the buttons. */ @@ -71,7 +77,7 @@ export function DeleteConfirmationDialog({ Cancel { event.preventDefault(); @@ -90,7 +96,7 @@ export function DeleteConfirmationDialog({ } }} > - {isDeleting ? "Deleting…" : confirmLabel} + {isDeleting ? pendingLabel : confirmLabel} diff --git a/apps/web/components/dashboard/delete-sequence-dialog.test.tsx b/apps/web/components/dashboard/delete-sequence-dialog.test.tsx index 7244c83..d017ccb 100644 --- a/apps/web/components/dashboard/delete-sequence-dialog.test.tsx +++ b/apps/web/components/dashboard/delete-sequence-dialog.test.tsx @@ -5,7 +5,7 @@ registerDom(); const getSequenceDeletionPreview = mock(async (_sequenceId: string) => ({ status: "paused", - blocker: null as "sequence_active" | null, + blocker: null as "sequence_active" | "broadcast_sent" | null, enrolledCount: 3, emailsSentCount: 12, })); @@ -80,4 +80,58 @@ describe("DeleteSequenceDialog", () => { expect(confirmButton().disabled).toBe(true); expect(deleteSequence).not.toHaveBeenCalled(); }); + + it("refuses to delete a sent broadcast and offers to archive it", async () => { + getSequenceDeletionPreview.mockResolvedValueOnce({ + status: "completed", + blocker: "broadcast_sent", + enrolledCount: 0, + emailsSentCount: 40, + }); + const onArchiveInstead = mock(); + render( + , + ); + await settle(); + + expect(screen.getByText(/its report would be lost/)).toBeTruthy(); + const confirm = screen.getByRole("button", { + name: "Delete broadcast", + }) as HTMLButtonElement; + expect(confirm.disabled).toBe(true); + fireEvent.click(screen.getByRole("button", { name: "Archive instead" })); + expect(onArchiveInstead).toHaveBeenCalled(); + expect(deleteSequence).not.toHaveBeenCalled(); + }); + + it("deletes a scheduled broadcast and says its send is cancelled", async () => { + getSequenceDeletionPreview.mockResolvedValueOnce({ + status: "active", + blocker: null, + enrolledCount: 0, + emailsSentCount: 0, + }); + const onDeleted = mock(); + render( + , + ); + await settle(); + + expect(screen.getByText(/scheduled send is cancelled/)).toBeTruthy(); + fireEvent.click(screen.getByRole("button", { name: "Delete broadcast" })); + await settle(); + expect(deleteSequence).toHaveBeenCalledWith("seq_4"); + expect(onDeleted).toHaveBeenCalled(); + }); }); diff --git a/apps/web/components/dashboard/delete-sequence-dialog.tsx b/apps/web/components/dashboard/delete-sequence-dialog.tsx index 9cbb226..8cc9443 100644 --- a/apps/web/components/dashboard/delete-sequence-dialog.tsx +++ b/apps/web/components/dashboard/delete-sequence-dialog.tsx @@ -1,33 +1,54 @@ "use client"; import type { SequenceDeletionPreview } from "@sendlit/api-contract"; +import { Archive } from "lucide-react"; import { useEffect, useState } from "react"; import { DeleteConfirmationDialog } from "@/components/dashboard/delete-confirmation-dialog"; import { Loading } from "@/components/dashboard/loading"; +import { Button } from "@/components/ui/codelit/button"; import { deleteSequence, getSequenceDeletionPreview } from "@/lib/api"; import { ApiError } from "@/lib/api-client"; -const ACTIVE_MESSAGE = - "This sequence is active. Pause it before deleting it, so no contact is dropped mid-flow."; +type Blocker = NonNullable; + +const BLOCKER_MESSAGES: Record<"sequence" | "broadcast", Record> = { + sequence: { + sequence_active: + "This sequence is active. Pause it before deleting it, so no contact is dropped mid-flow.", + broadcast_sent: "", + }, + broadcast: { + sequence_active: "This broadcast is sending. Pause it before deleting it.", + broadcast_sent: + "This broadcast has been sent, so it can't be deleted: its report would be lost. Archive it instead. It leaves the list and the web archive, and its report is kept.", + }, +}; function plural(count: number, one: string, many: string) { return `${count.toLocaleString()} ${count === 1 ? one : many}`; } /** - * Confirms deleting a sequence. Shows how many contacts are enrolled and how - * many emails it sent, because both go with it (ADR 0007), and refuses while - * the sequence is active. + * Confirms deleting a sequence or broadcast. For a sequence it shows how many + * contacts are enrolled and how many emails it sent, because both go with it + * (ADR 0007), and refuses while the sequence is active. A broadcast can only + * be deleted before it starts sending; a sent one is archived instead + * (ADR 0008), which `onArchiveInstead` offers. */ export function DeleteSequenceDialog({ sequence, + type = "sequence", onOpenChange, onDeleted, + onArchiveInstead, }: { /** The sequence to delete; the dialog is open while this is set. */ sequence: { sequenceId: string; title: string } | null; + type?: "sequence" | "broadcast"; onOpenChange: (open: boolean) => void; onDeleted: () => Promise | void; + /** Offered when a sent broadcast can't be deleted. */ + onArchiveInstead?: () => void; }) { const sequenceId = sequence?.sequenceId; const [preview, setPreview] = useState(null); @@ -45,30 +66,35 @@ export function DeleteSequenceDialog({ .catch((err) => { if (!cancelled) { setLoadError( - err instanceof ApiError ? err.message : "Couldn't load this sequence.", + err instanceof ApiError ? err.message : `Couldn't load this ${type}.`, ); } }); return () => { cancelled = true; }; - }, [sequenceId]); + }, [sequenceId, type]); - const blocked = preview?.blocker === "sequence_active"; - const name = sequence?.title || "Untitled sequence"; + const blocker = preview?.blocker ?? null; + const messages = BLOCKER_MESSAGES[type]; + const name = sequence?.title || `Untitled ${type}`; return ( - error instanceof ApiError && error.message === "sequence_active" - ? ACTIVE_MESSAGE - : "Unable to delete this sequence. Please try again." + error instanceof ApiError && error.message in messages + ? messages[error.message as Blocker] + : `Unable to delete this ${type}. Please try again.` } onConfirm={async () => { if (!sequenceId) return; @@ -82,13 +108,28 @@ export function DeleteSequenceDialog({

) : !preview ? ( - ) : blocked ? ( -

- {ACTIVE_MESSAGE} -

+

{messages[blocker]}

+ {blocker === "broadcast_sent" && onArchiveInstead && ( + + )} + + ) : type === "broadcast" ? ( +
    +
  • + {preview.status === "active" + ? "Its scheduled send is cancelled. Nobody gets it." + : "It hasn't been sent, so no report is lost."} +
  • +
  • Images only this broadcast uses are deleted from the media library.
  • +
) : (
  • diff --git a/apps/web/components/dashboard/sequence-list.tsx b/apps/web/components/dashboard/sequence-list.tsx index 2c2d834..ec090f2 100644 --- a/apps/web/components/dashboard/sequence-list.tsx +++ b/apps/web/components/dashboard/sequence-list.tsx @@ -1,9 +1,17 @@ "use client"; import type { MailType, Sequence } from "@sendlit/email-blocks"; -import { MoreHorizontal, Pause, Play, Trash2 } from "lucide-react"; +import { + Archive, + ArchiveRestore, + MoreHorizontal, + Pause, + Play, + Trash2, +} from "lucide-react"; import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; +import { ArchiveBroadcastDialog } from "@/components/dashboard/archive-broadcast-dialog"; import { Banner } from "@/components/dashboard/banner"; import { useSetBreadcrumb } from "@/components/dashboard/breadcrumb-context"; import { DeleteSequenceDialog } from "@/components/dashboard/delete-sequence-dialog"; @@ -22,9 +30,21 @@ import { DropdownMenuTrigger, } from "@/components/ui/codelit/dropdown-menu"; import { IconButton } from "@/components/ui/codelit/icon-button"; -import { listSequences, pauseSequence, startSequence } from "@/lib/api"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/codelit/select"; +import { + listSequences, + pauseSequence, + startSequence, + unarchiveBroadcast, +} from "@/lib/api"; import { ApiError } from "@/lib/api-client"; -import { presentBroadcastStatus } from "@/lib/broadcast"; +import { broadcastDeleteActions, presentBroadcastStatus } from "@/lib/broadcast"; import { ITEMS_PER_PAGE } from "@/lib/pagination"; const STATUS_VARIANT: Record = { @@ -56,6 +76,10 @@ export function SequenceListPage({ const [sequencePendingDelete, setSequencePendingDelete] = useState( null, ); + const [broadcastPendingArchive, setBroadcastPendingArchive] = + useState(null); + // Broadcasts only: archived ones are listed on their own (ADR 0008). + const [archived, setArchived] = useState<"exclude" | "only">("exclude"); useSetBreadcrumb([{ label: title }]); @@ -65,6 +89,7 @@ export function SequenceListPage({ const result = await listSequences(type, { offset: pageToLoad, itemsPerPage: ITEMS_PER_PAGE, + ...(type === "broadcast" && { archived }), }); setSequences(result.items); setTotal(result.total); @@ -76,10 +101,25 @@ export function SequenceListPage({ } } - // biome-ignore lint/correctness/useExhaustiveDependencies: load is recreated every render and reads type; reload only when type changes + // biome-ignore lint/correctness/useExhaustiveDependencies: load is recreated every render and reads type and archived; reload only when they change useEffect(() => { load(); - }, [type]); + }, [type, archived]); + + /** Reloads the current page, or the previous one when this row was its last. */ + async function reloadAfterRemoving() { + const lastOnPage = sequences?.length === 1 && page > 1; + await load(lastOnPage ? page - 1 : page); + } + + async function unarchive(sequence: Sequence) { + try { + await unarchiveBroadcast(sequence.sequenceId); + await reloadAfterRemoving(); + } catch (err) { + setError(err instanceof ApiError ? err.message : "Action failed"); + } + } async function toggle(sequence: Sequence) { try { @@ -110,15 +150,43 @@ export function SequenceListPage({ { if (!open) setSequencePendingDelete(null); }} - onDeleted={async () => { - const lastOnPage = sequences?.length === 1 && page > 1; - await load(lastOnPage ? page - 1 : page); + onDeleted={reloadAfterRemoving} + onArchiveInstead={() => { + setBroadcastPendingArchive(sequencePendingDelete); + setSequencePendingDelete(null); }} /> + {type === "broadcast" && ( + <> + { + if (!open) setBroadcastPendingArchive(null); + }} + onArchived={reloadAfterRemoving} + /> +
    + +
    + + )} + {error && ( @@ -130,7 +198,9 @@ export function SequenceListPage({ ) : sequences.length === 0 ? ( - Nothing here yet. + {type === "broadcast" && archived === "only" + ? "No archived broadcasts. Archive a sent broadcast to hide it from the list." + : "Nothing here yet."} ) : ( @@ -157,9 +227,14 @@ export function SequenceListPage({ {type === "broadcast" ? ( - - {presentBroadcastStatus(sequence).label} - + + + {presentBroadcastStatus(sequence).label} + + {sequence.archivedAt && ( + archived + )} + ) : ( {sequence.status} @@ -196,7 +271,14 @@ export function SequenceListPage({ )} )} - {/* Broadcast delete and archive arrive with #147. */} + {type === "broadcast" && ( + setSequencePendingDelete(sequence)} + onArchive={() => setBroadcastPendingArchive(sequence)} + onUnarchive={() => void unarchive(sequence)} + /> + )} {type === "sequence" && ( @@ -249,3 +331,57 @@ export function SequenceListPage({ ); } + +/** Delete, archive or unarchive a broadcast, with the reason an action is + * unavailable shown on the disabled item. */ +function BroadcastRowMenu({ + sequence, + onDelete, + onArchive, + onUnarchive, +}: { + sequence: Sequence; + onDelete: () => void; + onArchive: () => void; + onUnarchive: () => void; +}) { + const actions = broadcastDeleteActions(sequence); + return ( + + + e.stopPropagation()} + > + + + + e.stopPropagation()}> + {sequence.archivedAt ? ( + + + Unarchive + + ) : actions.archive.allowed ? ( + + + Archive + + ) : null} + + + {actions.delete.allowed ? "Delete" : "Delete (unavailable)"} + + {actions.delete.reason && ( +

    + {actions.delete.reason} +

    + )} +
    +
    + ); +} diff --git a/apps/web/lib/api.ts b/apps/web/lib/api.ts index 93e5b7b..7918769 100644 --- a/apps/web/lib/api.ts +++ b/apps/web/lib/api.ts @@ -539,7 +539,12 @@ export function deleteTemplate(templateId: string) { export function listSequences( type: MailType, - params: { offset?: number; itemsPerPage?: number } = {}, + params: { + offset?: number; + itemsPerPage?: number; + /** Archived broadcasts are left out unless this is `only` or `include`. */ + archived?: "exclude" | "only" | "include"; + } = {}, ) { return unwrap>( client.sequences.list({ query: { type, ...params } }), @@ -717,6 +722,15 @@ export function pauseSequence(sequenceId: string) { return unwrap(client.sequences.pause({ params: { sequenceId } })); } +/** Hides a sent broadcast from the list and the web archive, keeping its report. */ +export function archiveBroadcast(sequenceId: string) { + return unwrap(client.sequences.archive({ params: { sequenceId } })); +} + +export function unarchiveBroadcast(sequenceId: string) { + return unwrap(client.sequences.unarchive({ params: { sequenceId } })); +} + /** Enrolled contacts and sent emails a delete would remove, and any blocker. */ export function getSequenceDeletionPreview(sequenceId: string) { return unwrap( diff --git a/apps/web/lib/broadcast.test.ts b/apps/web/lib/broadcast.test.ts index b35c9bf..8616bd2 100644 --- a/apps/web/lib/broadcast.test.ts +++ b/apps/web/lib/broadcast.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "bun:test"; import type { Sequence } from "@sendlit/email-blocks"; -import { presentBroadcastStatus } from "./broadcast"; +import { broadcastDeleteActions, presentBroadcastStatus } from "./broadcast"; const broadcast = (status: string, lockedAt: number | null) => ({ @@ -20,3 +20,34 @@ describe("presentBroadcastStatus", () => { ); }); }); + +describe("broadcastDeleteActions", () => { + const scheduled = { + status: "active", + report: {}, + emails: [{ delayInMillis: Date.now() + 3_600_000 }], + } as unknown as Sequence; + + it("deletes drafts and scheduled broadcasts, archives sent ones", () => { + expect(broadcastDeleteActions(broadcast("draft", null))).toMatchObject({ + delete: { allowed: true }, + archive: { allowed: false }, + }); + expect(broadcastDeleteActions(scheduled).delete.allowed).toBe(true); + for (const status of ["completed", "paused"]) { + expect(broadcastDeleteActions(broadcast(status, Date.now()))).toMatchObject({ + delete: { allowed: false }, + archive: { allowed: true }, + }); + } + }); + + it("allows neither while sending", () => { + const sending = broadcastDeleteActions(broadcast("active", Date.now())); + expect(sending.delete).toEqual({ + allowed: false, + reason: "It's sending. Pause it first.", + }); + expect(sending.archive.allowed).toBe(false); + }); +}); diff --git a/apps/web/lib/broadcast.ts b/apps/web/lib/broadcast.ts index 254ab7c..243af06 100644 --- a/apps/web/lib/broadcast.ts +++ b/apps/web/lib/broadcast.ts @@ -36,3 +36,42 @@ export function broadcastScheduledFor(sequence: Sequence): Date | null { const sendAt = sequence.emails[0]?.delayInMillis ?? 0; return sendAt > Date.now() ? new Date(sendAt) : null; } + +export interface BroadcastActionState { + allowed: boolean; + /** Why it isn't allowed, shown next to the disabled action. */ + reason?: string; +} + +/** + * Which of delete and archive a broadcast allows now, mirroring the API's + * blockers (`apps/api/src/sequences/delete-blockers.ts`): drafts and + * scheduled broadcasts can be deleted, a sending one neither, and one that + * has started sending can only be archived. + */ +export function broadcastDeleteActions(sequence: Sequence): { + delete: BroadcastActionState; + archive: BroadcastActionState; +} { + const started = Boolean(sequence.report?.broadcast?.lockedAt); + if (sequence.status === "active" && !broadcastScheduledFor(sequence)) { + const reason = "It's sending. Pause it first."; + return { delete: { allowed: false, reason }, archive: { allowed: false, reason } }; + } + if (started) { + return { + delete: { + allowed: false, + reason: "Sent broadcasts are archived, not deleted, to keep their report.", + }, + archive: { allowed: true }, + }; + } + return { + delete: { allowed: true }, + archive: { + allowed: false, + reason: "Only sent broadcasts can be archived.", + }, + }; +} diff --git a/docs/decisions/0008-archive-sent-broadcasts.md b/docs/decisions/0008-archive-sent-broadcasts.md new file mode 100644 index 0000000..efa88ed --- /dev/null +++ b/docs/decisions/0008-archive-sent-broadcasts.md @@ -0,0 +1,49 @@ +# ADR 0008: Archive sent broadcasts instead of deleting them + +Status: accepted +Date: 2026-10-29 + +## Context + +`DELETE /sequences/:sequenceId` deletes a broadcast together with its +delivery, open and click history (#147). ADR 0007 settled deletes for +sequences and left broadcasts to this issue. A broadcast's value after it is +sent is its report and its public web archive page, and the tracker (#152) +says anything that has sent mail is archived rather than deleted. + +Mailchimp lets you delete draft and unsent scheduled campaigns, and archives +sent ones: they leave the campaign list but keep their report, and can be +restored. Klaviyo and ActiveCampaign behave similarly for sent campaigns. + +## Decision + +- A draft broadcast, or a scheduled one whose send time hasn't come, can be + hard deleted. Its schedule goes with it, and its images follow ADR 0006. +- A broadcast that is sending (active, with its audience locked or its send + time reached) can't be deleted or archived: `409 sequence_active`. +- Once a broadcast has started sending (sent, or paused mid-send) it can't be + deleted: `409 broadcast_sent`. The delete statement repeats the check, so a + broadcast that starts sending in between is kept. +- Such a broadcast can be archived instead (`POST .../archive`, nullable + `sequences.archived_at`). Archived broadcasts are left out of + `GET /sequences` unless `archived=only|include`; their report, delivery + history and images are kept, and `.../unarchive` restores them. A broadcast + archived while paused mid-send can't resume until it is restored. +- Archived broadcasts leave the public web archive, its RSS feed and sitemap, + as the issue asks: archiving means "hide this", and an issue the sender + hid from their own list shouldn't keep being promoted publicly. Unarchiving + brings the page back if it is still published. Per-recipient "view in + browser" pages keep working, because they are linked from mail already in + inboxes. +- Blocker codes stay in `apps/api/src/sequences/delete-blockers.ts`; MCP + reuses `delete_sequence` and adds `archive_broadcast` and + `unarchive_broadcast`. + +## Consequences + +- Sent broadcasts are never hard deleted through the API; their history only + goes with the team or organization. +- Archive is broadcast-only. Sequences can be given it later without changing + the delete API (see ADR 0007). +- The column is nullable and additive, so the previous release keeps working + against the migrated schema (it simply shows archived broadcasts). diff --git a/packages/api-contract/src/contract.ts b/packages/api-contract/src/contract.ts index 7f4b876..da7fc5f 100644 --- a/packages/api-contract/src/contract.ts +++ b/packages/api-contract/src/contract.ts @@ -598,6 +598,8 @@ const sequencesContract = c.router( query: listSequencesQuerySchema, responses: { 200: paginated(sequenceSchema) }, summary: "List broadcasts or sequences", + description: + "Archived broadcasts are left out unless `archived` is `only` or `include`.", }, get: { method: "GET", @@ -624,7 +626,7 @@ const sequencesContract = c.router( responses: { 204: c.noBody(), 404: errorSchema, 409: errorSchema }, summary: "Delete a broadcast or sequence", description: - "Permanently deletes the broadcast or sequence, its emails and triggers. An active one can't be deleted: pause it first (409 `sequence_active`). Contacts still enrolled in a paused sequence leave it, and its delivery, open and click history is deleted too, so its stats are gone; check GET /sequences/{sequenceId}/deletion-preview first. The outbound message log and suppressions are kept. Images it uses that nothing else uses are deleted from the media library and storage; images used by sent mail are kept.", + "Permanently deletes the broadcast or sequence, its emails and triggers. An active sequence or a sending broadcast can't be deleted: pause it first (409 `sequence_active`). A draft or scheduled broadcast can be deleted; one that has started sending can't (409 `broadcast_sent`): archive it instead with POST /sequences/{sequenceId}/archive. Contacts still enrolled in a paused sequence leave it, and its delivery, open and click history is deleted too, so its stats are gone; check GET /sequences/{sequenceId}/deletion-preview first. The outbound message log and suppressions are kept. Images it uses that nothing else uses are deleted from the media library and storage; images used by sent mail are kept.", }, deletionPreview: { method: "GET", @@ -634,6 +636,24 @@ const sequencesContract = c.router( description: "How many contacts are enrolled and how many emails it has sent, and whether it can be deleted right now. Nothing is changed.", }, + archive: { + method: "POST", + path: "/sequences/:sequenceId/archive", + body: c.noBody(), + responses: { 200: sequenceSchema, 404: errorSchema, 409: errorSchema }, + summary: "Archive a sent broadcast", + description: + 'Hides a sent broadcast from the broadcasts list (see `archived` on GET /sequences) and from the public web archive, its feed and sitemap. Its report, delivery history and images are kept, and recipients\' "view in browser" links keep working. 409 `broadcast_not_sent` for a draft or scheduled broadcast (delete it instead) and `sequence_active` while it is sending. Archiving an archived broadcast changes nothing. 404 for sequences.', + }, + unarchive: { + method: "POST", + path: "/sequences/:sequenceId/unarchive", + body: c.noBody(), + responses: { 200: sequenceSchema, 404: errorSchema }, + summary: "Restore an archived broadcast", + description: + "Puts an archived broadcast back in the broadcasts list, and back on the web archive when it is published there.", + }, addEmail: { method: "POST", path: "/sequences/:sequenceId/emails", diff --git a/packages/api-contract/src/mcp-parity.ts b/packages/api-contract/src/mcp-parity.ts index f68d511..587544f 100644 --- a/packages/api-contract/src/mcp-parity.ts +++ b/packages/api-contract/src/mcp-parity.ts @@ -141,6 +141,8 @@ export const mcpParityManifest: readonly McpParityManifestEntry[] = [ required("sequences.update", "update_sequence", "write"), required("sequences.remove", "delete_sequence", "destructive"), required("sequences.deletionPreview", "preview_sequence_deletion", "read"), + required("sequences.archive", "archive_broadcast", "write"), + required("sequences.unarchive", "unarchive_broadcast", "write"), required("sequences.addEmail", "add_sequence_email", "write"), required("sequences.updateEmail", "update_sequence_email", "write"), required("contacts.import", "import_contacts", "write"), diff --git a/packages/api-contract/src/schemas/sequences.ts b/packages/api-contract/src/schemas/sequences.ts index d803a5f..909612c 100644 --- a/packages/api-contract/src/schemas/sequences.ts +++ b/packages/api-contract/src/schemas/sequences.ts @@ -166,6 +166,8 @@ export const sequenceSchema = z.object({ /** Whether sends link a hosted web version ("View in browser"). On for * new broadcasts, off for new sequences. */ viewInBrowser: z.boolean(), + /** When a sent broadcast was archived; `null` when it isn't. */ + archivedAt: z.string().nullable().optional(), createdAt: z.string().nullable(), updatedAt: z.string().nullable(), emails: z.array(sequenceEmailSchema), @@ -183,8 +185,16 @@ export const sequenceSchema = z.object({ .optional(), }); +export const archivedFilters = ["exclude", "only", "include"] as const; + export const listSequencesQuerySchema = z.object({ type: z.enum(mailTypes), + archived: z + .enum(archivedFilters) + .optional() + .describe( + "Archived broadcasts: exclude (default) hides them, only lists just them, include lists both", + ), offset: z.coerce.number().int().min(1).optional(), itemsPerPage: z.coerce.number().int().min(1).max(50).optional(), }); @@ -293,10 +303,10 @@ export const sequenceStatsSchema = z.object({ export const sequenceDeletionPreviewSchema = z.object({ status: z.string().describe("The broadcast or sequence's current status"), blocker: z - .enum(["sequence_active"]) + .enum(["sequence_active", "broadcast_sent"]) .nullable() .describe( - "Why it can't be deleted right now (sequence_active: pause it first), or null when it can", + "Why it can't be deleted right now (sequence_active: pause it first; broadcast_sent: archive the broadcast instead), or null when it can", ), enrolledCount: z .number() diff --git a/packages/api-contract/src/schemas/web.ts b/packages/api-contract/src/schemas/web.ts index 7ae0821..6efd496 100644 --- a/packages/api-contract/src/schemas/web.ts +++ b/packages/api-contract/src/schemas/web.ts @@ -26,7 +26,7 @@ export const webPublishingSchema = z.object({ ogImage: z.string().nullable(), noindex: z.boolean(), canonicalUrl: z.string().nullable(), - /** Published and already sent, so the page is public. */ + /** Published, already sent and not archived, so the page is public. */ live: z.boolean(), /** When the issue was sent; `null` until then. */ publishedAt: z.string().nullable(), diff --git a/packages/email-blocks/src/types.ts b/packages/email-blocks/src/types.ts index 132e0a7..33a2b41 100644 --- a/packages/email-blocks/src/types.ts +++ b/packages/email-blocks/src/types.ts @@ -171,6 +171,8 @@ export interface Sequence { report: SequenceReport; /** Whether its emails link a hosted web version ("View in browser"). */ viewInBrowser: boolean; + /** When a sent broadcast was archived; `null` when it isn't. */ + archivedAt?: string | null; createdAt: string; updatedAt: string; emails: SequenceEmail[];