263 lines
8.6 KiB
Markdown
263 lines
8.6 KiB
Markdown
# Sprint 8 — Etapa 2: Repositorios y tipos de dominio
|
|
|
|
> **Tipo:** Backend TypeScript — dominio + repos
|
|
> **Rama:** main
|
|
> **Estimación:** 1.5-2 horas
|
|
> **Dependencias:** Etapa 1 completada ✅ (tabla `areas` y columna `area_id` en `processes` ya existen en DB)
|
|
|
|
---
|
|
|
|
## Objetivo
|
|
|
|
Extender la capa de dominio y repositorios para soportar el nuevo campo `area_id`. Esto es **no destructivo**: se agregan los campos nuevos sin eliminar los viejos (`clientName`, `groupId`), que todavía son usados por la UI hasta que Etapas 3 y 4 los remplacen.
|
|
|
|
Al terminar esta etapa, el dominio conoce `areaId/areaName`, el repositorio lee y escribe `area_id` en Supabase, y el código de UI existente sigue funcionando sin cambios.
|
|
|
|
---
|
|
|
|
## Contexto del código actual
|
|
|
|
Leer estos archivos antes de modificar nada:
|
|
|
|
- `src/domain/types.ts` — `Process` tiene `clientName: string`, `groupId: UUID | null`, `orgId`, `orgName`. Sin `areaId` aún.
|
|
- `src/persistence/supabase/process-repo.ts` — `fromRow()` extrae `clientName` y `groupId`; `toRow()` escribe `client` y `group_id`; `getById()` hace JOIN con `organizations!org_id(name)`.
|
|
- `src/persistence/supabase/group-repo.ts` — repositorio del agrupador viejo `process_groups`. **No tocar** — sigue siendo usado por la Settings page.
|
|
- `src/domain/schemas.ts` — leer el contenido actual antes de modificar.
|
|
|
|
---
|
|
|
|
## Alcance
|
|
|
|
### 1. Actualizar `src/domain/types.ts`
|
|
|
|
**A. Agregar interface `Area`:**
|
|
|
|
```typescript
|
|
export interface Area {
|
|
id: string
|
|
orgId: string
|
|
name: string
|
|
description: string | null
|
|
createdAt: number // timestamp ms, igual que Process.createdAt
|
|
}
|
|
```
|
|
|
|
Agregar después de la interface `ProcessGroup` existente (o en una sección nueva de tipos de organización).
|
|
|
|
**B. Extender interface `Process`:**
|
|
|
|
Agregar `areaId` y `areaName` **después** de `orgName`:
|
|
|
|
```typescript
|
|
orgId: string | null // 🆕 Sprint 7
|
|
orgName?: string | null // 🆕 Sprint 7 — poblado via JOIN en getById
|
|
areaId: string | null // 🆕 Sprint 8 — área de la org (null = sin área)
|
|
areaName?: string | null // 🆕 Sprint 8 — poblado via JOIN en getById
|
|
```
|
|
|
|
**NO eliminar** `clientName: string` ni `groupId: UUID | null` — siguen siendo necesarios hasta Etapas 3-4.
|
|
|
|
### 2. Actualizar `src/domain/schemas.ts`
|
|
|
|
Leer el archivo primero y reportar su contenido.
|
|
|
|
Agregar `areaId` al schema de Process:
|
|
```typescript
|
|
areaId: z.string().uuid().nullable(),
|
|
```
|
|
|
|
**NO eliminar** `clientName` ni `groupId` del schema — todavía se usan.
|
|
|
|
Si `schemas.ts` no tiene un schema de Process (posiblemente solo tiene schemas para formularios), reportarlo antes de modificar.
|
|
|
|
### 3. Crear `src/persistence/supabase/area-repo.ts`
|
|
|
|
Nuevo repositorio para la tabla `areas`. Seguir el mismo patrón que `group-repo.ts`.
|
|
|
|
```typescript
|
|
import { supabase } from '@/lib/supabase'
|
|
import type { Area } from '@/domain/types'
|
|
|
|
function fromRow(row: Record<string, unknown>): Area {
|
|
return {
|
|
id: row.id as string,
|
|
orgId: row.org_id as string,
|
|
name: row.name as string,
|
|
description: (row.description as string | null) ?? null,
|
|
createdAt: new Date(row.created_at as string).getTime(),
|
|
}
|
|
}
|
|
|
|
export const supabaseAreaRepo = {
|
|
// Todas las áreas de una organización, ordenadas por nombre
|
|
async getByOrg(orgId: string): Promise<Area[]> {
|
|
const { data, error } = await supabase
|
|
.from('areas')
|
|
.select('*')
|
|
.eq('org_id', orgId)
|
|
.order('name', { ascending: true })
|
|
if (error) throw error
|
|
return (data ?? []).map(fromRow)
|
|
},
|
|
|
|
// Crear área nueva en una org
|
|
async create(data: { orgId: string; name: string; description?: string }): Promise<Area> {
|
|
const { data: row, error } = await supabase
|
|
.from('areas')
|
|
.insert({
|
|
org_id: data.orgId,
|
|
name: data.name,
|
|
description: data.description ?? null,
|
|
})
|
|
.select('*')
|
|
.single()
|
|
if (error) throw error
|
|
return fromRow(row as Record<string, unknown>)
|
|
},
|
|
|
|
// Actualizar nombre y/o descripción de un área
|
|
async update(id: string, data: { name?: string; description?: string }): Promise<Area> {
|
|
const { data: row, error } = await supabase
|
|
.from('areas')
|
|
.update({
|
|
...(data.name !== undefined && { name: data.name }),
|
|
...(data.description !== undefined && { description: data.description }),
|
|
updated_at: new Date().toISOString(),
|
|
})
|
|
.eq('id', id)
|
|
.select('*')
|
|
.single()
|
|
if (error) throw error
|
|
return fromRow(row as Record<string, unknown>)
|
|
},
|
|
|
|
// Eliminar área. Los procesos con esta área pasan a area_id = null (ON DELETE SET NULL)
|
|
async delete(id: string): Promise<void> {
|
|
const { error } = await supabase.from('areas').delete().eq('id', id)
|
|
if (error) throw error
|
|
},
|
|
}
|
|
```
|
|
|
|
### 4. Actualizar `src/persistence/supabase/process-repo.ts`
|
|
|
|
Cuatro cambios específicos en el archivo existente:
|
|
|
|
**4A — `fromRow()`: extraer `areaId` y `areaName`**
|
|
|
|
Agregar a la función existente, después de la extracción de `org`:
|
|
|
|
```typescript
|
|
// area puede estar presente si el query hizo JOIN con areas (getById lo incluye)
|
|
const area = row.area as { name: string } | null
|
|
```
|
|
|
|
Y en el objeto de retorno, después de `orgName`:
|
|
```typescript
|
|
areaId: (row.area_id as string | null) ?? null,
|
|
areaName: area?.name ?? null,
|
|
```
|
|
|
|
**4B — `toRow()`: incluir `area_id`**
|
|
|
|
Agregar al objeto de retorno de `toRow()`, después de `group_id`:
|
|
```typescript
|
|
area_id: process.areaId,
|
|
```
|
|
|
|
**4C — `updateEditable()`: incluir `area_id`**
|
|
|
|
Agregar al objeto del `.update()`, después de `group_id`:
|
|
```typescript
|
|
area_id: process.areaId,
|
|
```
|
|
|
|
**4D — `getById()`: extender el JOIN para incluir área**
|
|
|
|
Cambiar el select actual:
|
|
```typescript
|
|
// Antes:
|
|
.select('*, org:organizations!org_id(name)')
|
|
|
|
// Después:
|
|
.select('*, org:organizations!org_id(name), area:areas!area_id(name)')
|
|
```
|
|
|
|
**Lo que NO cambia en `process-repo.ts`:**
|
|
- `client: process.clientName` en `toRow()` — permanece
|
|
- `group_id: process.groupId` en `toRow()` — permanece
|
|
- `client: process.clientName` en `updateEditable()` — permanece
|
|
- `group_id: process.groupId` en `updateEditable()` — permanece
|
|
- `fromRow()` sigue extrayendo `clientName` y `groupId` — permanece
|
|
- `getAll()` — no cambia (no hace JOIN con areas, solo devuelve `area_id` como UUID crudo via `select('*')`)
|
|
|
|
---
|
|
|
|
## Tests
|
|
|
|
Agregar al menos 2 tests unitarios para `area-repo.ts`. Seguir el patrón de los tests existentes de repositorios (probablemente con `vi.mock('@/lib/supabase')`).
|
|
|
|
Tests mínimos:
|
|
1. `getByOrg()` devuelve lista correcta de áreas
|
|
2. `create()` inserta y retorna el área creada
|
|
|
|
---
|
|
|
|
## Criterios de validación
|
|
|
|
- [ ] `Area` interface existe en `types.ts`
|
|
- [ ] `Process.areaId` y `Process.areaName` existen en `types.ts` (sin eliminar `clientName`/`groupId`)
|
|
- [ ] `area-repo.ts` existe con las 4 operaciones (getByOrg, create, update, delete)
|
|
- [ ] `process-repo.ts`: `fromRow()` extrae `areaId` y `areaName`, `toRow()` incluye `area_id`, `updateEditable()` incluye `area_id`, `getById()` hace JOIN con `areas`
|
|
- [ ] `npm run build` sin errores TypeScript
|
|
- [ ] `npm run test` sin regresiones + 2 tests nuevos de `area-repo.ts`
|
|
- [ ] Ningún archivo de UI modificado (LibraryPage, WorkspacePage, etc. no cambian)
|
|
|
|
---
|
|
|
|
## Restricciones explícitas
|
|
|
|
- **NO eliminar** `clientName`, `groupId` de `types.ts` — aún usados por UI
|
|
- **NO modificar** `group-repo.ts` — sigue siendo usado por la Settings page
|
|
- **NO modificar** `library-store.ts` — los cambios de store van en Etapa 3
|
|
- **NO modificar** ningún componente React — solo capa de dominio y persistencia
|
|
- **NO agregar** `area_id` al `select('*')` de `getAll()` explícitamente — ya está incluido en el wildcard
|
|
|
|
---
|
|
|
|
## Archivos autorizados a modificar/crear
|
|
|
|
```
|
|
src/
|
|
├── domain/
|
|
│ ├── types.ts ← agregar Area interface + areaId/areaName a Process
|
|
│ └── schemas.ts ← agregar areaId al schema (leer primero)
|
|
└── persistence/
|
|
└── supabase/
|
|
├── area-repo.ts ← crear
|
|
└── process-repo.ts ← 4 cambios puntuales
|
|
```
|
|
|
|
Opcionalmente, un archivo de tests para `area-repo.ts` en `tests/`.
|
|
|
|
---
|
|
|
|
## Commit esperado
|
|
|
|
```
|
|
feat(area-repo): repositorio CRUD de áreas por organización
|
|
feat(process-repo): extender fromRow/toRow/getById con areaId y areaName
|
|
feat(domain): interface Area + campos areaId/areaName en Process
|
|
```
|
|
|
|
---
|
|
|
|
## Reporte esperado
|
|
|
|
1. Contenido actual de `schemas.ts` (antes de modificar)
|
|
2. Diff compacto de `types.ts`
|
|
3. Diff compacto de `process-repo.ts`
|
|
4. Contenido de `area-repo.ts` creado
|
|
5. Resultado de `npm run build` y `npm run test` (con conteo de tests)
|
|
6. Confirmar que `clientName` y `groupId` siguen presentes en `types.ts`
|