workspace/projects/gamilit/docs/95-guias-desarrollo/backend/DATABASE-INTEGRATION.md
rckrdmrd ea1879f4ad feat: Initial workspace structure with multi-level Git configuration
- Configure workspace Git repository with comprehensive .gitignore
- Add Odoo as submodule for ERP reference code
- Include documentation: SETUP.md, GIT-STRUCTURE.md
- Add gitignore templates for projects (backend, frontend, database)
- Structure supports independent repos per project/subproject level

Workspace includes:
- core/ - Reusable patterns, modules, orchestration system
- projects/ - Active projects (erp-suite, gamilit, trading-platform, etc.)
- knowledge-base/ - Reference code and patterns (includes Odoo submodule)
- devtools/ - Development tools and templates
- customers/ - Client implementations template

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-08 10:44:23 -06:00

8.6 KiB

Integración con Base de Datos

Versión: 1.0.0 Última Actualización: 2025-11-28 Aplica a: apps/backend/src/ + apps/database/


Resumen

GAMILIT utiliza PostgreSQL con una arquitectura multi-esquema. El backend se conecta mediante TypeORM y aplica Row-Level Security (RLS) para aislamiento de datos por tenant.


Arquitectura de Esquemas

PostgreSQL Database: gamilit_dev
├── auth_management        # Usuarios, roles, sesiones
├── educational_content    # Ejercicios, módulos, contenido
├── gamification_system    # Logros, rangos, ML Coins
├── user_progress          # Progreso, intentos, submissions
├── social_features        # Escuelas, aulas, equipos
├── notification_system    # Notificaciones multicanal
├── audit_logging          # Logs de auditoría
├── admin_dashboard        # Configuración administrativa
└── system_configuration   # Parámetros del sistema

Configuración de TypeORM

Archivo de Configuración

// src/config/typeorm.config.ts
import { DataSourceOptions } from 'typeorm';

export const typeOrmConfig: DataSourceOptions = {
  type: 'postgres',
  host: process.env.DB_HOST || 'localhost',
  port: parseInt(process.env.DB_PORT, 10) || 5432,
  username: process.env.DB_USERNAME,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_DATABASE || 'gamilit_dev',
  entities: [__dirname + '/../**/*.entity{.ts,.js}'],
  synchronize: false, // NUNCA true en producción
  logging: process.env.NODE_ENV === 'development',
};

Registro en AppModule

@Module({
  imports: [
    TypeOrmModule.forRoot(typeOrmConfig),
    // ...
  ],
})
export class AppModule {}

Definición de Entidades

Estructura Básica

// modules/gamification/entities/user-stats.entity.ts
import { Entity, Column, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm';
import { SCHEMAS } from '@shared/constants/database.constants';

@Entity({ name: 'user_stats', schema: SCHEMAS.GAMIFICATION_SYSTEM })
export class UserStatsEntity {
  @PrimaryColumn('uuid')
  id: string;

  @Column('uuid', { name: 'user_id' })
  userId: string;

  @Column('uuid', { name: 'tenant_id' })
  tenantId: string;

  @Column('integer', { name: 'total_xp', default: 0 })
  totalXp: number;

  @Column('integer', { name: 'current_level', default: 1 })
  currentLevel: number;

  @Column('integer', { name: 'ml_coins', default: 0 })
  mlCoins: number;

  @Column('integer', { name: 'current_streak', default: 0 })
  currentStreak: number;

  @Column('timestamp with time zone', { name: 'created_at' })
  createdAt: Date;

  @Column('timestamp with time zone', { name: 'updated_at' })
  updatedAt: Date;
}

Con Relaciones

@Entity({ name: 'user_achievements', schema: SCHEMAS.GAMIFICATION_SYSTEM })
export class UserAchievementEntity {
  @PrimaryColumn('uuid')
  id: string;

  @Column('uuid', { name: 'user_id' })
  userId: string;

  @Column('uuid', { name: 'achievement_id' })
  achievementId: string;

  @ManyToOne(() => AchievementEntity)
  @JoinColumn({ name: 'achievement_id' })
  achievement: AchievementEntity;

  @Column('boolean', { name: 'is_completed', default: false })
  isCompleted: boolean;

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

Row-Level Security (RLS)

Concepto

RLS aísla datos por tenant automáticamente. Cada query solo accede a datos del tenant del usuario autenticado.

Implementación en PostgreSQL

-- Habilitar RLS en tabla
ALTER TABLE gamification_system.user_stats ENABLE ROW LEVEL SECURITY;

-- Política de lectura
CREATE POLICY user_stats_tenant_isolation ON gamification_system.user_stats
  FOR ALL
  USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

Implementación en NestJS

// shared/interceptors/rls.interceptor.ts
@Injectable()
export class RlsInterceptor implements NestInterceptor {
  constructor(private readonly dataSource: DataSource) {}

  async intercept(context: ExecutionContext, next: CallHandler) {
    const request = context.switchToHttp().getRequest();
    const tenantId = request.user?.tenantId;

    if (tenantId) {
      await this.dataSource.query(
        `SET LOCAL app.current_tenant_id = $1`,
        [tenantId]
      );
    }

    return next.handle();
  }
}

Repositorios

Inyección en Servicios

@Injectable()
export class UserStatsService {
  constructor(
    @InjectRepository(UserStatsEntity)
    private readonly userStatsRepository: Repository<UserStatsEntity>,
  ) {}

  async findByUserId(userId: string): Promise<UserStatsEntity | null> {
    return this.userStatsRepository.findOne({
      where: { userId },
    });
  }
}

Registro en Módulo

@Module({
  imports: [
    TypeOrmModule.forFeature([
      UserStatsEntity,
      AchievementEntity,
      UserAchievementEntity,
    ]),
  ],
  providers: [UserStatsService],
})
export class GamificationModule {}

Queries Comunes

QueryBuilder

async findTopUsers(limit: number): Promise<UserStatsEntity[]> {
  return this.userStatsRepository
    .createQueryBuilder('stats')
    .orderBy('stats.totalXp', 'DESC')
    .limit(limit)
    .getMany();
}

Con Joins

async findWithAchievements(userId: string): Promise<UserStatsEntity> {
  return this.userStatsRepository
    .createQueryBuilder('stats')
    .leftJoinAndSelect('stats.achievements', 'ua')
    .leftJoinAndSelect('ua.achievement', 'a')
    .where('stats.userId = :userId', { userId })
    .getOne();
}

Transacciones

async transferCoins(fromId: string, toId: string, amount: number): Promise<void> {
  await this.dataSource.transaction(async (manager) => {
    const fromStats = await manager.findOne(UserStatsEntity, {
      where: { userId: fromId },
    });

    if (fromStats.mlCoins < amount) {
      throw new BadRequestException('Insufficient coins');
    }

    await manager.decrement(UserStatsEntity, { userId: fromId }, 'mlCoins', amount);
    await manager.increment(UserStatsEntity, { userId: toId }, 'mlCoins', amount);
  });
}

Migraciones

Ubicación

Los scripts DDL están en apps/database/ddl/:

apps/database/ddl/
├── 00-prerequisites.sql       # Extensiones y configuración inicial
├── 01-schemas.sql             # Creación de esquemas
└── schemas/
    ├── auth_management/
    │   ├── tables/
    │   ├── functions/
    │   ├── triggers/
    │   └── indexes/
    ├── gamification_system/
    └── ...

Ejecutar Recreación

cd apps/database
./drop-and-recreate-database.sh

Funciones de Base de Datos

Llamar Funciones PostgreSQL

async updateUserRank(userId: string): Promise<void> {
  await this.dataSource.query(
    `SELECT gamification_system.update_user_rank($1)`,
    [userId]
  );
}

async validateAnswer(exerciseId: string, answer: any): Promise<boolean> {
  const [result] = await this.dataSource.query(
    `SELECT educational_content.validate_answer($1, $2) as is_correct`,
    [exerciseId, JSON.stringify(answer)]
  );
  return result.is_correct;
}

Constantes de Base de Datos

// shared/constants/database.constants.ts
export const SCHEMAS = {
  AUTH_MANAGEMENT: 'auth_management',
  EDUCATIONAL_CONTENT: 'educational_content',
  GAMIFICATION_SYSTEM: 'gamification_system',
  USER_PROGRESS: 'user_progress',
  SOCIAL_FEATURES: 'social_features',
  NOTIFICATION_SYSTEM: 'notification_system',
  AUDIT_LOGGING: 'audit_logging',
  ADMIN_DASHBOARD: 'admin_dashboard',
  SYSTEM_CONFIGURATION: 'system_configuration',
} as const;

Buenas Prácticas

  1. Nunca synchronize: true en producción
  2. Usar esquemas explícitos en entidades con schema: SCHEMAS.X
  3. Nombres snake_case para columnas de BD
  4. Nombres camelCase para propiedades TypeScript
  5. Siempre usar transacciones para operaciones múltiples
  6. RLS habilitado en todas las tablas con datos de tenant
  7. Indexes en columnas usadas en WHERE frecuentemente

Troubleshooting

Error: relation does not exist

  • Verificar que el esquema está en el entity decorator
  • Verificar que la tabla existe en la BD

Error: permission denied

  • Verificar RLS policies
  • Verificar que app.current_tenant_id está configurado

Queries lentos

  • Revisar indexes en apps/database/ddl/schemas/*/indexes/
  • Usar EXPLAIN ANALYZE para diagnosticar

Ver También