# MAPEO DE TIPOS: PostgreSQL DDL → TypeScript/TypeORM

**Versión:** 1.0.0
**Fecha:** 2025-12-08
**Prioridad:** OBLIGATORIA - Consultar antes de crear Entity
**Sistema:** SIMCO + CAPVED

---

## PROPÓSITO

Este documento define el mapeo EXACTO entre tipos de PostgreSQL y TypeScript/TypeORM para garantizar alineación 100% entre DDL y Entities.

---

## REGLA FUNDAMENTAL

```
╔══════════════════════════════════════════════════════════════════════╗
║  DDL ES LA FUENTE DE VERDAD (DDL-First)                              ║
║                                                                       ║
║  1. DDL define el tipo → Entity lo REFLEJA                           ║
║  2. NUNCA crear Entity sin DDL existente                             ║
║  3. Si DDL cambia → Entity DEBE cambiar                              ║
║  4. Si Entity no coincide → ERROR (corregir Entity, no DDL)          ║
╚══════════════════════════════════════════════════════════════════════╝
```

---

## TABLA DE MAPEO COMPLETA

### Tipos Numéricos

| PostgreSQL | TypeScript | TypeORM Column | Notas |
|------------|------------|----------------|-------|
| `SMALLINT` | `number` | `@Column({ type: 'smallint' })` | -32,768 a 32,767 |
| `INTEGER` | `number` | `@Column({ type: 'integer' })` | -2B a 2B |
| `BIGINT` | `string` | `@Column({ type: 'bigint' })` | Usar string para precisión |
| `DECIMAL(p,s)` | `string` | `@Column({ type: 'decimal', precision: p, scale: s })` | Usar string para dinero |
| `NUMERIC(p,s)` | `string` | `@Column({ type: 'numeric', precision: p, scale: s })` | Igual que DECIMAL |
| `REAL` | `number` | `@Column({ type: 'real' })` | Punto flotante 4 bytes |
| `DOUBLE PRECISION` | `number` | `@Column({ type: 'double precision' })` | Punto flotante 8 bytes |
| `SERIAL` | `number` | `@PrimaryGeneratedColumn()` | Auto-increment |
| `BIGSERIAL` | `string` | `@PrimaryGeneratedColumn('increment')` | Auto-increment grande |

**Ejemplo Numéricos:**
```sql
-- DDL
CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    price DECIMAL(10,2) NOT NULL,
    quantity INTEGER DEFAULT 0,
    rating REAL
);
```

```typescript
// Entity
@Entity({ schema: 'inventory', name: 'products' })
export class ProductEntity {
    @PrimaryGeneratedColumn()
    id: number;

    @Column({ type: 'decimal', precision: 10, scale: 2 })
    price: string;  // String para precisión decimal

    @Column({ type: 'integer', default: 0 })
    quantity: number;

    @Column({ type: 'real', nullable: true })
    rating: number;
}
```

---

### Tipos de Texto

| PostgreSQL | TypeScript | TypeORM Column | Notas |
|------------|------------|----------------|-------|
| `CHAR(n)` | `string` | `@Column({ type: 'char', length: n })` | Longitud fija |
| `VARCHAR(n)` | `string` | `@Column({ type: 'varchar', length: n })` | Longitud variable |
| `TEXT` | `string` | `@Column({ type: 'text' })` | Sin límite |

**Ejemplo Texto:**
```sql
-- DDL
CREATE TABLE users (
    code CHAR(10) NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    bio TEXT
);
```

```typescript
// Entity
@Column({ type: 'char', length: 10 })
code: string;

@Column({ type: 'varchar', length: 255, unique: true })
email: string;

@Column({ type: 'text', nullable: true })
bio: string;
```

---

### Tipos de Fecha y Hora

| PostgreSQL | TypeScript | TypeORM Column | Notas |
|------------|------------|----------------|-------|
| `DATE` | `Date` | `@Column({ type: 'date' })` | Solo fecha |
| `TIME` | `string` | `@Column({ type: 'time' })` | Solo hora |
| `TIMESTAMP` | `Date` | `@Column({ type: 'timestamp' })` | Fecha + hora sin TZ |
| `TIMESTAMPTZ` | `Date` | `@Column({ type: 'timestamptz' })` | Fecha + hora con TZ |
| `INTERVAL` | `string` | `@Column({ type: 'interval' })` | Intervalo de tiempo |

**Ejemplo Fechas:**
```sql
-- DDL
CREATE TABLE events (
    event_date DATE NOT NULL,
    start_time TIME,
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW(),
    duration INTERVAL
);
```

```typescript
// Entity
@Column({ type: 'date' })
eventDate: Date;

@Column({ type: 'time', nullable: true })
startTime: string;

@CreateDateColumn({ type: 'timestamp' })
createdAt: Date;

@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date;

@Column({ type: 'interval', nullable: true })
duration: string;
```

---

### Tipos Booleanos

| PostgreSQL | TypeScript | TypeORM Column | Notas |
|------------|------------|----------------|-------|
| `BOOLEAN` | `boolean` | `@Column({ type: 'boolean' })` | true/false |

**Ejemplo Boolean:**
```sql
-- DDL
CREATE TABLE users (
    is_active BOOLEAN DEFAULT TRUE,
    is_verified BOOLEAN DEFAULT FALSE
);
```

```typescript
// Entity
@Column({ type: 'boolean', default: true })
isActive: boolean;

@Column({ type: 'boolean', default: false })
isVerified: boolean;
```

---

### Tipos UUID

| PostgreSQL | TypeScript | TypeORM Column | Notas |
|------------|------------|----------------|-------|
| `UUID` | `string` | `@Column({ type: 'uuid' })` | Usar con gen_random_uuid() |

**Ejemplo UUID:**
```sql
-- DDL
CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL REFERENCES tenants(id)
);
```

```typescript
// Entity
@PrimaryGeneratedColumn('uuid')
id: string;

@Column({ type: 'uuid' })
tenantId: string;

@ManyToOne(() => TenantEntity)
@JoinColumn({ name: 'tenant_id' })
tenant: TenantEntity;
```

---

### Tipos JSON

| PostgreSQL | TypeScript | TypeORM Column | Notas |
|------------|------------|----------------|-------|
| `JSON` | `object \| any` | `@Column({ type: 'json' })` | Sin indexación |
| `JSONB` | `object \| any` | `@Column({ type: 'jsonb' })` | Con indexación |

**Ejemplo JSON:**
```sql
-- DDL
CREATE TABLE users (
    preferences JSONB DEFAULT '{}',
    metadata JSON
);
```

```typescript
// Entity - Opción 1: any
@Column({ type: 'jsonb', default: {} })
preferences: any;

// Entity - Opción 2: Interface tipada (RECOMENDADO)
interface UserPreferences {
    theme: 'light' | 'dark';
    notifications: boolean;
    language: string;
}

@Column({ type: 'jsonb', default: {} })
preferences: UserPreferences;

@Column({ type: 'json', nullable: true })
metadata: Record<string, any>;
```

---

### Tipos ENUM

| PostgreSQL | TypeScript | TypeORM Column | Notas |
|------------|------------|----------------|-------|
| `CREATE TYPE ... AS ENUM` | `enum` | `@Column({ type: 'enum', enum: X })` | Definir enum TS |
| `VARCHAR CHECK (IN ...)` | `enum \| string` | `@Column()` + validator | Alternativa sin tipo |

**Ejemplo ENUM (Recomendado):**
```sql
-- DDL
CREATE TYPE user_status AS ENUM ('active', 'inactive', 'suspended', 'deleted');

CREATE TABLE users (
    status user_status DEFAULT 'active'
);
```

```typescript
// Entity
export enum UserStatus {
    ACTIVE = 'active',
    INACTIVE = 'inactive',
    SUSPENDED = 'suspended',
    DELETED = 'deleted',
}

@Column({
    type: 'enum',
    enum: UserStatus,
    default: UserStatus.ACTIVE,
})
status: UserStatus;
```

**Ejemplo VARCHAR con CHECK (Alternativa):**
```sql
-- DDL
CREATE TABLE orders (
    status VARCHAR(20) CHECK (status IN ('pending', 'processing', 'completed', 'cancelled'))
);
```

```typescript
// Entity - Usar tipo union + validación en DTO
type OrderStatus = 'pending' | 'processing' | 'completed' | 'cancelled';

@Column({ type: 'varchar', length: 20 })
status: OrderStatus;

// DTO - Validación obligatoria
@IsIn(['pending', 'processing', 'completed', 'cancelled'])
status: string;
```

---

### Tipos Array

| PostgreSQL | TypeScript | TypeORM Column | Notas |
|------------|------------|----------------|-------|
| `INTEGER[]` | `number[]` | `@Column({ type: 'integer', array: true })` | Array de enteros |
| `VARCHAR[]` | `string[]` | `@Column({ type: 'varchar', array: true })` | Array de strings |
| `UUID[]` | `string[]` | `@Column({ type: 'uuid', array: true })` | Array de UUIDs |

**Ejemplo Arrays:**
```sql
-- DDL
CREATE TABLE posts (
    tags VARCHAR(50)[],
    category_ids UUID[]
);
```

```typescript
// Entity
@Column({ type: 'varchar', array: true, nullable: true })
tags: string[];

@Column({ type: 'uuid', array: true, nullable: true })
categoryIds: string[];
```

---

### Tipos Especiales

| PostgreSQL | TypeScript | TypeORM Column | Notas |
|------------|------------|----------------|-------|
| `BYTEA` | `Buffer` | `@Column({ type: 'bytea' })` | Datos binarios |
| `INET` | `string` | `@Column({ type: 'inet' })` | Dirección IP |
| `CIDR` | `string` | `@Column({ type: 'cidr' })` | Red IP |
| `MACADDR` | `string` | `@Column({ type: 'macaddr' })` | Dirección MAC |

---

## MAPEO DE CONSTRAINTS

### NOT NULL

```sql
-- DDL
email VARCHAR(255) NOT NULL
```

```typescript
// Entity
@Column({ type: 'varchar', length: 255, nullable: false })
email: string;  // Sin ? = no nullable
```

### DEFAULT

```sql
-- DDL
created_at TIMESTAMP DEFAULT NOW()
status VARCHAR(20) DEFAULT 'active'
```

```typescript
// Entity
@CreateDateColumn({ type: 'timestamp' })
createdAt: Date;

@Column({ type: 'varchar', length: 20, default: 'active' })
status: string;
```

### UNIQUE

```sql
-- DDL
email VARCHAR(255) UNIQUE
```

```typescript
// Entity
@Column({ type: 'varchar', length: 255, unique: true })
email: string;
```

### CHECK (No soportado directamente en TypeORM)

```sql
-- DDL
age INTEGER CHECK (age >= 0 AND age <= 150)
```

```typescript
// Entity - Sin validación en Entity
@Column({ type: 'integer' })
age: number;

// DTO - OBLIGATORIO validar aquí
@IsInt()
@Min(0)
@Max(150)
age: number;
```

---

## MAPEO DE RELACIONES

### ONE-TO-MANY / MANY-TO-ONE

```sql
-- DDL
CREATE TABLE orders (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE
);
```

```typescript
// Entity Order
@ManyToOne(() => UserEntity, user => user.orders, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'user_id' })
user: UserEntity;

@Column({ type: 'uuid' })
userId: string;

// Entity User
@OneToMany(() => OrderEntity, order => order.user)
orders: OrderEntity[];
```

### MANY-TO-MANY

```sql
-- DDL
CREATE TABLE user_roles (
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
    PRIMARY KEY (user_id, role_id)
);
```

```typescript
// Entity User
@ManyToMany(() => RoleEntity, role => role.users)
@JoinTable({
    name: 'user_roles',
    joinColumn: { name: 'user_id', referencedColumnName: 'id' },
    inverseJoinColumn: { name: 'role_id', referencedColumnName: 'id' },
})
roles: RoleEntity[];

// Entity Role
@ManyToMany(() => UserEntity, user => user.roles)
users: UserEntity[];
```

---

## CHECKLIST DE ALINEACIÓN

Antes de completar Entity, verificar:

```
[ ] Cada columna DDL tiene @Column en Entity
[ ] Tipos PostgreSQL mapeados correctamente (ver tabla)
[ ] nullable: false para NOT NULL
[ ] default: X para DEFAULT X
[ ] unique: true para UNIQUE
[ ] Relaciones (@ManyToOne, etc.) coinciden con FOREIGN KEY
[ ] onDelete coincide con ON DELETE
[ ] Nombre de tabla correcto en @Entity({ name: 'x' })
[ ] Schema correcto en @Entity({ schema: 'x' })
[ ] PK coincide (@PrimaryGeneratedColumn vs @PrimaryColumn)
```

---

## ERRORES COMUNES

| Error | Causa | Solución |
|-------|-------|----------|
| `bigint` retorna string inesperado | BIGINT se mapea a string | Usar `string` en TypeScript |
| Decimales pierden precisión | DECIMAL mapeado a number | Usar `string` para dinero |
| Enum no reconocido | Tipo no creado en BD | Crear TYPE primero en DDL |
| Array vacío falla | Array sin default | Agregar `default: []` |
| Fecha inválida | TIMESTAMP vs TIMESTAMPTZ | Usar TIMESTAMPTZ para TZ |

---

## REFERENCIAS

- SIMCO-DDL.md - Crear tablas
- SIMCO-BACKEND.md - Crear entities
- PRINCIPIO-VALIDACION-OBLIGATORIA.md - Validar alineación

---

**Versión:** 1.0.0 | **Sistema:** SIMCO | **Tipo:** Patrón de Mapeo
