Packages

phoenix_kit

1.7.111
1.7.208 1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib phoenix_kit migrations postgres v22.ex
Raw

lib/phoenix_kit/migrations/postgres/v22.ex

defmodule PhoenixKit.Migrations.Postgres.V22 do
@moduledoc """
PhoenixKit V22 Migration: Email System Improvements & Audit Logging
This migration addresses critical issues in the email system and adds comprehensive audit logging:
## Changes
### Email System Fixes
- Adds aws_message_id field to phoenix_kit_email_logs for AWS SES message ID tracking
- Adds partial unique index on aws_message_id (preventing duplicates while allowing nulls)
- Adds composite index (email_log_id, event_type) for faster duplicate event checking
- Creates phoenix_kit_email_orphaned_events table for tracking unmatched SQS events
- Adds phoenix_kit_email_metrics table for system metrics tracking
### Audit Logging System
- Adds phoenix_kit_audit_logs table for comprehensive action tracking
- Records admin actions with complete context (who, what, when, where)
- Supports metadata storage for additional context
- Indexed for efficient querying by user, action, and date
### New Tables
- **phoenix_kit_email_orphaned_events**: Tracks SQS events without matching email logs
- **phoenix_kit_email_metrics**: Tracks email system metrics (extraction rates, placeholder logs, etc.)
- **phoenix_kit_audit_logs**: Immutable audit trail for administrative actions
### Database Improvements
- Improved email log searching with dual message_id strategy
- Better duplicate prevention for events
- Enhanced debugging capabilities for AWS SES integration
- Complete audit trail for admin password resets and other sensitive operations
## Migration Strategy
The aws_message_id field addition is idempotent - it's added only if it doesn't exist.
All indexes use create_if_not_exists for safe re-runs.
"""
use Ecto.Migration
@doc """
Run the V22 migration to add email system improvements.
"""
def up(%{prefix: prefix} = _opts) do
# Add aws_message_id column to email_logs if it doesn't exist
alter table(:phoenix_kit_email_logs, prefix: prefix) do
# AWS SES message ID from provider response
add_if_not_exists :aws_message_id, :string, null: true
# Timestamps for when email was bounced, complained, opened, clicked
add_if_not_exists :bounced_at, :utc_datetime_usec, null: true
add_if_not_exists :complained_at, :utc_datetime_usec, null: true
add_if_not_exists :opened_at, :utc_datetime_usec, null: true
add_if_not_exists :clicked_at, :utc_datetime_usec, null: true
end
# Add partial unique index on aws_message_id (only where not null)
# This prevents duplicate AWS message IDs while allowing multiple nulls
create_if_not_exists unique_index(
:phoenix_kit_email_logs,
[:aws_message_id],
prefix: prefix,
name: :phoenix_kit_email_logs_aws_message_id_uidx,
where: "aws_message_id IS NOT NULL"
)
# Add composite index for (message_id, aws_message_id) for faster correlation
create_if_not_exists index(
:phoenix_kit_email_logs,
[:message_id, :aws_message_id],
prefix: prefix,
name: :phoenix_kit_email_logs_message_ids_idx
)
# Add composite index for (email_log_id, event_type) on email_events
# This dramatically speeds up event_exists? queries
create_if_not_exists index(
:phoenix_kit_email_events,
[:email_log_id, :event_type],
prefix: prefix,
name: :phoenix_kit_email_events_log_type_idx
)
# Create table for tracking orphaned SQS events (events without matching email logs)
create_if_not_exists table(:phoenix_kit_email_orphaned_events, prefix: prefix) do
# AWS SES message ID from the event
add :aws_message_id, :string, null: false
# Event type (delivery, bounce, open, etc.)
add :event_type, :string, null: false
# Full event data from SQS
add :event_data, :map, null: false, default: %{}
# When we received the orphaned event
add :received_at, :utc_datetime_usec, null: false, default: fragment("NOW()")
# Whether this event was later matched to a log
add :matched, :boolean, null: false, default: false
# ID of the email log it was matched to (if any)
add :matched_email_log_id, :integer, null: true
# When it was matched
add :matched_at, :utc_datetime_usec, null: true
# Error details if processing failed
add :error_message, :text, null: true
timestamps(type: :utc_datetime_usec)
end
# Indexes for orphaned events
create_if_not_exists index(
:phoenix_kit_email_orphaned_events,
[:aws_message_id],
prefix: prefix,
name: :phoenix_kit_orphaned_events_aws_id_idx
)
create_if_not_exists index(
:phoenix_kit_email_orphaned_events,
[:matched],
prefix: prefix,
name: :phoenix_kit_orphaned_events_matched_idx
)
create_if_not_exists index(
:phoenix_kit_email_orphaned_events,
[:event_type, :received_at],
prefix: prefix,
name: :phoenix_kit_orphaned_events_type_received_idx
)
# Create table for email system metrics tracking
create_if_not_exists table(:phoenix_kit_email_metrics, prefix: prefix) do
# Metric name/key
add :metric_key, :string, null: false
# Metric value (counter, gauge, etc.)
add :value, :bigint, null: false, default: 0
# Metric metadata
add :metadata, :map, null: true, default: %{}
# Date for the metric (for daily aggregations)
add :metric_date, :date, null: false, default: fragment("CURRENT_DATE")
timestamps(type: :utc_datetime_usec)
end
# Indexes for metrics
create_if_not_exists unique_index(
:phoenix_kit_email_metrics,
[:metric_key, :metric_date],
prefix: prefix,
name: :phoenix_kit_email_metrics_key_date_uidx
)
create_if_not_exists index(
:phoenix_kit_email_metrics,
[:metric_date],
prefix: prefix,
name: :phoenix_kit_email_metrics_date_idx
)
# Create audit logs table for tracking administrative actions
create_if_not_exists table(:phoenix_kit_audit_logs, prefix: prefix) do
# ID of the user affected by the action
add :target_user_id, :integer, null: false
# ID of the admin who performed the action
add :admin_user_id, :integer, null: false
# Action type (admin_password_reset, user_created, etc.)
add :action, :string, null: false
# IP address from which the action was performed
add :ip_address, :string, null: true
# User agent string of the client
add :user_agent, :text, null: true
# Additional metadata (JSONB for flexibility)
add :metadata, :map, null: true, default: %{}
# Timestamp for when the action occurred (immutable, no updated_at)
timestamps(type: :utc_datetime_usec, updated_at: false)
end
# Create performance indexes for audit logs
# Index for querying logs by target user
create_if_not_exists index(:phoenix_kit_audit_logs, [:target_user_id], prefix: prefix)
# Index for querying logs by admin user
create_if_not_exists index(:phoenix_kit_audit_logs, [:admin_user_id], prefix: prefix)
# Index for querying logs by action type
create_if_not_exists index(:phoenix_kit_audit_logs, [:action], prefix: prefix)
# Index for chronological queries
create_if_not_exists index(:phoenix_kit_audit_logs, [:inserted_at], prefix: prefix)
# Composite indexes for common query patterns
# Query logs for specific user and action type
create_if_not_exists index(:phoenix_kit_audit_logs, [:target_user_id, :action],
prefix: prefix
)
# Query logs by admin and action type
create_if_not_exists index(:phoenix_kit_audit_logs, [:admin_user_id, :action], prefix: prefix)
# Query logs by action and date range
create_if_not_exists index(:phoenix_kit_audit_logs, [:action, :inserted_at], prefix: prefix)
# Set version comment on phoenix_kit table for version tracking
execute "COMMENT ON TABLE #{prefix_table_name("phoenix_kit", prefix)} IS '22'"
end
@doc """
Rollback the V22 migration.
"""
def down(%{prefix: prefix} = _opts) do
# Drop audit logs indexes first
drop_if_exists index(:phoenix_kit_audit_logs, [:action, :inserted_at], prefix: prefix)
drop_if_exists index(:phoenix_kit_audit_logs, [:admin_user_id, :action], prefix: prefix)
drop_if_exists index(:phoenix_kit_audit_logs, [:target_user_id, :action], prefix: prefix)
drop_if_exists index(:phoenix_kit_audit_logs, [:inserted_at], prefix: prefix)
drop_if_exists index(:phoenix_kit_audit_logs, [:action], prefix: prefix)
drop_if_exists index(:phoenix_kit_audit_logs, [:admin_user_id], prefix: prefix)
drop_if_exists index(:phoenix_kit_audit_logs, [:target_user_id], prefix: prefix)
# Drop audit logs table
drop_if_exists table(:phoenix_kit_audit_logs, prefix: prefix)
# Drop metrics table and indexes
drop_if_exists index(
:phoenix_kit_email_metrics,
[:metric_date],
prefix: prefix,
name: :phoenix_kit_email_metrics_date_idx
)
drop_if_exists index(
:phoenix_kit_email_metrics,
[:metric_key, :metric_date],
prefix: prefix,
name: :phoenix_kit_email_metrics_key_date_uidx
)
drop_if_exists table(:phoenix_kit_email_metrics, prefix: prefix)
# Drop orphaned events table and indexes
drop_if_exists index(
:phoenix_kit_email_orphaned_events,
[:event_type, :received_at],
prefix: prefix,
name: :phoenix_kit_orphaned_events_type_received_idx
)
drop_if_exists index(
:phoenix_kit_email_orphaned_events,
[:matched],
prefix: prefix,
name: :phoenix_kit_orphaned_events_matched_idx
)
drop_if_exists index(
:phoenix_kit_email_orphaned_events,
[:aws_message_id],
prefix: prefix,
name: :phoenix_kit_orphaned_events_aws_id_idx
)
drop_if_exists table(:phoenix_kit_email_orphaned_events, prefix: prefix)
# Drop email_events composite index
drop_if_exists index(
:phoenix_kit_email_events,
[:email_log_id, :event_type],
prefix: prefix,
name: :phoenix_kit_email_events_log_type_idx
)
# Drop email_logs composite index
drop_if_exists index(
:phoenix_kit_email_logs,
[:message_id, :aws_message_id],
prefix: prefix,
name: :phoenix_kit_email_logs_message_ids_idx
)
# Drop partial unique index on aws_message_id
drop_if_exists index(
:phoenix_kit_email_logs,
[:aws_message_id],
prefix: prefix,
name: :phoenix_kit_email_logs_aws_message_id_uidx
)
# Remove added columns from email_logs
alter table(:phoenix_kit_email_logs, prefix: prefix) do
remove_if_exists :clicked_at, :utc_datetime_usec
remove_if_exists :opened_at, :utc_datetime_usec
remove_if_exists :complained_at, :utc_datetime_usec
remove_if_exists :bounced_at, :utc_datetime_usec
remove_if_exists :aws_message_id, :string
end
# Update version comment to V21
execute "COMMENT ON TABLE #{prefix_table_name("phoenix_kit", prefix)} IS '21'"
end
# Helper function to build table name with prefix
defp prefix_table_name(table_name, nil), do: table_name
defp prefix_table_name(table_name, prefix), do: "#{prefix}.#{table_name}"
end