Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion lib/database.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,29 +37,35 @@ export type Database = {
engagement_sessions: {
Row: {
device: string | null
focused_seconds: number
id: string
last_seen_at: string
link_clicked: boolean
max_scroll_pct: number
post_id: string
started_at: string
status: string
user_id: string
}
Insert: {
device?: string | null
focused_seconds?: number
id?: string
last_seen_at?: string
link_clicked?: boolean
max_scroll_pct?: number
post_id: string
started_at?: string
status?: string
user_id: string
}
Update: {
device?: string | null
focused_seconds?: number
id?: string
last_seen_at?: string
link_clicked?: boolean
max_scroll_pct?: number
post_id?: string
started_at?: string
status?: string
Expand Down Expand Up @@ -564,4 +570,3 @@ export const Constants = {
},
},
} as const

Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
-- Migration: alter engagement_sessions — alineación de status + columnas de comportamiento
-- Part of Epic N04 / Feature F-N04-02 (#174) / Issue I-F-N04-02-01 (#178)
-- refs: docs/adr/0001-engagement.md (status viewed/engaged/clicked)
-- docs/adr/0006-engagement-behavioral-signals.md (columnas aditivas)
--
-- Cambio puramente ADITIVO sobre el esquema restaurado (20260618500000):
-- 1. Alinea `status` de active/idle/closed → viewed/engaged/clicked (canónico, ADR-001).
-- 2. Añade columnas opcionales `focused_seconds` y `max_scroll_pct` (ADR-0006).
-- Mantiene `id` PK, `unique (user_id, post_id)` y la FK `user_id → auth.users(id)`.
-- No dropea la tabla: las filas existentes quedan válidas (defaults en las nuevas columnas).
--
-- Rollback (no versionado; el repo es forward-only vía `supabase db reset`):
-- alter table public.engagement_sessions
-- drop column max_scroll_pct, drop column focused_seconds;
-- alter table public.engagement_sessions drop constraint engagement_sessions_status_check;
-- alter table public.engagement_sessions alter column status set default 'active';
-- alter table public.engagement_sessions
-- add constraint engagement_sessions_status_check check (status in ('active','idle','closed'));

-- 1. Alinear el status al modelo canónico ADR-001 (viewed/engaged/clicked)
alter table public.engagement_sessions
drop constraint if exists engagement_sessions_status_check;

update public.engagement_sessions
set status = 'viewed'
where status not in ('viewed', 'engaged', 'clicked');

alter table public.engagement_sessions
alter column status set default 'viewed';

alter table public.engagement_sessions
add constraint engagement_sessions_status_check
check (status in ('viewed', 'engaged', 'clicked'));

-- 2. Señales de comportamiento OPCIONALES y aditivas (ADR-0006).
-- Default 0: no rompen filas previas ni gobiernan el status.
alter table public.engagement_sessions
add column if not exists focused_seconds integer not null default 0
check (focused_seconds >= 0),
add column if not exists max_scroll_pct numeric(4,3) not null default 0
check (max_scroll_pct >= 0 and max_scroll_pct <= 1);

comment on column public.engagement_sessions.link_clicked is
'Métrica principal: el usuario clicó el enlace externo. Append-only (nunca true→false).';
comment on column public.engagement_sessions.focused_seconds is
'OPCIONAL (ADR-0006): segundos de foco in-app acumulados. No gobierna status.';
comment on column public.engagement_sessions.max_scroll_pct is
'OPCIONAL (ADR-0006): scroll máximo de la sesión, monotónico ∈ [0,1]. No gobierna status.';
152 changes: 152 additions & 0 deletions supabase/tests/rls/schema_engagement_sessions.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
-- Schema/constraints tests: engagement_sessions (modelo aditivo)
-- Issue I-F-N04-02-01 (#178) — migración 20260710000000_alter_engagement_sessions_additive
-- refs: docs/adr/0001-engagement.md · docs/adr/0006-engagement-behavioral-signals.md
--
-- Cubre la alineación de `status` (viewed/engaged/clicked) y las columnas aditivas
-- `focused_seconds` / `max_scroll_pct`, sus defaults, CHECKs, UPSERT y cascades.
--
-- Seed UUIDs (supabase/seed.sql):
-- admin: aaaaaaaa-0000-0000-0000-000000000001
-- manager: aaaaaaaa-0000-0000-0000-000000000002
-- staff: aaaaaaaa-0000-0000-0000-000000000003

begin;
select plan(19);

-- ── Estructura de columnas ──────────────────────────────────────────────────
select has_column('public', 'engagement_sessions', 'link_clicked', 'existe link_clicked');
select has_column('public', 'engagement_sessions', 'status', 'existe status');
select has_column('public', 'engagement_sessions', 'focused_seconds', 'existe focused_seconds (ADR-0006)');
select has_column('public', 'engagement_sessions', 'max_scroll_pct', 'existe max_scroll_pct (ADR-0006)');

select col_type_is('public', 'engagement_sessions', 'focused_seconds', 'integer', 'focused_seconds es integer');
select col_type_is('public', 'engagement_sessions', 'max_scroll_pct', 'numeric(4,3)', 'max_scroll_pct es numeric(4,3)');

-- ── Defaults ────────────────────────────────────────────────────────────────
select col_default_is('public', 'engagement_sessions', 'link_clicked', 'false', 'link_clicked default false');
select col_default_is('public', 'engagement_sessions', 'status', 'viewed', 'status default viewed (ADR-001)');
select col_default_is('public', 'engagement_sessions', 'focused_seconds', '0', 'focused_seconds default 0');
select col_default_is('public', 'engagement_sessions', 'max_scroll_pct', '0', 'max_scroll_pct default 0');

-- ── Legacy fuera: el modelo reading-session revertido (#226) no debe reaparecer ─
select hasnt_column('public', 'engagement_sessions', 'session_id', 'no existe session_id (modelo revertido)');
select hasnt_column('public', 'engagement_sessions', 'state', 'no existe state (modelo revertido)');

-- ── Fixtures (como postgres = bypass RLS) ───────────────────────────────────
insert into public.posts (id, author_id, title, external_url)
select
'aaaaaaaa-1111-0000-0000-000000000001'::uuid,
p.id,
'Post for engagement schema test',
'https://example.com/test'
from public.profiles p where p.user_id = 'aaaaaaaa-0000-0000-0000-000000000002'::uuid;

-- ── CHECK de status: solo viewed/engaged/clicked ────────────────────────────
select throws_ok(
$test$
insert into public.engagement_sessions (user_id, post_id, status)
values ('aaaaaaaa-0000-0000-0000-000000000003'::uuid,
'aaaaaaaa-1111-0000-0000-000000000001'::uuid, 'active')
$test$,
'23514',
null,
'status rechaza el valor legacy active (solo viewed/engaged/clicked)'
);

-- ── CHECK de columnas aditivas ──────────────────────────────────────────────
select throws_ok(
$test$
insert into public.engagement_sessions (user_id, post_id, max_scroll_pct)
values ('aaaaaaaa-0000-0000-0000-000000000003'::uuid,
'aaaaaaaa-1111-0000-0000-000000000001'::uuid, 1.5)
$test$,
'23514',
null,
'max_scroll_pct = 1.5 viola el CHECK ∈ [0,1]'
);

select throws_ok(
$test$
insert into public.engagement_sessions (user_id, post_id, focused_seconds)
values ('aaaaaaaa-0000-0000-0000-000000000003'::uuid,
'aaaaaaaa-1111-0000-0000-000000000001'::uuid, -1)
$test$,
'23514',
null,
'focused_seconds = -1 viola el CHECK >= 0'
);

-- ── UPSERT on conflict (user_id, post_id): acumula sin crear filas duplicadas ─
insert into public.engagement_sessions (user_id, post_id, focused_seconds, max_scroll_pct)
values ('aaaaaaaa-0000-0000-0000-000000000003'::uuid,
'aaaaaaaa-1111-0000-0000-000000000001'::uuid, 5, 0.30)
on conflict (user_id, post_id) do update set
focused_seconds = engagement_sessions.focused_seconds + excluded.focused_seconds,
max_scroll_pct = greatest(engagement_sessions.max_scroll_pct, excluded.max_scroll_pct);

insert into public.engagement_sessions (user_id, post_id, focused_seconds, max_scroll_pct)
values ('aaaaaaaa-0000-0000-0000-000000000003'::uuid,
'aaaaaaaa-1111-0000-0000-000000000001'::uuid, 7, 0.10)
on conflict (user_id, post_id) do update set
focused_seconds = engagement_sessions.focused_seconds + excluded.focused_seconds,
max_scroll_pct = greatest(engagement_sessions.max_scroll_pct, excluded.max_scroll_pct);

select results_eq(
$test$
select focused_seconds, max_scroll_pct
from public.engagement_sessions
where user_id = 'aaaaaaaa-0000-0000-0000-000000000003'::uuid
and post_id = 'aaaaaaaa-1111-0000-0000-000000000001'::uuid
$test$,
$expected$ values (12, 0.300::numeric(4,3)) $expected$,
'UPSERT acumula focused_seconds (5+7=12) y toma el máximo de max_scroll_pct (0.30)'
);

select results_eq(
$test$
select count(*)::int from public.engagement_sessions
where user_id = 'aaaaaaaa-0000-0000-0000-000000000003'::uuid
and post_id = 'aaaaaaaa-1111-0000-0000-000000000001'::uuid
$test$,
$expected$ values (1) $expected$,
'un par (user_id, post_id) = 1 fila tras varios UPSERT'
);

-- ── Cascade al borrar el post ───────────────────────────────────────────────
delete from public.posts where id = 'aaaaaaaa-1111-0000-0000-000000000001'::uuid;

select results_eq(
$test$ select count(*)::int from public.engagement_sessions
where post_id = 'aaaaaaaa-1111-0000-0000-000000000001'::uuid $test$,
$expected$ values (0) $expected$,
'borrar el post elimina sus engagement_sessions (cascade)'
);

-- ── Cascade al borrar el usuario (auth.users) ───────────────────────────────
insert into public.posts (id, author_id, title, external_url)
select
'aaaaaaaa-1111-0000-0000-000000000002'::uuid,
p.id,
'Post for cascade-user test',
'https://example.com/test2'
from public.profiles p where p.user_id = 'aaaaaaaa-0000-0000-0000-000000000002'::uuid;

-- Usuario efímero solo para probar el cascade sobre auth.users
insert into auth.users (id, email)
values ('aaaaaaaa-0000-0000-0000-0000000000ff'::uuid, 'cascade-test@nun-ibiza.dev');

insert into public.engagement_sessions (user_id, post_id)
values ('aaaaaaaa-0000-0000-0000-0000000000ff'::uuid,
'aaaaaaaa-1111-0000-0000-000000000002'::uuid);

delete from auth.users where id = 'aaaaaaaa-0000-0000-0000-0000000000ff'::uuid;

select results_eq(
$test$ select count(*)::int from public.engagement_sessions
where user_id = 'aaaaaaaa-0000-0000-0000-0000000000ff'::uuid $test$,
$expected$ values (0) $expected$,
'borrar el usuario (auth.users) elimina sus engagement_sessions (cascade)'
);

select * from finish();
rollback;
Loading