Packages
phoenix_kit
1.7.199
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
Current section
Files
lib/phoenix_kit/migrations/postgres/v59.ex
defmodule PhoenixKit.Migrations.Postgres.V59 do
@moduledoc """
V59: Publishing Module — Database Tables
Creates core publishing tables (4 tables) to support the filesystem-to-database
migration of the Publishing module. Social features (likes, views) are deferred
to later migrations.
## Tables
- `phoenix_kit_publishing_groups` — Content groups (blog, faq, legal, etc.)
- `phoenix_kit_publishing_posts` — Posts within groups
- `phoenix_kit_publishing_versions` — Version history per post
- `phoenix_kit_publishing_contents` — Per-language content per version
## Design
- JSONB `data` column on every table for extensibility without future migrations
- Real columns for indexed/queried/FK fields (status, slug, language, dates)
- UUID v7 primary keys defaulting to the schema-qualified `<prefix>.uuid_generate_v7()`
- Dual-write user FKs: `created_by_uuid` (UUID, FK) + `created_by_id` (bigint, no FK)
- All timestamps use `timestamptz` (per V58 standardization)
- One content row per language (mirrors filesystem one-file-per-language model)
- Per-group feature toggles stored in `data` JSONB (comments_enabled, likes_enabled, etc.)
## Idempotency
All CREATE TABLE and CREATE INDEX use IF NOT EXISTS. Safe to re-run.
"""
use Ecto.Migration
def up(%{prefix: prefix} = _opts) do
prefix_str = if prefix && prefix != "public", do: "#{prefix}.", else: ""
# =========================================================================
# Table 1: phoenix_kit_publishing_groups
# =========================================================================
execute("""
CREATE TABLE IF NOT EXISTS #{prefix_str}phoenix_kit_publishing_groups (
uuid UUID PRIMARY KEY DEFAULT #{prefix}.uuid_generate_v7(),
name VARCHAR(255) NOT NULL,
slug VARCHAR(255) NOT NULL,
mode VARCHAR(20) NOT NULL DEFAULT 'timestamp',
position INTEGER NOT NULL DEFAULT 0,
data JSONB NOT NULL DEFAULT '{}',
inserted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
""")
execute("""
CREATE UNIQUE INDEX IF NOT EXISTS idx_publishing_groups_slug
ON #{prefix_str}phoenix_kit_publishing_groups (slug)
""")
# =========================================================================
# Table 2: phoenix_kit_publishing_posts
# =========================================================================
execute("""
CREATE TABLE IF NOT EXISTS #{prefix_str}phoenix_kit_publishing_posts (
uuid UUID PRIMARY KEY DEFAULT #{prefix}.uuid_generate_v7(),
group_id UUID NOT NULL,
slug VARCHAR(500) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'draft',
mode VARCHAR(20) NOT NULL DEFAULT 'timestamp',
primary_language VARCHAR(10) NOT NULL DEFAULT 'en',
published_at TIMESTAMPTZ,
scheduled_at TIMESTAMPTZ,
post_date DATE,
post_time TIME,
created_by_uuid UUID,
created_by_id BIGINT,
updated_by_uuid UUID,
updated_by_id BIGINT,
data JSONB NOT NULL DEFAULT '{}',
inserted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT fk_publishing_posts_group
FOREIGN KEY (group_id)
REFERENCES #{prefix_str}phoenix_kit_publishing_groups(uuid)
ON DELETE CASCADE,
CONSTRAINT fk_publishing_posts_created_by
FOREIGN KEY (created_by_uuid)
REFERENCES #{prefix_str}phoenix_kit_users(uuid)
ON DELETE SET NULL,
CONSTRAINT fk_publishing_posts_updated_by
FOREIGN KEY (updated_by_uuid)
REFERENCES #{prefix_str}phoenix_kit_users(uuid)
ON DELETE SET NULL
)
""")
# Unique constraint: (group_id, slug)
execute("""
CREATE UNIQUE INDEX IF NOT EXISTS idx_publishing_posts_group_slug
ON #{prefix_str}phoenix_kit_publishing_posts (group_id, slug)
""")
# FK index for group_id
execute("""
CREATE INDEX IF NOT EXISTS idx_publishing_posts_group_id
ON #{prefix_str}phoenix_kit_publishing_posts (group_id)
""")
# Filter by status within a group
execute("""
CREATE INDEX IF NOT EXISTS idx_publishing_posts_group_status
ON #{prefix_str}phoenix_kit_publishing_posts (group_id, status)
""")
# Sort by published_at descending within a group
execute("""
CREATE INDEX IF NOT EXISTS idx_publishing_posts_group_published_at
ON #{prefix_str}phoenix_kit_publishing_posts (group_id, published_at DESC)
""")
# Timestamp-mode ordering (partial index — only for posts with post_date)
execute("""
CREATE INDEX IF NOT EXISTS idx_publishing_posts_group_date_time
ON #{prefix_str}phoenix_kit_publishing_posts (group_id, post_date DESC, post_time DESC)
WHERE post_date IS NOT NULL
""")
# Scheduled publishing lookup (partial index — only scheduled posts)
execute("""
CREATE INDEX IF NOT EXISTS idx_publishing_posts_scheduled
ON #{prefix_str}phoenix_kit_publishing_posts (scheduled_at)
WHERE status = 'scheduled'
""")
# FK indexes for user references
execute("""
CREATE INDEX IF NOT EXISTS idx_publishing_posts_created_by
ON #{prefix_str}phoenix_kit_publishing_posts (created_by_uuid)
""")
execute("""
CREATE INDEX IF NOT EXISTS idx_publishing_posts_updated_by
ON #{prefix_str}phoenix_kit_publishing_posts (updated_by_uuid)
""")
# =========================================================================
# Table 3: phoenix_kit_publishing_versions
# =========================================================================
execute("""
CREATE TABLE IF NOT EXISTS #{prefix_str}phoenix_kit_publishing_versions (
uuid UUID PRIMARY KEY DEFAULT #{prefix}.uuid_generate_v7(),
post_id UUID NOT NULL,
version_number INTEGER NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'draft',
created_by_uuid UUID,
created_by_id BIGINT,
data JSONB NOT NULL DEFAULT '{}',
inserted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT fk_publishing_versions_post
FOREIGN KEY (post_id)
REFERENCES #{prefix_str}phoenix_kit_publishing_posts(uuid)
ON DELETE CASCADE,
CONSTRAINT fk_publishing_versions_created_by
FOREIGN KEY (created_by_uuid)
REFERENCES #{prefix_str}phoenix_kit_users(uuid)
ON DELETE SET NULL
)
""")
# Unique constraint: (post_id, version_number)
execute("""
CREATE UNIQUE INDEX IF NOT EXISTS idx_publishing_versions_post_number
ON #{prefix_str}phoenix_kit_publishing_versions (post_id, version_number)
""")
# FK index for post_id
execute("""
CREATE INDEX IF NOT EXISTS idx_publishing_versions_post_id
ON #{prefix_str}phoenix_kit_publishing_versions (post_id)
""")
# Filter by status within a post
execute("""
CREATE INDEX IF NOT EXISTS idx_publishing_versions_post_status
ON #{prefix_str}phoenix_kit_publishing_versions (post_id, status)
""")
# FK index for created_by
execute("""
CREATE INDEX IF NOT EXISTS idx_publishing_versions_created_by
ON #{prefix_str}phoenix_kit_publishing_versions (created_by_uuid)
""")
# =========================================================================
# Table 4: phoenix_kit_publishing_contents
# =========================================================================
execute("""
CREATE TABLE IF NOT EXISTS #{prefix_str}phoenix_kit_publishing_contents (
uuid UUID PRIMARY KEY DEFAULT #{prefix}.uuid_generate_v7(),
version_id UUID NOT NULL,
language VARCHAR(10) NOT NULL,
title VARCHAR(500) NOT NULL,
content TEXT,
status VARCHAR(20) NOT NULL DEFAULT 'draft',
url_slug VARCHAR(500),
data JSONB NOT NULL DEFAULT '{}',
inserted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT fk_publishing_contents_version
FOREIGN KEY (version_id)
REFERENCES #{prefix_str}phoenix_kit_publishing_versions(uuid)
ON DELETE CASCADE
)
""")
# Unique constraint: (version_id, language)
execute("""
CREATE UNIQUE INDEX IF NOT EXISTS idx_publishing_contents_version_language
ON #{prefix_str}phoenix_kit_publishing_contents (version_id, language)
""")
# FK index for version_id
execute("""
CREATE INDEX IF NOT EXISTS idx_publishing_contents_version_id
ON #{prefix_str}phoenix_kit_publishing_contents (version_id)
""")
# Per-language URL slug lookup (partial index — only rows with custom url_slug)
execute("""
CREATE INDEX IF NOT EXISTS idx_publishing_contents_url_slug
ON #{prefix_str}phoenix_kit_publishing_contents (url_slug)
WHERE url_slug IS NOT NULL
""")
# GIN index for JSONB @> queries (e.g. previous_url_slugs redirect lookup)
execute("""
CREATE INDEX IF NOT EXISTS idx_publishing_contents_data_gin
ON #{prefix_str}phoenix_kit_publishing_contents USING GIN (data)
""")
# =========================================================================
# Seed default settings
# =========================================================================
execute("""
INSERT INTO #{prefix_str}phoenix_kit_settings (key, value, date_added, date_updated)
VALUES ('publishing_storage', 'filesystem', NOW(), NOW())
ON CONFLICT (key) DO NOTHING
""")
# Record migration version
execute("COMMENT ON TABLE #{prefix_str}phoenix_kit IS '59'")
end
def down(%{prefix: prefix} = _opts) do
prefix_str = if prefix && prefix != "public", do: "#{prefix}.", else: ""
# Drop in reverse order (contents → versions → posts → groups)
execute("DROP TABLE IF EXISTS #{prefix_str}phoenix_kit_publishing_contents CASCADE")
execute("DROP TABLE IF EXISTS #{prefix_str}phoenix_kit_publishing_versions CASCADE")
execute("DROP TABLE IF EXISTS #{prefix_str}phoenix_kit_publishing_posts CASCADE")
execute("DROP TABLE IF EXISTS #{prefix_str}phoenix_kit_publishing_groups CASCADE")
# Remove setting
execute("""
DELETE FROM #{prefix_str}phoenix_kit_settings
WHERE key = 'publishing_storage'
""")
execute("COMMENT ON TABLE #{prefix_str}phoenix_kit IS '58'")
end
end