13 KiB
Sprint 4 — Etapa 2: Schema PostgreSQL + migración de repositorios (procesos + grupos + settings)
Contexto
InQ ROI es una herramienta web de análisis de procesos BPMN y ROI de automatización.
Stack: React 18 + Vite + TypeScript + TanStack Router + Zustand + shadcn/ui + Tailwind.
Etapa 1 (completada): auth con Google, rutas protegidas, tabla users en Supabase, AuthService abstraction.
Esta etapa migra la persistencia de procesos, grupos de biblioteca y settings de IndexedDB/Dexie a Supabase PostgreSQL. Las actividades, gateways, recursos y simulaciones siguen en Dexie hasta Etapas 3-4.
Prerequisitos — verificar antes de empezar
.env.localtieneVITE_SUPABASE_URLyVITE_SUPABASE_ANON_KEY- El script
supabase/migrations/001_create_users.sqlya fue ejecutado en Supabase src/auth/AuthContext.tsxyuseAuth()hook existen y funcionan
Alcance estricto — qué entra en esta etapa
- Scripts SQL para
processes,process_groups,app_settingsensupabase/migrations/ - Nuevos repositorios Supabase para procesos, grupos y settings (reemplazando las implementaciones Dexie de esas entidades)
- Actualizar los stores (
useProcessStore,useLibraryStore) para pasaruserIda los repos en operaciones de escritura - Limpieza de estado híbrido: el
deletede proceso sigue limpiando Dexie (activities, gateways, resources, simulations) hasta que se migren en Etapa 3-4
Qué NO entra en esta etapa
- Migración de actividades, gateways, resources, simulations → Etapa 3
- Tabla
user_groupsygroup_memberships(acceso a usuarios) → Etapa 5 - Tabla
process_access→ Etapa 5 - UI de visibilidad privado/compartido → Etapa 5
- Avatar/nombre en AppHeader → Etapa 6
- Eliminar Dexie del proyecto → Etapa 4 (cuando todo esté migrado)
- Cambiar el tipo TypeScript
Process.clientName— NO modificarsrc/domain/types.ts
Distinción importante: dos tipos de grupos
process_groups (esta etapa): grupos para organizar la biblioteca de procesos — "Cliente X", "Sin clasificar". Corresponde a ProcessGroup en src/domain/types.ts. Son compartidos por todo el equipo, sin control de acceso propio.
user_groups (Etapa 5): grupos de usuarios para control de acceso — "Equipo de consultoría", "Cliente Acme". Concepto nuevo, no existe en Dexie. No crear en esta etapa.
Scripts SQL — crear en supabase/migrations/
002_create_process_groups.sql
CREATE TABLE IF NOT EXISTS public.process_groups (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name text NOT NULL,
created_by uuid REFERENCES public.users(id),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
ALTER TABLE public.process_groups ENABLE ROW LEVEL SECURITY;
-- Todos los autenticados pueden leer y escribir grupos (son recursos compartidos del equipo)
CREATE POLICY "all_process_groups" ON public.process_groups
FOR ALL USING (auth.uid() IS NOT NULL)
WITH CHECK (auth.uid() IS NOT NULL);
003_create_processes.sql
CREATE TABLE IF NOT EXISTS public.processes (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name text NOT NULL,
client text NOT NULL DEFAULT '',
bpmn_xml text NOT NULL DEFAULT '',
currency text NOT NULL DEFAULT 'USD',
overhead_percentage numeric NOT NULL DEFAULT 0,
annual_frequency numeric NOT NULL DEFAULT 1000,
analysis_horizon_years numeric NOT NULL DEFAULT 3,
automation_investment numeric NOT NULL DEFAULT 0,
group_id uuid REFERENCES public.process_groups(id) ON DELETE SET NULL,
tags text[] NOT NULL DEFAULT '{}',
owner_id uuid NOT NULL REFERENCES public.users(id),
created_by uuid NOT NULL REFERENCES public.users(id),
updated_by uuid NOT NULL REFERENCES public.users(id),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
ALTER TABLE public.processes ENABLE ROW LEVEL SECURITY;
-- En Sprint 4 todos los usuarios son platform_admin: ven todo.
-- Las políticas granulares (process_access) se agregan en Etapa 5.
-- Por ahora: cualquier usuario autenticado puede leer y escribir todos los procesos.
CREATE POLICY "authenticated_all_processes" ON public.processes
FOR ALL USING (auth.uid() IS NOT NULL)
WITH CHECK (auth.uid() IS NOT NULL);
004_create_app_settings.sql
-- Settings globales del equipo (singleton compartido)
CREATE TABLE IF NOT EXISTS public.app_settings (
id text PRIMARY KEY DEFAULT 'singleton',
group_label text NOT NULL DEFAULT 'Cliente',
updated_at timestamptz NOT NULL DEFAULT now()
);
ALTER TABLE public.app_settings ENABLE ROW LEVEL SECURITY;
-- Todos los autenticados pueden leer y actualizar settings
CREATE POLICY "all_app_settings" ON public.app_settings
FOR ALL USING (auth.uid() IS NOT NULL)
WITH CHECK (auth.uid() IS NOT NULL);
-- Insertar el singleton con defaults
INSERT INTO public.app_settings (id, group_label)
VALUES ('singleton', 'Cliente')
ON CONFLICT (id) DO NOTHING;
Estos scripts se ejecutan manualmente en Supabase Dashboard → SQL Editor, en orden numérico.
Mapeo de tipos TypeScript ↔ PostgreSQL
Los repositorios Supabase deben mapear entre los tipos del dominio (camelCase, timestamps como number ms Unix) y las columnas de PostgreSQL (snake_case, timestamptz).
NO modificar src/domain/types.ts. El mapeo es responsabilidad del repo.
Process → processes
TypeScript (Process) |
PostgreSQL (processes) |
Notas |
|---|---|---|
id |
id |
igual |
name |
name |
igual |
clientName |
client |
rename solo en DB |
bpmnXml |
bpmn_xml |
snake_case |
currency |
currency |
igual |
overheadPercentage |
overhead_percentage |
snake_case |
annualFrequency |
annual_frequency |
snake_case |
analysisHorizonYears |
analysis_horizon_years |
snake_case |
automationInvestment |
automation_investment |
snake_case |
groupId |
group_id |
snake_case |
tags |
tags |
igual (array) |
createdAt (number ms) |
created_at (timestamptz) |
new Date(createdAt).toISOString() |
updatedAt (number ms) |
updated_at (timestamptz) |
igual conversión |
| — | owner_id |
nuevo campo, inyectado por el store |
| — | created_by |
nuevo campo, inyectado en primer save |
| — | updated_by |
nuevo campo, inyectado en cada save |
Para from_db (PostgreSQL → TypeScript):
createdAt: new Date(row.created_at).getTime()updatedAt: new Date(row.updated_at).getTime()clientName: row.client
ProcessGroup → process_groups
TypeScript (ProcessGroup) |
PostgreSQL (process_groups) |
|---|---|
id |
id |
name |
name |
createdAt (number ms) |
created_at (timestamptz) |
updatedAt (number ms) |
updated_at (timestamptz) |
AppSettings → app_settings
TypeScript (AppSettings) |
PostgreSQL (app_settings) |
|---|---|
id ('singleton') |
id |
groupLabel |
group_label |
Nuevos repositorios — estructura de archivos
Crear src/persistence/supabase/ con:
src/persistence/supabase/
├── process-repo.ts
├── group-repo.ts
└── settings-repo.ts
Cada archivo importa supabase de src/lib/supabase.ts y expone una interfaz idéntica al repo Dexie correspondiente, con la firma extendida donde sea necesario para userId.
src/persistence/supabase/process-repo.ts
// Contrato (misma interface que el repo Dexie, más userId donde aplica)
export const supabaseProcessRepo = {
async save(process: Process, userId: string): Promise<void>
async getById(id: string): Promise<Process | undefined>
async getAll(): Promise<Process[]>
async delete(id: string): Promise<void> // ver nota sobre estado híbrido
}
Nota sobre delete en estado híbrido:
La tabla processes está en Supabase. Las tablas activities, gateways, resources, simulations todavía están en Dexie. El delete debe:
- Eliminar el proceso de Supabase
- Importar
dbdesrc/persistence/db.tsy limpiar las tablas Dexie relacionadas
Es feo pero temporal. Dejar un comentario // TODO Sprint 4 Etapa 3-4: eliminar limpieza Dexie cuando actividades migren a Supabase.
src/persistence/supabase/group-repo.ts
export const supabaseGroupRepo = {
async save(group: ProcessGroup, userId: string): Promise<void>
async getAll(): Promise<ProcessGroup[]>
async getById(id: string): Promise<ProcessGroup | undefined>
async delete(id: string): Promise<void> // debe hacer SET NULL en processes.group_id (la FK ya tiene ON DELETE SET NULL)
}
src/persistence/supabase/settings-repo.ts
export const supabaseSettingsRepo = {
async get(): Promise<AppSettings>
async update(updates: Partial<Omit<AppSettings, 'id'>>): Promise<void>
}
Actualización de stores
Los stores Zustand llaman a los repos. Con el cambio a Supabase, los repos de escritura necesitan el userId. Los stores obtienen el usuario actual del AuthContext.
Patrón recomendado en los stores:
// En el store, acceder al userId desde el contexto de auth
// Los stores son módulos, no React components — no pueden usar hooks directamente.
// Opciones:
// A) Pasar userId como parámetro a las acciones del store que lo necesitan
// B) Leer el usuario del AuthContext en el componente que llama al store y pasarlo
// C) Usar un getter del store de auth
// Opción recomendada: exponer un authStore simple con el userId,
// o pasar userId como parámetro a las acciones de escritura del store.
Claude Code debe elegir el patrón más limpio dado el código existente de los stores. La regla es: ningún componente llama al repo directamente — todo pasa por el store.
Criterios de validación — esta etapa está completa cuando
npm run buildsin errores TypeScriptnpm run test— 561 tests siguen verdes (los tests de dominio no tocan los repos)- En la biblioteca se pueden crear y listar procesos (datos vienen de Supabase, no de Dexie)
- En Supabase Dashboard → Table Editor →
processes: aparecen los procesos creados - En Supabase Dashboard → Table Editor →
processes: las columnasowner_id,created_by,updated_bytienen el UUID del usuario que los creó - Crear un grupo de biblioteca → aparece en Supabase tabla
process_groups - Setting
groupLabel→ aparece/actualiza en Supabase tablaapp_settings - Eliminar un proceso → desaparece de Supabase y sus actividades/gateways/resources/simulations desaparecen de Dexie
- No hay imports directos de la tabla Dexie
processes/groups/settingsfuera dedb.ts(los repos Supabase no usan esas tablas) - Validación visual del director: biblioteca cargando procesos desde Supabase
Reporte esperado al completar
- Lista de archivos creados/modificados
- Resultado de
npm run test(número de tests, pass/fail) - Resultado de
npm run build - Captura de pantalla de la biblioteca con al menos un proceso cargado
- Screenshot de Supabase Table Editor mostrando
processesconowner_idpopulado - Descripción de cómo se resolvió el patrón
userIden los stores - Cualquier decisión de iniciativa fuera del scope (clasificar nivel 1/2/3)
Referencia de archivos existentes
| Archivo | Relevancia |
|---|---|
src/persistence/db.ts |
Schema Dexie actual — referencia para entender el modelo |
src/persistence/repositories.ts |
Repos Dexie actuales — base para los nuevos repos Supabase |
src/domain/types.ts |
Tipos TypeScript — NO modificar |
src/store/ |
Stores Zustand a actualizar |
src/lib/supabase.ts |
Cliente Supabase singleton |
src/auth/AuthContext.tsx |
Fuente del userId actual |
supabase/migrations/001_create_users.sql |
Referencia de estilo para los nuevos scripts |
Notas arquitectónicas
Estado híbrido es intencional
Al terminar esta etapa, la app tiene:
- Procesos, grupos, settings → Supabase PostgreSQL
- Actividades, gateways, recursos, simulaciones → Dexie (hasta Etapa 3-4)
Esto es correcto. No anticipar la migración de las tablas restantes.
Los tests de dominio no se rompen
Los tests en src/domain/ son funciones puras sin persistencia. No dependen de Dexie ni de Supabase. Deben seguir verdes sin cambios.
Los tests de integración que usan los repos directamente (si los hay) pueden necesitar mocks de Supabase — evaluar caso por caso.
Supabase genera updated_at automáticamente
Para que updated_at se actualice automáticamente en cada UPDATE, se puede agregar un trigger PostgreSQL, o simplemente incluir updated_at: new Date().toISOString() en cada UPDATE desde el repo. Usar el approach del repo (más simple, sin trigger en esta etapa).