Integración ORM
Integración ORM
Sección titulada «Integración ORM»Veloce-TS proporciona una capa ORM integrada con soporte de primera clase para Drizzle ORM, Prisma y TypeORM, además de un puente de inyección de dependencias para inyectar tu base de datos en cualquier parte de la aplicación.
Tabla de Contenidos
Sección titulada «Tabla de Contenidos»- Integración con Drizzle ORM
- Integración con Prisma
- Integración con TypeORM
- Patrón Repositorio Base
- Transacciones
- Mejores Prácticas
Integración con Drizzle ORM
Sección titulada «Integración con Drizzle ORM»Instalación
Sección titulada «Instalación»bun add drizzle-orm# SQLite (nativo de Bun)bun add drizzle-orm @libsql/client# PostgreSQLbun add drizzle-orm postgres# MySQLbun add drizzle-orm mysql2Registrar Drizzle en el Contenedor DI
Sección titulada «Registrar Drizzle en el Contenedor DI»Usa registerDrizzle para hacer tu instancia de base de datos disponible en toda la aplicación mediante inyección de dependencias:
import { VeloceTS, registerDrizzle } from 'veloce-ts';import { drizzle } from 'drizzle-orm/bun-sqlite';import { Database } from 'bun:sqlite';
const sqlite = new Database('app.db');const db = drizzle(sqlite);
const app = new VeloceTS({ title: 'Mi API', version: '1.0.0',});
// Registrar antes de compilarregisterDrizzle(app, db);
app.include(ProductoController);await app.compile();app.listen(3000);Inyectar la Base de Datos con @InjectDB
Sección titulada «Inyectar la Base de Datos con @InjectDB»import { Controller, Get, Post, Body, Param, HttpCode, InjectDB, NotFoundException } from 'veloce-ts';import { eq } from 'drizzle-orm';import { productos } from './schema';import { z } from 'zod';
const CreateProductoSchema = z.object({ nombre: z.string().min(2), precio: z.number().positive(), stock: z.number().int().min(0).default(0),});
@Controller('/productos')class ProductoController { @Get('/') async listar(@InjectDB() db: ReturnType<typeof drizzle>) { return await db.select().from(productos); }
@Get('/:id') async obtener( @Param('id') id: string, @InjectDB() db: ReturnType<typeof drizzle> ) { const resultado = await db .select() .from(productos) .where(eq(productos.id, parseInt(id)));
if (!resultado.length) { throw new NotFoundException('Producto no encontrado'); }
return resultado[0]; }
@Post('/') @HttpCode(201) async crear( @Body(CreateProductoSchema) data: z.infer<typeof CreateProductoSchema>, @InjectDB() db: ReturnType<typeof drizzle> ) { const insertado = await db .insert(productos) .values(data) .returning();
return insertado[0]; }}Token de Inyección Personalizado
Sección titulada «Token de Inyección Personalizado»Si necesitas múltiples bases de datos (p.ej. réplica de lectura + primaria de escritura):
import { registerDrizzle, InjectDB } from 'veloce-ts';
const WRITE_DB = Symbol('WRITE_DB');const READ_DB = Symbol('READ_DB');
registerDrizzle(app, primaryDb, WRITE_DB);registerDrizzle(app, replicaDb, READ_DB);
@Get('/')async listar(@InjectDB(READ_DB) readDb: any) { return await readDb.select().from(productos);}
@Post('/')async crear( @Body(CreateProductoSchema) data: any, @InjectDB(WRITE_DB) writeDb: any,) { return await writeDb.insert(productos).values(data).returning();}Definir un Esquema
Sección titulada «Definir un Esquema»import { sqliteTable, integer, text, real } from 'drizzle-orm/sqlite-core';
export const productos = sqliteTable('productos', { id: integer('id').primaryKey({ autoIncrement: true }), nombre: text('nombre').notNull(), precio: real('precio').notNull(), stock: integer('stock').notNull().default(0), creadoEn: text('creado_en').default(new Date().toISOString()),});
export const usuarios = sqliteTable('usuarios', { id: integer('id').primaryKey({ autoIncrement: true }), email: text('email').notNull().unique(), nombre: text('nombre').notNull(), passwordHash: text('password_hash').notNull(),});Integración con Prisma
Sección titulada «Integración con Prisma»Instalación
Sección titulada «Instalación»bun add @prisma/clientbunx prisma initConfiguración
Sección titulada «Configuración»import { VeloceTS } from 'veloce-ts';import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();const DB_TOKEN = Symbol('PRISMA_DB');
const app = new VeloceTS();app.getContainer().registerFactory(DB_TOKEN, () => prisma, { scope: 'singleton' });
app.include(UsuarioController);await app.compile();app.listen(3000);@Controller('/usuarios')class UsuarioController { @Get('/') async listar(@Depends(DB_TOKEN) db: PrismaClient) { return await db.user.findMany(); }
@Post('/') @HttpCode(201) async crear( @Body(CreateUsuarioSchema) data: any, @Depends(DB_TOKEN) db: PrismaClient ) { return await db.user.create({ data }); }}Integración con TypeORM
Sección titulada «Integración con TypeORM»Instalación
Sección titulada «Instalación»bun add typeorm reflect-metadataConfiguración
Sección titulada «Configuración»import { VeloceTS } from 'veloce-ts';import { DataSource } from 'typeorm';import { Usuario } from './entities/usuario.entity';
const dataSource = new DataSource({ type: 'sqlite', database: 'app.db', entities: [Usuario], synchronize: true,});
await dataSource.initialize();
const app = new VeloceTS();const DS_TOKEN = Symbol('DATA_SOURCE');app.getContainer().registerFactory(DS_TOKEN, () => dataSource, { scope: 'singleton' });Patrón Repositorio Base
Sección titulada «Patrón Repositorio Base»Para una arquitectura más limpia, crea clases repositorio e inyéctalas con el contenedor DI:
class ProductoRepository { constructor(@InjectDB() private db: ReturnType<typeof drizzle>) {}
async findAll() { return await this.db.select().from(productos); }
async findById(id: number) { const result = await this.db .select() .from(productos) .where(eq(productos.id, id)); return result[0] ?? null; }
async create(data: typeof productos.$inferInsert) { const inserted = await this.db .insert(productos) .values(data) .returning(); return inserted[0]; }
async update(id: number, data: Partial<typeof productos.$inferInsert>) { const updated = await this.db .update(productos) .set(data) .where(eq(productos.id, id)) .returning(); return updated[0] ?? null; }
async delete(id: number) { const deleted = await this.db .delete(productos) .where(eq(productos.id, id)) .returning(); return deleted.length > 0; }}// Controlador usando el repositorio@Controller('/productos')class ProductoController { @Get('/') async listar(@Depends(ProductoRepository) repo: ProductoRepository) { return await repo.findAll(); }
@Get('/:id') async obtener( @Param('id') id: string, @Depends(ProductoRepository) repo: ProductoRepository ) { const producto = await repo.findById(parseInt(id)); if (!producto) throw new NotFoundException('Producto no encontrado'); return producto; }}Transacciones
Sección titulada «Transacciones»Transacciones con Drizzle
Sección titulada «Transacciones con Drizzle»@Post('/transferencia')async transferir( @Body(TransferenciaSchema) data: any, @InjectDB() db: ReturnType<typeof drizzle>) { return await db.transaction(async (tx) => { // Descontar del emisor await tx .update(cuentas) .set({ saldo: sql`saldo - ${data.monto}` }) .where(eq(cuentas.id, data.desdeCuentaId));
// Agregar al receptor await tx .update(cuentas) .set({ saldo: sql`saldo + ${data.monto}` }) .where(eq(cuentas.id, data.haCuentaId));
return { success: true, monto: data.monto }; });}Mejores Prácticas
Sección titulada «Mejores Prácticas»1. Registra la DB antes de compilar
Sección titulada «1. Registra la DB antes de compilar»// ✓ Orden correctoregisterDrizzle(app, db);app.include(ProductoController);await app.compile(); // <-- siempre usar awaitapp.listen(3000);2. Usa una sola instancia de DB (singleton)
Sección titulada «2. Usa una sola instancia de DB (singleton)»registerDrizzle registra como singleton por defecto. No crees nuevas conexiones por petición.
3. Mantén la lógica de negocio en repositorios o servicios
Sección titulada «3. Mantén la lógica de negocio en repositorios o servicios»// ✓ Bueno - controlador delgado, lógica en repositorio@Get('/:id')async obtener(@Param('id') id: string, @Depends(ProductoRepo) repo: ProductoRepo) { return await repo.findById(parseInt(id));}4. Valida el input antes de tocar la base de datos
Sección titulada «4. Valida el input antes de tocar la base de datos»Usa siempre @Body(Schema). Veloce-TS retorna automáticamente un 422 si la validación falla.
Próximos Pasos
Sección titulada «Próximos Pasos»- Lee la Guía de Paginación para los helpers de paginación cursor y offset
- Aprende sobre Inyección de Dependencias
- Explora Caché para reducir la carga de la base de datos
- Revisa la Referencia API