Packages
phoenix_kit
1.7.198
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/v141.ex
defmodule PhoenixKit.Migrations.Postgres.V141 do
@moduledoc """
V141: Personal calendar events + participants for the
`phoenix_kit_calendar` module.
One implicit personal calendar per user — events are keyed by
`owner_uuid` (no calendars table in v1; recurrence is deliberately
deferred).
Time model (mirrors `phoenix_live_calendar`'s Event semantics):
- Timed events use the `starts_at`/`ends_at` UTC pair; `ends_at` is
EXCLUSIVE (`[start, end)`, iCal/RFC 5545 style).
- All-day events use the `starts_on`/`ends_on` DATE pair (also
end-exclusive) — proper date semantics instead of UTC-midnight
instants, so a "day" never shifts across timezones/DST.
- A CHECK constraint enforces exactly one pair per row, matching the
`all_day` flag, with end > start on both pairs.
`owner_uuid` cascades on user delete — a personal calendar follows its
account's lifecycle. `location_uuid` optionally links a stored location
from the locations module (loose uuid reference, NO cross-module FK —
the location NAME is snapshotted into the `location` string at save, so
rendering never needs the locations module).
## Participants
`phoenix_kit_calendar_event_participants` attaches people to an event.
Loose `kind` + `target_uuid` references (activity-feed pattern — no
cross-module FKs) with a `display_name` snapshot frozen at save, so
participants render even if a source module is later disabled or the
record deleted. Kinds: `user`, `staff_person`, `crm_contact`,
`crm_company`, `free_text` (free text has no target and grants no
visibility).
Visibility is resolved LIVE at query time by joining the PHYSICAL
staff/CRM tables (they exist in every install via these core
migrations, so no module code is required and empty tables no-op):
a company participant means "whoever is a member of that company NOW",
and a staff person / CRM contact resolves through its current
`user_uuid` link. `added_by_uuid` records who attached the participant.
All statements are idempotent AND additive — this migration was
extended in place while unreleased (per project policy); re-running it
on a database that has the earlier shape adds only the missing pieces.
"""
use Ecto.Migration
def up(opts) do
prefix = Map.get(opts, :prefix, "public")
p = prefix_str(prefix)
execute("""
CREATE TABLE IF NOT EXISTS #{p}phoenix_kit_calendar_events (
uuid UUID PRIMARY KEY DEFAULT #{prefix}.uuid_generate_v7(),
owner_uuid UUID NOT NULL REFERENCES #{p}phoenix_kit_users(uuid) ON DELETE CASCADE,
title VARCHAR(255) NOT NULL,
description TEXT,
location VARCHAR(255),
all_day BOOLEAN NOT NULL DEFAULT FALSE,
starts_at TIMESTAMP(0),
ends_at TIMESTAMP(0),
starts_on DATE,
ends_on DATE,
color VARCHAR(50),
status VARCHAR(20) NOT NULL DEFAULT 'active',
inserted_at TIMESTAMP(0) NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP(0) NOT NULL DEFAULT NOW(),
CONSTRAINT calendar_event_time_shape CHECK (
(
all_day = FALSE
AND starts_at IS NOT NULL AND ends_at IS NOT NULL
AND starts_on IS NULL AND ends_on IS NULL
AND ends_at > starts_at
)
OR
(
all_day = TRUE
AND starts_on IS NOT NULL AND ends_on IS NOT NULL
AND starts_at IS NULL AND ends_at IS NULL
AND ends_on > starts_on
)
),
CONSTRAINT calendar_event_status CHECK (status IN ('active', 'cancelled'))
)
""")
# Status vocabulary: the only meaningful states are active vs cancelled
# (there was never a "tentative"), so 'confirmed' was renamed to 'active'.
# Idempotent + safe for BOTH fresh installs (the drop+re-add nets to the
# same constraint the CREATE TABLE above already made) and existing ones
# (migrates legacy rows and swaps the CHECK). Runs on each up/1.
execute(
"ALTER TABLE #{p}phoenix_kit_calendar_events DROP CONSTRAINT IF EXISTS calendar_event_status"
)
execute(
"UPDATE #{p}phoenix_kit_calendar_events SET status = 'active' WHERE status = 'confirmed'"
)
execute(
"ALTER TABLE #{p}phoenix_kit_calendar_events ALTER COLUMN status SET DEFAULT 'active'"
)
execute("""
ALTER TABLE #{p}phoenix_kit_calendar_events
ADD CONSTRAINT calendar_event_status CHECK (status IN ('active', 'cancelled'))
""")
# Loose link to a stored location (locations module); the name is
# snapshotted into `location`, so this is enrichment only.
execute("""
ALTER TABLE #{p}phoenix_kit_calendar_events
ADD COLUMN IF NOT EXISTS location_uuid UUID
""")
execute("""
CREATE INDEX IF NOT EXISTS idx_calendar_events_owner_starts_at
ON #{p}phoenix_kit_calendar_events (owner_uuid, starts_at)
""")
execute("""
CREATE INDEX IF NOT EXISTS idx_calendar_events_owner_starts_on
ON #{p}phoenix_kit_calendar_events (owner_uuid, starts_on)
""")
execute("""
CREATE TABLE IF NOT EXISTS #{p}phoenix_kit_calendar_event_participants (
uuid UUID PRIMARY KEY DEFAULT #{prefix}.uuid_generate_v7(),
event_uuid UUID NOT NULL REFERENCES #{p}phoenix_kit_calendar_events(uuid) ON DELETE CASCADE,
kind VARCHAR(20) NOT NULL,
target_uuid UUID,
display_name VARCHAR(255) NOT NULL,
added_by_uuid UUID REFERENCES #{p}phoenix_kit_users(uuid) ON DELETE SET NULL,
inserted_at TIMESTAMP(0) NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP(0) NOT NULL DEFAULT NOW(),
CONSTRAINT calendar_participant_kind CHECK (
kind IN ('user', 'staff_person', 'crm_contact', 'crm_company', 'free_text')
),
CONSTRAINT calendar_participant_shape CHECK (
(kind = 'free_text' AND target_uuid IS NULL)
OR (kind <> 'free_text' AND target_uuid IS NOT NULL)
)
)
""")
# One row per (event, kind, target); free-text dedups case-insensitively
execute("""
CREATE UNIQUE INDEX IF NOT EXISTS idx_calendar_participants_target
ON #{p}phoenix_kit_calendar_event_participants (event_uuid, kind, target_uuid)
WHERE target_uuid IS NOT NULL
""")
execute("""
CREATE UNIQUE INDEX IF NOT EXISTS idx_calendar_participants_free_text
ON #{p}phoenix_kit_calendar_event_participants (event_uuid, LOWER(display_name))
WHERE kind = 'free_text'
""")
execute("""
CREATE INDEX IF NOT EXISTS idx_calendar_participants_event
ON #{p}phoenix_kit_calendar_event_participants (event_uuid)
""")
# Reverse direction: "which events does this person participate in" —
# drives the live visibility resolution on personal calendars
execute("""
CREATE INDEX IF NOT EXISTS idx_calendar_participants_kind_target
ON #{p}phoenix_kit_calendar_event_participants (kind, target_uuid)
WHERE target_uuid IS NOT NULL
""")
execute("COMMENT ON TABLE #{p}phoenix_kit IS '141'")
end
def down(opts) do
prefix = Map.get(opts, :prefix, "public")
p = prefix_str(prefix)
execute("DROP TABLE IF EXISTS #{p}phoenix_kit_calendar_event_participants")
execute("DROP TABLE IF EXISTS #{p}phoenix_kit_calendar_events")
execute("COMMENT ON TABLE #{p}phoenix_kit IS '140'")
end
defp prefix_str("public"), do: "public."
defp prefix_str(prefix), do: "#{prefix}."
end