feat(09-01): add owner_user_id to notification_channels + event-key and subscription tables
- migrations/085: ALTER TABLE notification_channels ADD COLUMN owner_user_id TEXT REFERENCES user(id) ON DELETE CASCADE - migrations/085: partial unique index notification_channels_owner_user_id_channel_type_uniq WHERE owner_user_id IS NOT NULL (UPSERT race defense) - migrations/086: CREATE TABLE notify_event_keys (key PK, display_label, description, sort_order, is_active) with seed row - migrations/086: CREATE TABLE user_event_subscriptions composite PK (user_id, event_key, channel_type) opt-out model - lib/types/pipeline.ts: NotificationChannel gains owner_user_id: string | null - lib/types/pipeline.ts: exports NotifyEventKey and UserEventSubscription interfaces
This commit is contained in:
parent
4fc4a3d3b9
commit
b4f8ccac8b
3 changed files with 100 additions and 0 deletions
|
|
@ -44,6 +44,7 @@ export interface NotificationChannel {
|
|||
channel_type: ChannelType;
|
||||
config: Record<string, any>;
|
||||
is_active: boolean;
|
||||
owner_user_id: string | null;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
}
|
||||
|
|
@ -169,3 +170,25 @@ export interface NotificationChannelInput {
|
|||
config: Record<string, any>;
|
||||
is_active?: boolean;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Phase 9 — Event taxonomy + user subscriptions
|
||||
// ============================================================================
|
||||
|
||||
export interface NotifyEventKey {
|
||||
key: string;
|
||||
display_label: string;
|
||||
description: string | null;
|
||||
sort_order: number;
|
||||
is_active: boolean;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
}
|
||||
|
||||
export interface UserEventSubscription {
|
||||
user_id: string;
|
||||
event_key: string;
|
||||
channel_type: ChannelType;
|
||||
enabled: boolean;
|
||||
updated_at: Date;
|
||||
}
|
||||
|
|
|
|||
31
migrations/085_personal_notification_channels.sql
Normal file
31
migrations/085_personal_notification_channels.sql
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
-- =============================================================================
|
||||
-- Personal notification channels (Phase 9 — CHAN-01)
|
||||
-- =============================================================================
|
||||
-- Adds owner_user_id to notification_channels so a row can be either:
|
||||
-- - Global (owner_user_id IS NULL) — existing rows, admin-managed
|
||||
-- - Personal (owner_user_id = "user".id) — created via /api/me/channels
|
||||
--
|
||||
-- Cascade delete: removing a Better Auth user removes their personal channels.
|
||||
--
|
||||
-- Defense-in-depth: a partial unique index enforces one-personal-channel-per-
|
||||
-- type-per-user at the DB layer (D-02 / CHAN-02). The API-layer WITH-CTE UPSERT
|
||||
-- in Plan 02 still has a small race window for concurrent saves; this index
|
||||
-- closes that gap. The WHERE clause keeps existing global rows (NULL owner)
|
||||
-- exempt — multiple global rows of the same type can still coexist.
|
||||
-- =============================================================================
|
||||
|
||||
ALTER TABLE notification_channels
|
||||
ADD COLUMN IF NOT EXISTS owner_user_id TEXT
|
||||
REFERENCES "user"(id) ON DELETE CASCADE;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_notification_channels_owner
|
||||
ON notification_channels(owner_user_id, channel_type)
|
||||
WHERE owner_user_id IS NOT NULL;
|
||||
|
||||
-- One personal channel per (owner, type). Globals (NULL owner) unaffected.
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS notification_channels_owner_user_id_channel_type_uniq
|
||||
ON notification_channels (owner_user_id, channel_type)
|
||||
WHERE owner_user_id IS NOT NULL;
|
||||
|
||||
COMMENT ON COLUMN notification_channels.owner_user_id IS
|
||||
'Personal channel owner. NULL = global channel (admin-managed). NOT NULL = personal channel reachable only by the owner and admins.';
|
||||
46
migrations/086_notify_event_keys_and_subscriptions.sql
Normal file
46
migrations/086_notify_event_keys_and_subscriptions.sql
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
-- =============================================================================
|
||||
-- Notify event taxonomy + per-user subscriptions (Phase 9 — SUB-01, SUB-02)
|
||||
-- =============================================================================
|
||||
-- notify_event_keys: humanization layer for the event keys that appear in
|
||||
-- pipeline_steps.config->'route_to_user'->>'event_key'. Admin-managed at
|
||||
-- /admin/workflow/event-keys. NOT a gate — keys not in this table are
|
||||
-- still routable; they just render with the raw key as their label.
|
||||
--
|
||||
-- user_event_subscriptions: opt-out matrix per (user, event_key, channel_type).
|
||||
-- Row absence = enabled (D-15 default-enabled / opt-out).
|
||||
-- =============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS notify_event_keys (
|
||||
key TEXT PRIMARY KEY,
|
||||
display_label TEXT NOT NULL,
|
||||
description TEXT,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_notify_event_keys_active
|
||||
ON notify_event_keys(is_active, sort_order, key);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_event_subscriptions (
|
||||
user_id TEXT NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
||||
event_key TEXT NOT NULL,
|
||||
channel_type VARCHAR(20) NOT NULL,
|
||||
enabled BOOLEAN NOT NULL DEFAULT true,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
PRIMARY KEY (user_id, event_key, channel_type)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_event_subs_user
|
||||
ON user_event_subscriptions(user_id);
|
||||
|
||||
COMMENT ON TABLE notify_event_keys IS
|
||||
'Humanization layer for event keys used in notify-step route_to_user blocks. Admins curate display_label/description/sort. Not a gate — unknown keys are still routable.';
|
||||
COMMENT ON TABLE user_event_subscriptions IS
|
||||
'Opt-out matrix. Row absence = enabled. Composite PK enforces one row per (user, event_key, channel_type).';
|
||||
|
||||
-- Seed an example row so the admin UI is not empty on first run.
|
||||
INSERT INTO notify_event_keys (key, display_label, description, sort_order, is_active)
|
||||
VALUES ('ticket_assigned_to_me', 'Ticket assigned to me', 'Fires when an Autotask ticket is assigned to your resource.', 10, true)
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
Loading…
Add table
Add a link
Reference in a new issue