Documentación
Abstracción de Base de Datos

Abstracción de Base de Datos

Fox Framework proporciona una potente capa de abstracción de base de datos que permite interactuar con diferentes motores de bases de datos mediante una API unificada. Esta abstracción facilita el cambio entre bases de datos y simplifica las operaciones comunes.

Características Principales

  • API Unificada: Interactúa con múltiples motores de bases de datos a través de una misma interfaz
  • Query Builder: Construcción de consultas fluida y tipada
  • ORM Integrado: Mapeo objeto-relacional para trabajar con entidades
  • Migraciones: Sistema de migraciones para gestionar esquemas
  • Transacciones: Soporte completo para operaciones transaccionales
  • Conexiones Múltiples: Gestión de múltiples conexiones a diferentes bases de datos
  • Modelos: Definición de modelos con validación integrada
  • Soft Delete: Eliminación suave para preservar datos históricos
  • Caché: Estrategias de caché para optimizar consultas frecuentes
  • TypeScript: Soporte completo para tipos estáticos

Motores Soportados

Fox Framework soporta los siguientes motores de bases de datos:

  • MySQL / MariaDB
  • PostgreSQL
  • SQLite
  • Microsoft SQL Server
  • MongoDB
  • Redis (para caché y almacenamiento clave-valor)

Configuración Básica

Configuración de Conexiones

import { Database, DatabaseConfig } from '@foxframework/database';
 
// Configuración
const dbConfig: DatabaseConfig = {
  // Conexión principal por defecto
  default: 'postgres',
  
  // Definición de conexiones
  connections: {
    // Conexión PostgreSQL
    postgres: {
      driver: 'postgres',
      host: process.env.DB_HOST || 'localhost',
      port: parseInt(process.env.DB_PORT || '5432'),
      database: process.env.DB_NAME || 'myapp',
      username: process.env.DB_USER || 'postgres',
      password: process.env.DB_PASSWORD || 'secret',
      ssl: process.env.DB_SSL === 'true',
      options: {
        pool: {
          min: 2,
          max: 10,
          idleTimeoutMillis: 30000
        }
      }
    },
    
    // Conexión MySQL
    mysql: {
      driver: 'mysql',
      host: process.env.MYSQL_HOST || 'localhost',
      port: parseInt(process.env.MYSQL_PORT || '3306'),
      database: process.env.MYSQL_DB || 'myapp',
      username: process.env.MYSQL_USER || 'root',
      password: process.env.MYSQL_PASSWORD || 'secret',
      options: {
        charset: 'utf8mb4'
      }
    },
    
    // Conexión SQLite para pruebas
    sqlite: {
      driver: 'sqlite',
      database: ':memory:', // o ruta a archivo
      options: {
        useNullAsDefault: true
      }
    },
    
    // Conexión MongoDB
    mongodb: {
      driver: 'mongodb',
      url: process.env.MONGO_URL || 'mongodb://localhost:27017/myapp',
      options: {
        useUnifiedTopology: true
      }
    }
  },
  
  // Opciones globales
  options: {
    // Debug mode
    debug: process.env.DB_DEBUG === 'true',
    // Opciones globales de logging
    log: {
      enabled: true,
      level: 'info', // 'debug', 'info', 'warn', 'error'
      slowQueryThreshold: 1000 // ms
    }
  }
};
 
// Crear instancia de base de datos
const db = new Database(dbConfig);
 
// Inicializar conexiones
await db.initialize();
 
// Exportar para usar en la aplicación
export default db;

Integrando con FoxFactory

import { FoxFactory } from '@foxframework/core';
 
// Crear servidor con integración de base de datos
const server = FoxFactory.createServer({
  port: 3000,
  router,
  
  // Configuración de base de datos
  database: {
    default: 'postgres',
    connections: {
      postgres: {
        driver: 'postgres',
        host: process.env.DB_HOST || 'localhost',
        port: parseInt(process.env.DB_PORT || '5432'),
        database: process.env.DB_NAME || 'myapp',
        username: process.env.DB_USER || 'postgres',
        password: process.env.DB_PASSWORD || 'secret'
      }
    }
  }
});
 
// Acceder a la instancia de base de datos
const db = server.getDatabase();
 
// Iniciar servidor
await server.start();

Query Builder

El Query Builder permite construir consultas SQL de manera fluida y tipada:

Consultas Básicas

import { db } from '../database';
 
// Consultar todos los registros
const users = await db.table('users').get();
 
// Selección específica de columnas
const userNames = await db
  .table('users')
  .select('id', 'name', 'email')
  .get();
 
// Filtrar por condiciones
const activeUsers = await db
  .table('users')
  .where('active', true)
  .get();
 
// Filtros complejos
const filteredUsers = await db
  .table('users')
  .where('age', '>=', 18)
  .where('role', 'user')
  .whereNotNull('email_verified_at')
  .get();
 
// Ordenar resultados
const orderedUsers = await db
  .table('users')
  .orderBy('created_at', 'desc')
  .get();
 
// Paginación
const page = 1;
const pageSize = 10;
 
const pagedUsers = await db
  .table('users')
  .limit(pageSize)
  .offset((page - 1) * pageSize)
  .get();
 
// Contar registros
const totalUsers = await db
  .table('users')
  .count('id', { as: 'total' });
 
// Primera coincidencia
const user = await db
  .table('users')
  .where('email', 'john@example.com')
  .first();
 
// Obtener por ID
const userById = await db
  .table('users')
  .find(1); // Equivalente a .where('id', 1).first()

Consultas Avanzadas

// Joins
const usersWithPosts = await db
  .table('users')
  .select('users.*', 'posts.title', 'posts.content')
  .leftJoin('posts', 'users.id', 'posts.user_id')
  .get();
 
// Group By y Having
const usersByRole = await db
  .table('users')
  .select('role')
  .count('id', { as: 'user_count' })
  .groupBy('role')
  .having('user_count', '>', 5)
  .get();
 
// Subconsultas
const usersWithPostCount = await db
  .table('users')
  .select('users.*')
  .selectSub(
    db.table('posts').select(db.raw('COUNT(*)')).where('user_id', db.ref('users.id')),
    'post_count'
  )
  .get();
 
// Raw Queries
const activeUsersCount = await db.raw('SELECT COUNT(*) FROM users WHERE active = ?', [true]);
 
// Union
const allPeople = await db
  .table('users')
  .select('name', 'email')
  .where('role', 'customer')
  .union(
    db.table('staff')
      .select('name', 'email')
      .where('department', 'sales')
  );

Operaciones de Inserción

// Insertar un registro
const userId = await db
  .table('users')
  .insert({
    name: 'John Doe',
    email: 'john@example.com',
    age: 30,
    created_at: new Date()
  });
 
// Insertar múltiples registros
await db
  .table('users')
  .insert([
    { name: 'John Doe', email: 'john@example.com', age: 30 },
    { name: 'Jane Smith', email: 'jane@example.com', age: 25 }
  ]);
 
// Insertar y devolver el registro completo
const newUser = await db
  .table('users')
  .insertAndGet({
    name: 'John Doe',
    email: 'john@example.com',
    age: 30
  });

Operaciones de Actualización

// Actualizar registros
const updated = await db
  .table('users')
  .where('id', 1)
  .update({
    email: 'new-email@example.com',
    updated_at: new Date()
  });
 
// Actualizar o insertar (upsert)
await db
  .table('settings')
  .upsert(
    { key: 'site_title', value: 'My Awesome Site' },
    'key' // Columna única para determinar si actualizar o insertar
  );
 
// Incrementar/Decrementar valores
await db
  .table('products')
  .where('id', 1)
  .increment('stock', 5);
 
await db
  .table('users')
  .where('id', 1)
  .decrement('credits', 10);
 
// Actualizar con condiciones complejas
await db
  .table('orders')
  .where('status', 'pending')
  .where('created_at', '<', new Date(Date.now() - 86400000)) // Más de 1 día
  .update({ status: 'expired' });

Operaciones de Eliminación

// Eliminar por ID
await db
  .table('users')
  .where('id', 1)
  .delete();
 
// Eliminar con condiciones
await db
  .table('sessions')
  .where('expires_at', '<', new Date())
  .delete();
 
// Soft delete (si está configurado)
await db
  .table('posts')
  .where('id', 1)
  .softDelete();
 
// Restaurar soft deleted
await db
  .table('posts')
  .where('id', 1)
  .restore();
 
// Truncar tabla
await db
  .table('logs')
  .truncate();

ORM

Fox Framework incluye un ORM que permite trabajar con modelos de datos de manera orientada a objetos:

Definición de Modelos

import { Model, field, table } from '@foxframework/database';
 
@table('users')
export class User extends Model {
  // Definición de campos con tipos
  @field({ primaryKey: true, autoIncrement: true })
  id: number;
 
  @field({ required: true })
  name: string;
 
  @field({ required: true, unique: true })
  email: string;
 
  @field()
  age?: number;
 
  @field({ name: 'is_active', default: true })
  active: boolean;
 
  @field()
  createdAt: Date;
 
  @field()
  updatedAt: Date;
 
  // Relaciones
  @hasMany(() => Post)
  posts: Post[];
 
  @hasOne(() => Profile)
  profile: Profile;
 
  @belongsToMany(() => Role, 'user_roles', 'user_id', 'role_id')
  roles: Role[];
 
  // Métodos de instancia
  isAdmin(): boolean {
    return this.roles?.some(role => role.name === 'admin') ?? false;
  }
 
  fullName(): string {
    return `${this.name} (${this.email})`;
  }
 
  // Hooks de ciclo de vida
  @beforeSave()
  setDates() {
    if (!this.id) {
      this.createdAt = new Date();
    }
    this.updatedAt = new Date();
  }
 
  // Métodos estáticos
  static async findByEmail(email: string): Promise<User | null> {
    return this.query()
      .where('email', email)
      .first();
  }
}
 
@table('posts')
export class Post extends Model {
  @field({ primaryKey: true, autoIncrement: true })
  id: number;
 
  @field({ required: true })
  title: string;
 
  @field({ required: true })
  content: string;
 
  @field({ name: 'user_id' })
  userId: number;
 
  @field()
  createdAt: Date;
 
  @field()
  updatedAt: Date;
 
  @field({ name: 'deleted_at' })
  deletedAt: Date | null;
 
  // Relaciones
  @belongsTo(() => User, 'user_id')
  author: User;
 
  @hasMany(() => Comment)
  comments: Comment[];
 
  // Uso de soft delete
  @softDelete('deleted_at')
  static softDelete = true;
}

Uso de Modelos

// Crear una instancia de modelo
const user = new User();
user.name = 'John Doe';
user.email = 'john@example.com';
user.age = 30;
 
// Guardar en la base de datos
await user.save();
 
// Crear y guardar en un solo paso
const user = await User.create({
  name: 'John Doe',
  email: 'john@example.com',
  age: 30
});
 
// Buscar un registro
const user = await User.findById(1);
 
// Query builder con modelos
const users = await User.query()
  .where('age', '>', 18)
  .orderBy('name')
  .get();
 
// First o fail (lanza error si no encuentra)
try {
  const user = await User.query()
    .where('email', 'john@example.com')
    .firstOrFail();
} catch (error) {
  console.error('Usuario no encontrado');
}
 
// Actualizar modelo
user.name = 'John Updated';
await user.save();
 
// Actualizar directamente
await User.query()
  .where('id', 1)
  .update({ name: 'John Updated' });
 
// Eliminar modelo
await user.delete();
 
// Eliminar directamente
await User.query()
  .where('id', 1)
  .delete();
 
// Soft delete
await Post.query()
  .where('id', 1)
  .delete(); // Usa softDelete automáticamente
 
// Incluir soft deleted en consultas
const allPosts = await Post.query()
  .withTrashed()
  .get();
 
// Solo obtener soft deleted
const trashedPosts = await Post.query()
  .onlyTrashed()
  .get();
 
// Restaurar soft deleted
await Post.query()
  .where('id', 1)
  .restore();

Trabajando con Relaciones

// Eager loading (carga ansiosa)
const usersWithPosts = await User.query()
  .with('posts')
  .get();
 
// Múltiples relaciones
const usersWithAll = await User.query()
  .with(['posts', 'profile', 'roles'])
  .get();
 
// Relaciones anidadas
const usersWithPostsAndComments = await User.query()
  .with('posts.comments')
  .get();
 
// Filtrar por relaciones
const usersWithActivePosts = await User.query()
  .whereHas('posts', query => {
    query.where('active', true);
  })
  .get();
 
// Contar relaciones
const usersWithPostCount = await User.query()
  .withCount('posts')
  .get();
 
// Acceder a relaciones
const user = await User.findById(1);
const posts = await user.related('posts').get();
 
// Añadir a relaciones
const user = await User.findById(1);
const post = new Post({
  title: 'New Post',
  content: 'Post content'
});
 
await user.related('posts').save(post);
 
// Relaciones muchos a muchos
const user = await User.findById(1);
const adminRole = await Role.query().where('name', 'admin').first();
 
// Asociar
await user.related('roles').attach(adminRole.id);
 
// Asociar con datos pivote
await user.related('roles').attach(adminRole.id, {
  assigned_at: new Date()
});
 
// Desasociar
await user.related('roles').detach(adminRole.id);
 
// Sincronizar (eliminar otras asociaciones)
await user.related('roles').sync([1, 2, 3]);

Agregaciones y Consultas Avanzadas

// Agregaciones
const stats = await User.query()
  .count('* as total')
  .avg('age as averageAge')
  .min('age as minAge')
  .max('age as maxAge')
  .first();
 
// Consultas avanzadas con modelos
const topUsers = await User.query()
  .select('users.*')
  .selectSub(query => {
    query.from('posts')
      .count('*')
      .whereColumn('user_id', 'users.id');
  }, 'posts_count')
  .orderBy('posts_count', 'desc')
  .limit(10)
  .get();
 
// Subconsultas en where
const usersWithPopularPosts = await User.query()
  .whereExists(query => {
    query.from('posts')
      .whereColumn('posts.user_id', 'users.id')
      .where('posts.views', '>', 1000);
  })
  .get();

Migraciones

El sistema de migraciones permite gestionar la estructura de la base de datos de forma controlada y versionada:

Creación de Migraciones

import { Migration } from '@foxframework/database';
 
export class CreateUsersTable extends Migration {
  // Nombre único de la migración
  name = 'create_users_table';
  
  // Versión para ordenar las migraciones
  version = 1;
 
  // Aplicar migración
  async up(): Promise<void> {
    await this.schema.createTable('users', table => {
      table.increments('id').primary();
      table.string('name').notNull();
      table.string('email').notNull().unique();
      table.integer('age').nullable();
      table.boolean('is_active').default(true);
      table.timestamp('created_at').notNull();
      table.timestamp('updated_at').notNull();
    });
 
    // Crear índices
    await this.schema.table('users', table => {
      table.index('email');
      table.index('created_at');
    });
  }
 
  // Revertir migración
  async down(): Promise<void> {
    await this.schema.dropTable('users');
  }
}
 
export class CreatePostsTable extends Migration {
  name = 'create_posts_table';
  version = 2;
 
  async up(): Promise<void> {
    await this.schema.createTable('posts', table => {
      table.increments('id').primary();
      table.string('title').notNull();
      table.text('content').notNull();
      table.integer('user_id').notNull();
      table.timestamp('created_at').notNull();
      table.timestamp('updated_at').notNull();
      table.timestamp('deleted_at').nullable();
 
      // Clave foránea
      table.foreign('user_id')
        .references('id')
        .on('users')
        .onDelete('CASCADE');
    });
  }
 
  async down(): Promise<void> {
    await this.schema.dropTable('posts');
  }
}

Gestión de Tablas

// Crear tabla
await this.schema.createTable('products', table => {
  // Columnas básicas
  table.increments('id').primary();
  table.string('name').notNull();
  table.text('description').nullable();
  table.decimal('price', 10, 2).notNull();
  table.integer('stock').notNull().default(0);
  
  // Columna enum
  table.enum('status', ['active', 'inactive', 'discontinued']).default('active');
  
  // UUID
  table.uuid('uuid').notNull();
  
  // JSON
  table.json('metadata').nullable();
  
  // Timestamps
  table.timestamp('created_at').notNull();
  table.timestamp('updated_at').notNull();
  table.timestamp('deleted_at').nullable();
});
 
// Modificar tabla existente
await this.schema.table('users', table => {
  // Añadir columnas
  table.string('phone').nullable();
  table.date('birth_date').nullable();
  
  // Modificar columnas
  table.string('name', 100).notNull().alter();
  
  // Renombrar columnas
  table.renameColumn('is_active', 'active');
  
  // Eliminar columnas
  table.dropColumn('temporary_field');
  
  // Índices
  table.unique(['email', 'phone']);
  table.index('birth_date');
  
  // Foreign key
  table.integer('department_id').nullable();
  table.foreign('department_id')
    .references('id')
    .on('departments')
    .onDelete('SET NULL');
});
 
// Eliminar tabla
await this.schema.dropTable('logs');
 
// Comprobar si existe tabla
if (await this.schema.hasTable('users')) {
  // ...
}
 
// Comprobar si existe columna
if (await this.schema.hasColumn('users', 'phone')) {
  // ...
}
 
// Renombrar tabla
await this.schema.renameTable('old_name', 'new_name');

Ejecución de Migraciones

import { MigrationManager } from '@foxframework/database';
import { db } from '../database';
 
// Crear gestor de migraciones
const migrationManager = new MigrationManager({
  db,
  // Directorio donde se encuentran las migraciones
  directory: './src/database/migrations',
  // Tabla para almacenar el historial de migraciones
  tableName: 'migrations'
});
 
// Registrar migraciones (automáticamente carga los archivos del directorio)
await migrationManager.registerMigrations();
 
// Ejecutar todas las migraciones pendientes
await migrationManager.migrate();
 
// Revertir última migración
await migrationManager.rollback();
 
// Revertir todas las migraciones
await migrationManager.reset();
 
// Refrescar: revertir todas y ejecutar de nuevo
await migrationManager.refresh();
 
// Ejecutar hasta una versión específica
await migrationManager.migrateTo(5);
 
// Ver estado de migraciones
const status = await migrationManager.status();
console.log('Migrations status:', status);
 
// Integración con CLI
program
  .command('migrate')
  .description('Run pending migrations')
  .action(async () => {
    try {
      await migrationManager.migrate();
      console.log('Migrations completed successfully');
      process.exit(0);
    } catch (error) {
      console.error('Migration failed:', error);
      process.exit(1);
    }
  });

Semillas (Seeds)

Las semillas permiten poblar la base de datos con datos iniciales o de prueba:

import { Seeder } from '@foxframework/database';
import { User, Role, Post } from '../models';
 
export class UserSeeder extends Seeder {
  name = 'user_seeder';
  
  async run(): Promise<void> {
    // Crear roles
    const roles = await Role.createMany([
      { name: 'admin', description: 'Administrator' },
      { name: 'user', description: 'Regular user' },
      { name: 'guest', description: 'Guest user' }
    ]);
    
    // Crear usuario administrador
    const admin = await User.create({
      name: 'Admin User',
      email: 'admin@example.com',
      password: await this.hash('admin123'),
      createdAt: new Date(),
      updatedAt: new Date()
    });
    
    // Asignar rol de administrador
    await admin.related('roles').attach(roles[0].id);
    
    // Crear múltiples usuarios regulares
    const users = await User.createMany(Array(10).fill(null).map((_, i) => ({
      name: `User ${i+1}`,
      email: `user${i+1}@example.com`,
      password: this.hash(`password${i+1}`),
      createdAt: new Date(),
      updatedAt: new Date()
    })));
    
    // Asignar rol de usuario a todos
    for (const user of users) {
      await user.related('roles').attach(roles[1].id);
    }
    
    // Crear posts para cada usuario
    for (const user of [admin, ...users]) {
      await Post.createMany(Array(3).fill(null).map((_, i) => ({
        title: `Post ${i+1} by ${user.name}`,
        content: `This is the content of post ${i+1} by ${user.name}`,
        userId: user.id,
        createdAt: new Date(),
        updatedAt: new Date()
      })));
    }
  }
}

Ejecución de Seeds

import { SeedManager } from '@foxframework/database';
import { db } from '../database';
 
// Crear gestor de seeds
const seedManager = new SeedManager({
  db,
  directory: './src/database/seeds',
});
 
// Registrar seeds
await seedManager.registerSeeds();
 
// Ejecutar todas las seeds
await seedManager.run();
 
// Ejecutar seed específica
await seedManager.run('user_seeder');
 
// Integración con CLI
program
  .command('seed')
  .description('Run database seeds')
  .option('-s, --seed <name>', 'Run specific seed')
  .action(async (options) => {
    try {
      if (options.seed) {
        await seedManager.run(options.seed);
      } else {
        await seedManager.run();
      }
      console.log('Seeds completed successfully');
      process.exit(0);
    } catch (error) {
      console.error('Seeding failed:', error);
      process.exit(1);
    }
  });

Transacciones

Las transacciones permiten ejecutar múltiples operaciones como una unidad atómica:

import { db } from '../database';
import { User, Order, Product } from '../models';
 
// Función para crear orden
export async function createOrder(userId: number, items: Array<{productId: number, quantity: number}>) {
  // Iniciar transacción
  return await db.transaction(async (trx) => {
    // Cargar usuario
    const user = await User.query(trx)
      .findById(userId);
    
    if (!user) {
      throw new Error('User not found');
    }
    
    // Crear orden
    const order = await Order.query(trx)
      .insert({
        userId,
        status: 'pending',
        totalAmount: 0,
        createdAt: new Date(),
        updatedAt: new Date()
      });
    
    let totalAmount = 0;
    
    // Procesar items
    for (const item of items) {
      // Cargar producto
      const product = await Product.query(trx)
        .findById(item.productId);
      
      if (!product) {
        throw new Error(`Product ${item.productId} not found`);
      }
      
      // Verificar stock
      if (product.stock < item.quantity) {
        throw new Error(`Insufficient stock for product ${product.name}`);
      }
      
      // Actualizar stock
      await Product.query(trx)
        .where('id', product.id)
        .decrement('stock', item.quantity);
      
      // Añadir item a la orden
      await OrderItem.query(trx)
        .insert({
          orderId: order.id,
          productId: product.id,
          quantity: item.quantity,
          price: product.price,
          subtotal: product.price * item.quantity
        });
      
      totalAmount += product.price * item.quantity;
    }
    
    // Actualizar monto total
    await Order.query(trx)
      .where('id', order.id)
      .update({
        totalAmount,
        updatedAt: new Date()
      });
    
    // Si todo está bien, la transacción se commitea automáticamente
    // Si hay error, hace rollback automáticamente
    
    // Cargar orden completa
    return await Order.query(trx)
      .findById(order.id)
      .with('items.product');
  });
}
 
// Uso manual de transacciones
async function transferFunds(fromAccountId: number, toAccountId: number, amount: number) {
  const trx = await db.beginTransaction();
  
  try {
    // Verificar balance
    const fromAccount = await Account.query(trx)
      .where('id', fromAccountId)
      .first();
    
    if (!fromAccount || fromAccount.balance < amount) {
      throw new Error('Insufficient funds');
    }
    
    // Restar del origen
    await Account.query(trx)
      .where('id', fromAccountId)
      .decrement('balance', amount);
    
    // Sumar al destino
    await Account.query(trx)
      .where('id', toAccountId)
      .increment('balance', amount);
    
    // Registrar transacción
    await Transaction.query(trx)
      .insert({
        fromAccountId,
        toAccountId,
        amount,
        type: 'transfer',
        status: 'completed',
        createdAt: new Date()
      });
    
    // Commit
    await trx.commit();
    
    return true;
  } catch (error) {
    // Rollback en caso de error
    await trx.rollback();
    throw error;
  }
}

Sistema de Cache

Fox Framework incluye un sistema de caché para optimizar consultas a la base de datos:

import { db } from '../database';
import { User } from '../models';
 
// Configurar caché para la conexión
db.connection('postgres').cache({
  // Tiempo de vida en ms (1 hora)
  ttl: 3600000,
  
  // Prefijo para keys de caché
  prefix: 'db:postgres:',
  
  // Adaptor de caché (redis, memory, custom)
  adapter: 'redis',
  
  // Opciones para el adaptor
  options: {
    host: process.env.REDIS_HOST || 'localhost',
    port: parseInt(process.env.REDIS_PORT || '6379')
  }
});
 
// Uso de caché en consultas
const popularPosts = await db
  .table('posts')
  .orderBy('views', 'desc')
  .limit(10)
  .cache('popular_posts', 300000); // Clave y TTL (5 minutos)
 
// Caché con modelos
const topUsers = await User.query()
  .withCount('posts')
  .orderBy('posts_count', 'desc')
  .limit(5)
  .cache('top_users');
 
// Invalidar caché específica
await db.cache().forget('popular_posts');
 
// Invalidar caché por tags
await db
  .table('posts')
  .orderBy('created_at', 'desc')
  .limit(10)
  .cache('recent_posts', null, ['posts', 'home']);
 
// Al actualizar posts, invalidar por tag
await db.table('posts').insert({title: 'New Post', content: '...'});
await db.cache().forgetByTags(['posts']);
 
// Invalidar toda la caché
await db.cache().flush();
 
// Cache remember (obtener de caché o ejecutar y guardar)
const stats = await db.cache().remember('site_stats', 3600000, async () => {
  // Esta consulta solo se ejecuta si no existe en caché
  return await db.table('stats').first();
});

Raw SQL y Procedimientos Almacenados

import { db } from '../database';
 
// Ejecutar query SQL crudo
const results = await db.raw('SELECT * FROM users WHERE active = ?', [true]);
 
// En PostgreSQL con parámetros nombrados
const pgResults = await db.raw(
  'SELECT * FROM users WHERE email = :email AND active = :active',
  { email: 'john@example.com', active: true }
);
 
// Ejecutar múltiples consultas en una transacción
await db.transaction(async (trx) => {
  await trx.raw('UPDATE products SET stock = stock - 1 WHERE id = ?', [productId]);
  await trx.raw('INSERT INTO stock_movements (product_id, quantity, type) VALUES (?, ?, ?)', 
    [productId, 1, 'decrease']
  );
});
 
// Procedimientos almacenados (MySQL)
await db.raw('CALL calculate_statistics()');
 
// Procedimientos almacenados con parámetros
await db.raw('CALL transfer_funds(?, ?, ?)', [fromAccount, toAccount, amount]);
 
// Funciones (PostgreSQL)
const result = await db.raw('SELECT get_user_role(?) AS role', [userId]);
const role = result.rows[0].role;

Conexiones Múltiples

import { db } from '../database';
 
// Usar conexión por defecto
const users = await db.table('users').get();
 
// Usar conexión específica
const products = await db.connection('mysql').table('products').get();
 
// Crear modelos para conexión específica
import { Model } from '@foxframework/database';
 
export class Product extends Model {
  static connection = 'mysql';
  
  // Resto de la definición del modelo...
}
 
// Modelo con conexión dinámica
export class Log extends Model {
  static getConnection() {
    // Lógica para determinar qué conexión usar
    return process.env.NODE_ENV === 'production' ? 'logs_prod' : 'logs_dev';
  }
}
 
// Cerrar conexiones
await db.closeConnections();
// O cerrar una específica
await db.connection('mysql').close();

Testing

Fox Framework facilita el testing de operaciones de base de datos:

import { TestDatabase } from '@foxframework/database';
import { User, Post } from '../models';
 
describe('User Service Tests', () => {
  // Crear instancia de base de datos para testing
  const testDb = new TestDatabase({
    driver: 'sqlite',
    database: ':memory:',
    migrationsDirectory: './src/database/migrations',
    seedsDirectory: './src/database/seeds/testing'
  });
  
  beforeAll(async () => {
    // Inicializar base de datos
    await testDb.initialize();
    
    // Ejecutar migraciones
    await testDb.migrate();
  });
  
  beforeEach(async () => {
    // Limpiar tablas antes de cada test
    await testDb.truncate(['users', 'posts']);
    
    // O recrear esquema
    // await testDb.refresh();
    
    // Ejecutar seeds específicas para testing
    await testDb.seed(['test_users']);
  });
  
  afterAll(async () => {
    // Cerrar conexión
    await testDb.close();
  });
  
  test('should create a user', async () => {
    const user = await User.create({
      name: 'Test User',
      email: 'test@example.com'
    });
    
    expect(user.id).toBeDefined();
    expect(user.email).toBe('test@example.com');
    
    const found = await User.findById(user.id);
    expect(found).not.toBeNull();
  });
  
  test('should update user post count', async () => {
    // Crear usuario
    const user = await User.create({
      name: 'Blogger',
      email: 'blogger@example.com'
    });
    
    // Crear posts
    await Post.createMany([
      { title: 'Post 1', content: 'Content 1', userId: user.id },
      { title: 'Post 2', content: 'Content 2', userId: user.id }
    ]);
    
    // Ejecutar servicio a testear
    await userService.updatePostCounts();
    
    // Verificar resultado
    const updatedUser = await User.findById(user.id);
    expect(updatedUser.postCount).toBe(2);
  });
});
 
// Mocking de consultas
test('should handle database errors', async () => {
  // Mock del query builder
  jest.spyOn(User, 'query').mockImplementationOnce(() => {
    return {
      where: jest.fn().mockReturnThis(),
      first: jest.fn().mockRejectedValue(new Error('Database error'))
    };
  });
  
  // Verificar que el servicio maneja el error correctamente
  await expect(userService.getUserByEmail('test@example.com')).rejects.toThrow();
});

Generación de Esquema

Fox Framework permite generar modelos y migraciones a partir de una base de datos existente:

import { SchemaGenerator } from '@foxframework/database';
import { db } from '../database';
 
// Crear generador de esquema
const generator = new SchemaGenerator({
  db,
  connection: 'postgres',
  outputDirectory: './src/database/generated',
  options: {
    // Opciones de generación
    indentation: 2,
    generateModels: true,
    generateMigrations: true,
    modelNameCase: 'pascal',  // 'pascal', 'camel', 'snake'
    tablePrefix: 'tbl_',      // Prefijo a eliminar en nombres de modelo
    includeSchema: true,      // Incluir schema en migraciones
    generateIndexes: true,    // Incluir índices en migraciones
    modelNameSingular: true,  // Usar nombres en singular para modelos
  }
});
 
// Generar todo el esquema
await generator.generateAll();
 
// O generar solo para tablas específicas
await generator.generateForTables(['users', 'posts', 'comments']);
 
// Generar solo modelos
await generator.generateModels();
 
// Generar solo migraciones
await generator.generateMigrations();
 
// Integración con CLI
program
  .command('generate:schema')
  .description('Generate models and migrations from database schema')
  .option('-t, --tables <tables>', 'Comma separated list of tables')
  .option('-m, --models-only', 'Generate only models')
  .option('-g, --migrations-only', 'Generate only migrations')
  .action(async (options) => {
    try {
      const tables = options.tables ? options.tables.split(',') : undefined;
      
      if (options.modelsOnly) {
        await generator.generateModels(tables);
      } else if (options.migrationsOnly) {
        await generator.generateMigrations(tables);
      } else {
        await generator.generateAll(tables);
      }
      
      console.log('Schema generation completed successfully');
      process.exit(0);
    } catch (error) {
      console.error('Schema generation failed:', error);
      process.exit(1);
    }
  });

Schema Builder API

Referencia completa del API para construcción de esquemas en migraciones:

// Tipos de columnas
table.increments('id');                      // INTEGER PRIMARY KEY AUTO_INCREMENT
table.bigIncrements('id');                   // BIGINT PRIMARY KEY AUTO_INCREMENT
table.integer('count');                      // INTEGER
table.bigInteger('big_num');                 // BIGINT
table.text('content');                       // TEXT
table.string('name', 100);                   // VARCHAR(100)
table.float('amount', 8, 2);                 // FLOAT(8, 2)
table.decimal('price', 10, 2);               // DECIMAL(10, 2)
table.boolean('active');                     // BOOLEAN or TINYINT(1)
table.date('birth_date');                    // DATE
table.datetime('created_at');                // DATETIME
table.timestamp('updated_at');               // TIMESTAMP
table.time('start_time');                    // TIME
table.binary('data');                        // BLOB/BINARY
table.enum('status', ['on', 'off']);         // ENUM
table.json('metadata');                      // JSON
table.jsonb('settings');                     // JSONB (PostgreSQL)
table.uuid('id');                            // UUID/CHAR(36)
table.ipAddress('last_ip');                  // VARCHAR(45)
table.macAddress('mac');                     // VARCHAR(17)
table.geometry('position');                  // GEOMETRY (MySQL/PostgreSQL)
table.point('coordinates');                  // POINT (MySQL/PostgreSQL)
table.lineString('path');                    // LINESTRING (MySQL/PostgreSQL)
table.polygon('area');                       // POLYGON (MySQL/PostgreSQL)
 
// Modificadores de columnas
table.string('email').notNull();             // NOT NULL
table.string('name').nullable();             // NULL
table.integer('price').unsigned();           // UNSIGNED
table.integer('position').default(0);        // DEFAULT 0
table.increments('id').primary();            // PRIMARY KEY
table.string('email').unique();              // UNIQUE
table.timestamp('created_at').useCurrent();  // DEFAULT CURRENT_TIMESTAMP
table.integer('user_id').references('id').on('users');  // FOREIGN KEY
 
// Índices
table.index('email');                        // CREATE INDEX
table.index(['first_name', 'last_name']);    // Índice compuesto
table.unique('username');                    // UNIQUE INDEX
table.unique(['email', 'tenant_id']);        // UNIQUE compuesto
table.primary('id');                         // PRIMARY KEY
table.primary(['id', 'tenant_id']);          // PRIMARY KEY compuesto
table.spatialIndex('coordinates');           // Índice espacial (MySQL/PostgreSQL)
 
// Claves foráneas
table.integer('user_id').unsigned().notNull();
table.foreign('user_id')
  .references('id')         // Columna referenciada
  .on('users')              // Tabla referenciada
  .onDelete('CASCADE')      // ON DELETE CASCADE
  .onUpdate('CASCADE');     // ON UPDATE CASCADE
 
// Otras opciones ON DELETE/UPDATE
table.foreign('category_id').references('id').on('categories').onDelete('SET NULL');
table.foreign('status_id').references('id').on('statuses').onDelete('RESTRICT');
table.foreign('parent_id').references('id').on('items').onDelete('NO ACTION');
 
// Comentarios
table.string('code').comment('Unique product code');
table.text('description').comment('Product full description');

Patrones y Buenas Prácticas

Repositorios

import { BaseRepository } from '@foxframework/database';
import { User } from '../models/User';
 
// Definir repositorio
export class UserRepository extends BaseRepository<User> {
  // Especificar el modelo
  protected model = User;
  
  // Método personalizado para buscar por email
  async findByEmail(email: string): Promise<User | null> {
    return this.query()
      .where('email', email)
      .first();
  }
  
  // Buscar por criterios específicos
  async findActive(criteria: any = {}): Promise<User[]> {
    return this.query()
      .where('active', true)
      .where(criteria)
      .orderBy('name')
      .get();
  }
  
  // Métodos personalizados con relaciones
  async findWithPosts(id: number): Promise<User | null> {
    return this.query()
      .where('id', id)
      .with('posts')
      .first();
  }
  
  // Actualización segura
  async updateProfile(id: number, data: Partial<User>): Promise<User> {
    // Solo permitir campos seguros
    const allowedFields = ['name', 'bio', 'avatar', 'preferences'];
    const safeData = Object.keys(data)
      .filter(key => allowedFields.includes(key))
      .reduce((obj, key) => {
        obj[key] = data[key];
        return obj;
      }, {} as Partial<User>);
    
    await this.update(id, safeData);
    return this.findById(id);
  }
  
  // Métodos de paginación avanzados
  async paginate(page = 1, perPage = 15, filters = {}): Promise<PaginationResult<User>> {
    const query = this.query();
    
    // Aplicar filtros
    if (filters.search) {
      query.where(q => {
        q.where('name', 'like', `%${filters.search}%`)
          .orWhere('email', 'like', `%${filters.search}%`);
      });
    }
    
    if (filters.role) {
      query.whereHas('roles', q => {
        q.where('name', filters.role);
      });
    }
    
    // Ordenar
    const sortField = filters.sortBy || 'created_at';
    const sortDir = filters.sortDir || 'desc';
    query.orderBy(sortField, sortDir);
    
    // Ejecutar paginación
    return query.paginate(page, perPage);
  }
}
 
// Uso del repositorio
const userRepository = new UserRepository();
 
// Encontrar usuarios
const user = await userRepository.findById(1);
const users = await userRepository.findMany({ active: true });
const adminUser = await userRepository.findByEmail('admin@example.com');
 
// Crear usuario
const newUser = await userRepository.create({
  name: 'New User',
  email: 'new@example.com',
  active: true
});
 
// Actualizar
await userRepository.update(user.id, { name: 'Updated Name' });
 
// Eliminar
await userRepository.delete(user.id);

Unit of Work

import { UnitOfWork } from '@foxframework/database';
import { UserRepository } from '../repositories/UserRepository';
import { PostRepository } from '../repositories/PostRepository';
import { db } from '../database';
 
// Servicio utilizando Unit of Work
export class BlogService {
  private uow: UnitOfWork;
  private userRepo: UserRepository;
  private postRepo: PostRepository;
  
  constructor() {
    this.uow = new UnitOfWork(db);
    this.userRepo = new UserRepository();
    this.postRepo = new PostRepository();
  }
  
  async createPost(userId: number, postData: any): Promise<any> {
    return this.uow.execute(async (trx) => {
      // Obtener usuario en la transacción
      const user = await this.userRepo.findById(userId, trx);
      
      if (!user) {
        throw new Error('User not found');
      }
      
      // Crear post en la transacción
      const post = await this.postRepo.create({
        userId,
        title: postData.title,
        content: postData.content,
        createdAt: new Date(),
        updatedAt: new Date()
      }, trx);
      
      // Actualizar contador de posts del usuario
      await this.userRepo.update(userId, {
        postCount: (user.postCount || 0) + 1,
        lastPostedAt: new Date()
      }, trx);
      
      // Crear tags si es necesario
      if (postData.tags && Array.isArray(postData.tags)) {
        for (const tagName of postData.tags) {
          // Buscar o crear tag
          let tag = await this.tagRepo.findOne({ name: tagName }, trx);
          
          if (!tag) {
            tag = await this.tagRepo.create({ name: tagName }, trx);
          }
          
          // Asociar tag al post
          await this.postTagRepo.create({
            postId: post.id,
            tagId: tag.id
          }, trx);
        }
      }
      
      // Todo se ejecuta en una sola transacción
      // Si falla cualquier operación, se hace rollback automáticamente
      
      return post;
    });
  }
}

Query Scopes

import { Model, scope } from '@foxframework/database';
 
@table('users')
export class User extends Model {
  // Campos y relaciones
  
  // Scope para usuarios activos
  @scope
  static active(query) {
    return query.where('active', true);
  }
  
  // Scope para roles específicos
  @scope
  static byRole(query, role: string) {
    return query.whereHas('roles', q => q.where('name', role));
  }
  
  // Scope para búsqueda
  @scope
  static search(query, term: string) {
    return query.where(q => {
      q.where('name', 'like', `%${term}%`)
        .orWhere('email', 'like', `%${term}%`);
    });
  }
  
  // Scope con relaciones
  @scope
  static withPostCount(query) {
    return query.withCount('posts as post_count');
  }
}
 
// Uso de scopes
const activeAdmins = await User.query()
  .active()
  .byRole('admin')
  .orderBy('name')
  .get();
 
const searchResults = await User.query()
  .search('john')
  .withPostCount()
  .paginate(1, 20);

Integración con Validación

Fox Framework permite integrar la validación con la capa de base de datos:

import { Model, field, validate } from '@foxframework/database';
import { Schema, Validators } from '@foxframework/validation';
 
// Definir esquema de validación
const userSchema = new Schema({
  name: Validators.string().required().min(3).max(100),
  email: Validators.email().required(),
  age: Validators.number().min(18).optional(),
  active: Validators.boolean().default(true)
});
 
@table('users')
export class User extends Model {
  // Modelo con validación integrada
  @field()
  @validate(userSchema.fields.name)
  name: string;
  
  @field()
  @validate(userSchema.fields.email)
  email: string;
  
  @field()
  @validate(userSchema.fields.age)
  age?: number;
  
  @field({ name: 'is_active' })
  @validate(userSchema.fields.active)
  active: boolean;
  
  // Método para validar antes de guardar
  @beforeSave()
  async validate() {
    const { name, email, age, active } = this;
    
    try {
      await userSchema.validateAsync({
        name, email, age, active
      });
    } catch (error) {
      throw new ValidationError('Validation failed', error.details);
    }
  }
}
 
// Uso
try {
  const user = new User();
  user.name = 'Jo'; // Muy corto, no pasa validación
  user.email = 'not-an-email';
  user.age = 15; // Menor de 18
  
  await user.save();
} catch (error) {
  console.error('Validation error:', error.details);
}

Hooks y Eventos

import { Model, table, field, hooks } from '@foxframework/database';
 
@table('users')
@hooks({
  // Hooks a nivel de modelo
  beforeCreate: async function(user) {
    user.createdAt = new Date();
    user.updatedAt = new Date();
  },
  afterCreate: async function(user) {
    await NotificationService.notifyAdmins('New user created', { userId: user.id });
  },
  beforeUpdate: async function(user) {
    user.updatedAt = new Date();
  },
  beforeSave: async function(user) {
    // Se ejecuta antes de crear o actualizar
    if (user.email) {
      user.email = user.email.toLowerCase();
    }
  },
  afterDelete: async function(user) {
    await AuditService.log('user_deleted', { userId: user.id });
  }
})
export class User extends Model {
  // Campos
  @field({ primaryKey: true, autoIncrement: true })
  id: number;
  
  @field()
  name: string;
  
  @field()
  email: string;
  
  // Hooks a nivel de método
  @beforeCreate()
  generateApiKey() {
    this.apiKey = crypto.randomUUID();
  }
  
  @afterFind()
  loadSettings() {
    if (this.id) {
      this.settings = cache.get(`user_settings:${this.id}`);
    }
  }
}
 
// También se pueden usar eventos para reaccionar a cambios en modelos
Model.events.on('user.created', async (user) => {
  // Lógica adicional tras crear usuario
  await WelcomeMailer.send(user.email);
});
 
Model.events.on('user.deleted', async (userId) => {
  // Limpiar recursos asociados
  await cleanupUserResources(userId);
});

Registro y Monitoreo

import { db } from '../database';
 
// Configurar logging
db.setLogLevel('debug'); // 'debug', 'info', 'warn', 'error'
 
// Registro de queries (útil en desarrollo)
db.onQuery((query, bindings, connectionName) => {
  console.log(`[${connectionName}] ${query}`, bindings);
});
 
// Monitorear queries lentas
db.onSlowQuery((query, bindings, time, connectionName) => {
  console.warn(`[SLOW QUERY] ${time}ms - ${query}`, bindings);
  // Reportar a sistema de monitoreo
  metrics.recordSlowQuery(query, time, connectionName);
});
 
// Monitorear errores
db.onError((error, query, bindings, connectionName) => {
  console.error(`[DB ERROR] ${error.message}`, {
    query, bindings, connectionName
  });
  // Reportar a sistema de monitoreo de errores
  errorReporting.captureException(error, {
    extra: { query, bindings, connectionName }
  });
});
 
// Integración con sistemas de monitoreo
db.use({
  name: 'prometheus',
  onQuery: (query, time, connectionName) => {
    metrics.dbQueryCount.inc({ connection: connectionName });
    metrics.dbQueryTime.observe({ connection: connectionName }, time);
  }
});
 
// Performance monitoring
db.profileQuery('slow_queries', async () => {
  // Ejecutar consulta compleja
  return await db.table('large_table')
    .join('other_table', 'large_table.id', 'other_table.large_id')
    .where('status', 'active')
    .orderBy('created_at', 'desc')
    .limit(1000)
    .get();
});

Conclusión

La capa de abstracción de base de datos de Fox Framework proporciona una API completa y flexible para trabajar con diferentes motores de bases de datos, simplificando operaciones comunes y proporcionando herramientas avanzadas para consultas, migraciones, semillas y más.