Saltearse al contenido

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.


Ventana de terminal
bun add drizzle-orm
# SQLite (nativo de Bun)
bun add drizzle-orm @libsql/client
# PostgreSQL
bun add drizzle-orm postgres
# MySQL
bun add drizzle-orm mysql2

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 compilar
registerDrizzle(app, db);
app.include(ProductoController);
await app.compile();
app.listen(3000);
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];
}
}

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();
}
src/schema.ts
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(),
});

Ventana de terminal
bun add @prisma/client
bunx prisma init
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 });
}
}

Ventana de terminal
bun add typeorm reflect-metadata
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' });

Para una arquitectura más limpia, crea clases repositorio e inyéctalas con el contenedor DI:

src/repositories/producto.repository.ts
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;
}
}

@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 };
});
}

// ✓ Orden correcto
registerDrizzle(app, db);
app.include(ProductoController);
await app.compile(); // <-- siempre usar await
app.listen(3000);

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.