89 lines
3.7 KiB
Python
89 lines
3.7 KiB
Python
"""Letting somebody turn a notification off.
|
|||
|
|
|
||
|
|
Notifications are addressed to a person, deduplicated, and raised only for things
|
||
|
|
worth raising — and none of that helps if the one you get every day is the one
|
||
|
|
you do not want. A bell somebody has learned to ignore is worse than no bell,
|
||
|
|
because it also hides the notice that mattered.
|
||
|
|
|
||
|
|
## Why a row means "changed from the default"
|
||
|
|
|
||
|
|
The absence of a row means **enabled**. Nothing is written when somebody accepts
|
||
|
|
the defaults, which is almost everybody, so the table stays roughly the size of
|
||
|
|
the number of people who have actually expressed an opinion.
|
||
|
|
|
||
|
|
The alternative — a row per person per kind per channel, written on account
|
||
|
|
creation — is thousands of rows saying "yes, the default" and a migration every
|
||
|
|
time a new kind is added. It also has a worse failure mode: a kind added without
|
||
|
|
backfilling the table is a notification nobody receives, and nothing reports it.
|
||
|
|
|
||
|
|
## Why the channel is part of the key
|
||
|
|
|
||
|
|
"Tell me in the product but do not email me" is the setting people actually want,
|
||
|
|
and it is not expressible without it. Today only the in-app channel exists; email
|
||
|
|
and webhook are named so the column does not have to change when they do.
|
||
|
|
|
||
|
|
Revision ID: f8c3a6e1b72d
|
||
|
|
Revises: e7b2d5c9a14f
|
||
|
|
"""
|
||
|
|
|
||
|
|
from typing import Sequence, Union
|
||
|
|
|
||
|
|
import sqlalchemy as sa
|
||
|
|
from alembic import op
|
||
|
|
from sqlalchemy.dialects.postgresql import UUID
|
||
|
|
|
||
|
|
revision: str = "f8c3a6e1b72d"
|
||
|
|
down_revision: Union[str, Sequence[str], None] = "e7b2d5c9a14f"
|
||
|
|
branch_labels: Union[str, Sequence[str], None] = None
|
||
|
|
depends_on: Union[str, Sequence[str], None] = None
|
||
|
|
|
||
|
|
|
||
|
|
def upgrade() -> None:
|
||
|
|
op.create_table(
|
||
|
|
"notification_preferences",
|
||
|
|
sa.Column("id", UUID(as_uuid=True), primary_key=True,
|
||
|
|
server_default=sa.text("gen_random_uuid()")),
|
||
|
|
sa.Column("tenant_id", UUID(as_uuid=True),
|
||
|
|
sa.ForeignKey("tenants.id", ondelete="CASCADE"), nullable=True),
|
||
|
|
sa.Column("user_id", UUID(as_uuid=True),
|
||
|
|
sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=False),
|
||
|
|
sa.Column("notification_type", sa.String(100), nullable=False),
|
||
|
|
sa.Column("channel", sa.String(50), nullable=False, server_default="in_app"),
|
||
|
|
sa.Column("enabled", sa.Boolean(), nullable=False, server_default=sa.true()),
|
||
|
|
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False,
|
||
|
|
server_default=sa.func.now()),
|
||
|
|
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False,
|
||
|
|
server_default=sa.func.now(), onupdate=sa.func.now()),
|
||
|
|
sa.UniqueConstraint("user_id", "notification_type", "channel",
|
||
|
|
name="uq_notification_preference"),
|
||
|
|
)
|
||
|
|
op.create_index(
|
||
|
|
"ix_notification_preferences_user", "notification_preferences",
|
||
|
|
["user_id", "notification_type"],
|
||
|
|
)
|
||
|
|
|
||
|
|
op.execute("ALTER TABLE notification_preferences ENABLE ROW LEVEL SECURITY")
|
||
|
|
op.execute("ALTER TABLE notification_preferences FORCE ROW LEVEL SECURITY")
|
||
|
|
op.execute(
|
||
|
|
"""
|
||
|
|
CREATE POLICY tenant_isolation ON notification_preferences
|
||
|
|
USING (
|
||
|
|
current_setting('app.bypass_rls', true) = 'on'
|
||
|
|
OR tenant_id IS NULL
|
||
|
|
OR tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::uuid
|
||
|
|
)
|
||
|
|
WITH CHECK (
|
||
|
|
current_setting('app.bypass_rls', true) = 'on'
|
||
|
|
OR tenant_id IS NULL
|
||
|
|
OR tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::uuid
|
||
|
|
)
|
||
|
|
"""
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def downgrade() -> None:
|
||
|
|
op.execute("DROP POLICY IF EXISTS tenant_isolation ON notification_preferences")
|
||
|
|
op.drop_index("ix_notification_preferences_user",
|
||
|
|
table_name="notification_preferences")
|
||
|
|
op.drop_table("notification_preferences")
|