feat(sprint-4/etapa-2): migración persistencia proceso/grupos/settings a Supabase
- Scripts SQL 002-004: process_groups, processes, app_settings con RLS - supabaseProcessRepo: save/getById/getAll/delete con mapeo camelCase↔snake_case y limpieza Dexie en delete (actividades/gateways/resources/simulations) - supabaseGroupRepo: save/getAll/getById/delete; FK ON DELETE SET NULL en processes - supabaseSettingsRepo: get/update sobre fila singleton - Stores actualizados: library-store y process-store usan repos Supabase para procesos (actividades/gateways/recursos siguen en Dexie hasta Etapa 3-4) - ImportPage y useReportData actualizados a supabaseProcessRepo - userId inyectado internamente en repos via supabase.auth.getSession() — no se threadea por la call stack; stores mantienen interfaces sin cambios - 561/561 tests verdes, build limpio Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
55
src/persistence/supabase/group-repo.ts
Normal file
55
src/persistence/supabase/group-repo.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { supabase } from '@/lib/supabase'
|
||||
import type { ProcessGroup } from '@/domain/types'
|
||||
|
||||
async function getCurrentUserId(): Promise<string> {
|
||||
const { data: { session } } = await supabase.auth.getSession()
|
||||
if (!session?.user) throw new Error('Usuario no autenticado')
|
||||
return session.user.id
|
||||
}
|
||||
|
||||
function fromRow(row: Record<string, unknown>): ProcessGroup {
|
||||
return {
|
||||
id: row.id as string,
|
||||
name: row.name as string,
|
||||
createdAt: new Date(row.created_at as string).getTime(),
|
||||
updatedAt: new Date(row.updated_at as string).getTime(),
|
||||
}
|
||||
}
|
||||
|
||||
export const supabaseGroupRepo = {
|
||||
async save(group: ProcessGroup): Promise<void> {
|
||||
const userId = await getCurrentUserId()
|
||||
const { error } = await supabase.from('process_groups').upsert({
|
||||
id: group.id,
|
||||
name: group.name,
|
||||
created_by: userId,
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
if (error) throw error
|
||||
},
|
||||
|
||||
async getAll(): Promise<ProcessGroup[]> {
|
||||
const { data, error } = await supabase
|
||||
.from('process_groups')
|
||||
.select('*')
|
||||
.order('name', { ascending: true })
|
||||
if (error) throw error
|
||||
return (data ?? []).map(fromRow)
|
||||
},
|
||||
|
||||
async getById(id: string): Promise<ProcessGroup | undefined> {
|
||||
const { data, error } = await supabase
|
||||
.from('process_groups')
|
||||
.select('*')
|
||||
.eq('id', id)
|
||||
.single()
|
||||
if (error) return undefined
|
||||
return fromRow(data)
|
||||
},
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
// La FK group_id en processes tiene ON DELETE SET NULL → procesos pasan a "Sin clasificar"
|
||||
const { error } = await supabase.from('process_groups').delete().eq('id', id)
|
||||
if (error) throw error
|
||||
},
|
||||
}
|
||||
87
src/persistence/supabase/process-repo.ts
Normal file
87
src/persistence/supabase/process-repo.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { supabase } from '@/lib/supabase'
|
||||
import { db } from '@/persistence/db'
|
||||
import type { Process } from '@/domain/types'
|
||||
|
||||
async function getCurrentUserId(): Promise<string> {
|
||||
const { data: { session } } = await supabase.auth.getSession()
|
||||
if (!session?.user) throw new Error('Usuario no autenticado')
|
||||
return session.user.id
|
||||
}
|
||||
|
||||
function toRow(process: Process, userId: string) {
|
||||
return {
|
||||
id: process.id,
|
||||
name: process.name,
|
||||
client: process.clientName,
|
||||
bpmn_xml: process.bpmnXml,
|
||||
currency: process.currency,
|
||||
overhead_percentage: process.overheadPercentage,
|
||||
annual_frequency: process.annualFrequency,
|
||||
analysis_horizon_years: process.analysisHorizonYears,
|
||||
automation_investment: process.automationInvestment,
|
||||
group_id: process.groupId,
|
||||
tags: process.tags,
|
||||
owner_id: userId,
|
||||
created_by: userId,
|
||||
updated_by: userId,
|
||||
updated_at: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
function fromRow(row: Record<string, unknown>): Process {
|
||||
return {
|
||||
id: row.id as string,
|
||||
name: row.name as string,
|
||||
clientName: (row.client as string) ?? '',
|
||||
bpmnXml: (row.bpmn_xml as string) ?? '',
|
||||
currency: (row.currency as string) ?? 'USD',
|
||||
overheadPercentage: Number(row.overhead_percentage ?? 0),
|
||||
annualFrequency: Number(row.annual_frequency ?? 1000),
|
||||
analysisHorizonYears: Number(row.analysis_horizon_years ?? 3),
|
||||
automationInvestment: Number(row.automation_investment ?? 0),
|
||||
groupId: (row.group_id as string | null) ?? null,
|
||||
tags: Array.isArray(row.tags) ? (row.tags as string[]) : [],
|
||||
createdAt: new Date(row.created_at as string).getTime(),
|
||||
updatedAt: new Date(row.updated_at as string).getTime(),
|
||||
}
|
||||
}
|
||||
|
||||
export const supabaseProcessRepo = {
|
||||
async save(process: Process): Promise<void> {
|
||||
const userId = await getCurrentUserId()
|
||||
const { error } = await supabase.from('processes').upsert(toRow(process, userId))
|
||||
if (error) throw error
|
||||
},
|
||||
|
||||
async getById(id: string): Promise<Process | undefined> {
|
||||
const { data, error } = await supabase
|
||||
.from('processes')
|
||||
.select('*')
|
||||
.eq('id', id)
|
||||
.single()
|
||||
if (error) return undefined
|
||||
return fromRow(data)
|
||||
},
|
||||
|
||||
async getAll(): Promise<Process[]> {
|
||||
const { data, error } = await supabase
|
||||
.from('processes')
|
||||
.select('*')
|
||||
.order('updated_at', { ascending: false })
|
||||
if (error) throw error
|
||||
return (data ?? []).map(fromRow)
|
||||
},
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
const { error } = await supabase.from('processes').delete().eq('id', id)
|
||||
if (error) throw error
|
||||
|
||||
// TODO Sprint 4 Etapa 3-4: eliminar limpieza Dexie cuando actividades migren a Supabase
|
||||
await db.transaction('rw', [db.activities, db.gateways, db.resources, db.simulations], async () => {
|
||||
await db.activities.where('processId').equals(id).delete()
|
||||
await db.gateways.where('processId').equals(id).delete()
|
||||
await db.resources.where('processId').equals(id).delete()
|
||||
await db.simulations.where('processId').equals(id).delete()
|
||||
})
|
||||
},
|
||||
}
|
||||
27
src/persistence/supabase/settings-repo.ts
Normal file
27
src/persistence/supabase/settings-repo.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { supabase } from '@/lib/supabase'
|
||||
import type { AppSettings } from '@/domain/types'
|
||||
|
||||
const DEFAULT_SETTINGS: AppSettings = { id: 'singleton', groupLabel: 'Cliente' }
|
||||
|
||||
export const supabaseSettingsRepo = {
|
||||
async get(): Promise<AppSettings> {
|
||||
const { data } = await supabase
|
||||
.from('app_settings')
|
||||
.select('id, group_label')
|
||||
.eq('id', 'singleton')
|
||||
.single()
|
||||
if (!data) return DEFAULT_SETTINGS
|
||||
return { id: 'singleton', groupLabel: (data.group_label as string) ?? 'Cliente' }
|
||||
},
|
||||
|
||||
async update(updates: Partial<Omit<AppSettings, 'id'>>): Promise<void> {
|
||||
const patch: Record<string, unknown> = { updated_at: new Date().toISOString() }
|
||||
if (updates.groupLabel !== undefined) patch.group_label = updates.groupLabel
|
||||
|
||||
const { error } = await supabase
|
||||
.from('app_settings')
|
||||
.update(patch)
|
||||
.eq('id', 'singleton')
|
||||
if (error) throw error
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user