Corrección 1 — Delete de proceso restaurado +
This commit is contained in:
183
src/persistence/supabase/activity-repo.ts
Normal file
183
src/persistence/supabase/activity-repo.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import { supabase } from '@/lib/supabase'
|
||||
import type { Activity, ActivityResourceAssignment } from '@/domain/types'
|
||||
|
||||
function fromRow(
|
||||
row: Record<string, unknown>,
|
||||
assignedResources: ActivityResourceAssignment[],
|
||||
automatedAssignedResources: ActivityResourceAssignment[],
|
||||
): Activity {
|
||||
return {
|
||||
id: row.id as string,
|
||||
processId: row.process_id as string,
|
||||
bpmnElementId: row.bpmn_element_id as string,
|
||||
name: (row.name as string) ?? '',
|
||||
type: (row.type as Activity['type']) ?? 'task',
|
||||
directCostFixed: Number(row.direct_cost_fixed ?? 0),
|
||||
executionTimeMinutes: Number(row.execution_time_minutes ?? 0),
|
||||
automatable: Boolean(row.automatable),
|
||||
automatedCostFixed: Number(row.automated_cost_fixed ?? 0),
|
||||
automatedTimeMinutes: Number(row.automated_time_minutes ?? 0),
|
||||
assignedResources,
|
||||
automatedAssignedResources,
|
||||
}
|
||||
}
|
||||
|
||||
function assignmentFromRow(row: Record<string, unknown>): ActivityResourceAssignment {
|
||||
return {
|
||||
resourceId: row.resource_id as string,
|
||||
utilizationMinutes: Number(row.utilization_minutes ?? 0),
|
||||
units: Number(row.units ?? 1),
|
||||
}
|
||||
}
|
||||
|
||||
// Extrae assignments de la relación anidada que Supabase devuelve con select('*, activity_resource_assignments(*)')
|
||||
function extractAssignments(row: Record<string, unknown>): {
|
||||
actual: ActivityResourceAssignment[]
|
||||
automated: ActivityResourceAssignment[]
|
||||
} {
|
||||
const raw = (row.activity_resource_assignments as Array<Record<string, unknown>>) ?? []
|
||||
return {
|
||||
actual: raw.filter((a) => a.scenario === 'actual').map(assignmentFromRow),
|
||||
automated: raw.filter((a) => a.scenario === 'automated').map(assignmentFromRow),
|
||||
}
|
||||
}
|
||||
|
||||
export const supabaseActivityRepo = {
|
||||
async save(activity: Activity, userId: string): Promise<void> {
|
||||
const { error: actError } = await supabase.from('activities').upsert({
|
||||
id: activity.id,
|
||||
process_id: activity.processId,
|
||||
bpmn_element_id: activity.bpmnElementId,
|
||||
name: activity.name,
|
||||
type: activity.type,
|
||||
direct_cost_fixed: activity.directCostFixed,
|
||||
execution_time_minutes: activity.executionTimeMinutes,
|
||||
automatable: activity.automatable,
|
||||
automated_cost_fixed: activity.automatedCostFixed,
|
||||
automated_time_minutes: activity.automatedTimeMinutes,
|
||||
updated_by: userId,
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
if (actError) throw actError
|
||||
|
||||
const { error: delError } = await supabase
|
||||
.from('activity_resource_assignments')
|
||||
.delete()
|
||||
.eq('activity_id', activity.id)
|
||||
if (delError) throw delError
|
||||
|
||||
const assignments = [
|
||||
...activity.assignedResources.map((r) => ({
|
||||
activity_id: activity.id,
|
||||
resource_id: r.resourceId,
|
||||
utilization_minutes: r.utilizationMinutes,
|
||||
units: r.units,
|
||||
scenario: 'actual',
|
||||
})),
|
||||
...(activity.automatedAssignedResources ?? []).map((r) => ({
|
||||
activity_id: activity.id,
|
||||
resource_id: r.resourceId,
|
||||
utilization_minutes: r.utilizationMinutes,
|
||||
units: r.units,
|
||||
scenario: 'automated',
|
||||
})),
|
||||
]
|
||||
|
||||
if (assignments.length > 0) {
|
||||
const { error: insError } = await supabase
|
||||
.from('activity_resource_assignments')
|
||||
.insert(assignments)
|
||||
if (insError) throw insError
|
||||
}
|
||||
},
|
||||
|
||||
// Bulk insert optimizado: 3 round-trips totales sin importar el número de actividades
|
||||
async saveMany(activities: Activity[], userId: string): Promise<void> {
|
||||
if (activities.length === 0) return
|
||||
|
||||
const now = new Date().toISOString()
|
||||
|
||||
const { error: actError } = await supabase.from('activities').upsert(
|
||||
activities.map((a) => ({
|
||||
id: a.id,
|
||||
process_id: a.processId,
|
||||
bpmn_element_id: a.bpmnElementId,
|
||||
name: a.name,
|
||||
type: a.type,
|
||||
direct_cost_fixed: a.directCostFixed,
|
||||
execution_time_minutes: a.executionTimeMinutes,
|
||||
automatable: a.automatable,
|
||||
automated_cost_fixed: a.automatedCostFixed,
|
||||
automated_time_minutes: a.automatedTimeMinutes,
|
||||
updated_by: userId,
|
||||
updated_at: now,
|
||||
}))
|
||||
)
|
||||
if (actError) throw actError
|
||||
|
||||
const activityIds = activities.map((a) => a.id)
|
||||
const { error: delError } = await supabase
|
||||
.from('activity_resource_assignments')
|
||||
.delete()
|
||||
.in('activity_id', activityIds)
|
||||
if (delError) throw delError
|
||||
|
||||
const assignments = activities.flatMap((a) => [
|
||||
...a.assignedResources.map((r) => ({
|
||||
activity_id: a.id,
|
||||
resource_id: r.resourceId,
|
||||
utilization_minutes: r.utilizationMinutes,
|
||||
units: r.units,
|
||||
scenario: 'actual',
|
||||
})),
|
||||
...(a.automatedAssignedResources ?? []).map((r) => ({
|
||||
activity_id: a.id,
|
||||
resource_id: r.resourceId,
|
||||
utilization_minutes: r.utilizationMinutes,
|
||||
units: r.units,
|
||||
scenario: 'automated',
|
||||
})),
|
||||
])
|
||||
|
||||
if (assignments.length > 0) {
|
||||
const { error: insError } = await supabase
|
||||
.from('activity_resource_assignments')
|
||||
.insert(assignments)
|
||||
if (insError) throw insError
|
||||
}
|
||||
},
|
||||
|
||||
// Un solo round-trip: activities + assignments anidados en una query
|
||||
async getByProcess(processId: string): Promise<Activity[]> {
|
||||
const { data, error } = await supabase
|
||||
.from('activities')
|
||||
.select('*, activity_resource_assignments(*)')
|
||||
.eq('process_id', processId)
|
||||
if (error) throw error
|
||||
|
||||
return (data ?? []).map((row) => {
|
||||
const { actual, automated } = extractAssignments(row as Record<string, unknown>)
|
||||
return fromRow(row as Record<string, unknown>, actual, automated)
|
||||
})
|
||||
},
|
||||
|
||||
// Un solo round-trip: actividad + assignments anidados
|
||||
async getByBpmnElementId(processId: string, bpmnElementId: string): Promise<Activity | undefined> {
|
||||
const { data, error } = await supabase
|
||||
.from('activities')
|
||||
.select('*, activity_resource_assignments(*)')
|
||||
.eq('process_id', processId)
|
||||
.eq('bpmn_element_id', bpmnElementId)
|
||||
.single()
|
||||
if (error) return undefined
|
||||
|
||||
const { actual, automated } = extractAssignments(data as Record<string, unknown>)
|
||||
return fromRow(data as Record<string, unknown>, actual, automated)
|
||||
},
|
||||
|
||||
async deleteByProcess(processId: string): Promise<void> {
|
||||
// CASCADE elimina activity_resource_assignments automáticamente
|
||||
const { error } = await supabase.from('activities').delete().eq('process_id', processId)
|
||||
if (error) throw error
|
||||
},
|
||||
}
|
||||
55
src/persistence/supabase/gateway-repo.ts
Normal file
55
src/persistence/supabase/gateway-repo.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { supabase } from '@/lib/supabase'
|
||||
import type { GatewayConfig } from '@/domain/types'
|
||||
|
||||
function fromRow(row: Record<string, unknown>): GatewayConfig {
|
||||
return {
|
||||
id: row.id as string,
|
||||
processId: row.process_id as string,
|
||||
bpmnElementId: row.bpmn_element_id as string,
|
||||
gatewayType: row.gateway_type as GatewayConfig['gatewayType'],
|
||||
branches: Array.isArray(row.branches) ? row.branches as GatewayConfig['branches'] : [],
|
||||
}
|
||||
}
|
||||
|
||||
export const supabaseGatewayRepo = {
|
||||
async save(gateway: GatewayConfig): Promise<void> {
|
||||
const { error } = await supabase.from('gateways').upsert({
|
||||
id: gateway.id,
|
||||
process_id: gateway.processId,
|
||||
bpmn_element_id: gateway.bpmnElementId,
|
||||
gateway_type: gateway.gatewayType,
|
||||
branches: gateway.branches,
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
if (error) throw error
|
||||
},
|
||||
|
||||
async saveMany(gateways: GatewayConfig[]): Promise<void> {
|
||||
if (gateways.length === 0) return
|
||||
const { error } = await supabase.from('gateways').upsert(
|
||||
gateways.map((g) => ({
|
||||
id: g.id,
|
||||
process_id: g.processId,
|
||||
bpmn_element_id: g.bpmnElementId,
|
||||
gateway_type: g.gatewayType,
|
||||
branches: g.branches,
|
||||
updated_at: new Date().toISOString(),
|
||||
}))
|
||||
)
|
||||
if (error) throw error
|
||||
},
|
||||
|
||||
async getByProcess(processId: string): Promise<GatewayConfig[]> {
|
||||
const { data, error } = await supabase
|
||||
.from('gateways')
|
||||
.select('*')
|
||||
.eq('process_id', processId)
|
||||
if (error) throw error
|
||||
return (data ?? []).map(fromRow)
|
||||
},
|
||||
|
||||
async deleteByProcess(processId: string): Promise<void> {
|
||||
const { error } = await supabase.from('gateways').delete().eq('process_id', processId)
|
||||
if (error) throw error
|
||||
},
|
||||
}
|
||||
@@ -66,14 +66,12 @@ export const supabaseProcessRepo = {
|
||||
},
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
// CASCADE en Supabase elimina: activities, gateways, activity_resource_assignments
|
||||
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()
|
||||
// TODO Sprint 4 Etapa 4: eliminar cuando simulaciones migren a Supabase
|
||||
await db.transaction('rw', [db.simulations], async () => {
|
||||
await db.simulations.where('processId').equals(id).delete()
|
||||
})
|
||||
},
|
||||
|
||||
40
src/persistence/supabase/resource-repo.ts
Normal file
40
src/persistence/supabase/resource-repo.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { supabase } from '@/lib/supabase'
|
||||
import type { Resource } from '@/domain/types'
|
||||
|
||||
function fromRow(row: Record<string, unknown>): Resource {
|
||||
return {
|
||||
id: row.id as string,
|
||||
name: row.name as string,
|
||||
type: row.type as Resource['type'],
|
||||
costPerHour: Number(row.cost_per_hour ?? 0),
|
||||
}
|
||||
}
|
||||
|
||||
export const supabaseResourceRepo = {
|
||||
async save(resource: Resource, userId: string): Promise<void> {
|
||||
const { error } = await supabase.from('resources').upsert({
|
||||
id: resource.id,
|
||||
name: resource.name,
|
||||
type: resource.type,
|
||||
cost_per_hour: resource.costPerHour,
|
||||
created_by: userId,
|
||||
updated_by: userId,
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
if (error) throw error
|
||||
},
|
||||
|
||||
async getAll(): Promise<Resource[]> {
|
||||
const { data, error } = await supabase
|
||||
.from('resources')
|
||||
.select('*')
|
||||
.order('name', { ascending: true })
|
||||
if (error) throw error
|
||||
return (data ?? []).map(fromRow)
|
||||
},
|
||||
|
||||
async delete(resourceId: string): Promise<void> {
|
||||
const { error } = await supabase.from('resources').delete().eq('id', resourceId)
|
||||
if (error) throw error
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user