Referencia: Entidades y Repositorios
IStorableData
Interfaz base que deben extender todas las interfaces de datos persistidos.
import { IStorableData } from '@wabot-dev/framework'
export interface IProductoData extends IStorableData { nombre: string precio: number categoria: string}Entity<D>
Clase base genérica para entidades de dominio. Encapsula datos y lógica de negocio.
import { Entity } from '@wabot-dev/framework'
export class Producto extends Entity<IProductoData> { get nombre() { return this.data.nombre } get precio() { return this.data.precio }
aplicarDescuento(porcentaje: number) { this.data.precio *= (1 - porcentaje / 100) }}| Propiedad | Tipo | Descripción |
|---|---|---|
id | string | UUID generado automáticamente. |
createdAt | Date | Fecha de creación. |
data | D | Objeto con los datos de la entidad. |
@pgJsonRepository(config) + PgJsonRepository
Enfoque moderno. El decorador configura el repositorio y genera automáticamente el SQL de los métodos @query().
import { PgJsonRepository, pgJsonRepository, query } from '@wabot-dev/framework'Config
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
table | string | Sí | Nombre de la tabla PostgreSQL. |
schema | string | No | Schema (default: public). |
constructor | IConstructor<E> | Sí | Clase de la entidad. |
add.columns | object | No | Columnas físicas adicionales (ver abajo). |
@pgJsonRepository({ schema: 'tienda', table: 'producto', constructor: Producto })export class ProductoRepository extends PgJsonRepository<Producto> { @query() findByCategoria(categoria: string): Promise<Producto[]> { return null! } @query() findOneById(id: string): Promise<Producto | null> { return null! }}El decorador @pgJsonRepository aplica @singleton() automáticamente.
@query() — DSL de consultas automáticas
Marca un método para generación automática de SQL a partir de su nombre. El cuerpo del método nunca se ejecuta; escribe return null!.
Estructura del nombre
<prefijo>[By<condiciones>][OrderBy<campo>Asc|Desc][Limit<N>]Prefijos
| Prefijo | Retorna | SQL |
|---|---|---|
find | Promise<E[]> | SELECT ... FROM tabla |
findOne | Promise<E | null> | SELECT ... LIMIT 1 |
count | Promise<number> | SELECT COUNT(*) |
exists | Promise<boolean> | SELECT EXISTS(...) |
delete | Promise<void> | DELETE FROM tabla |
Operadores
El operador se infiere del sufijo del nombre del campo (antes del siguiente And/Or):
| Sufijo | SQL | Parámetro |
|---|---|---|
| (ninguno) | = $n | 1 valor |
Not | <> $n | 1 valor |
Like | LIKE $n | 1 string (%valor%) |
NotLike | NOT LIKE $n | 1 string |
In | = ANY($n) | 1 array |
NotIn | NOT (= ANY($n)) | 1 array |
Gt / GreaterThan | > $n | 1 número |
Gte / GreaterThanEqual | >= $n | 1 número |
Lt / LessThan | < $n | 1 número |
Lte / LessThanEqual | <= $n | 1 número |
IsNull | IS NULL | sin parámetro |
IsNotNull | IS NOT NULL | sin parámetro |
Ejemplos
@pgJsonRepository({ schema: 'app', table: 'reserva', constructor: Reserva })export class ReservaRepository extends PgJsonRepository<Reserva> {
// Búsquedas simples @query() findByStatus(status: string): Promise<Reserva[]> { return null! } @query() findOneByConfirmationCode(code: string): Promise<Reserva | null> { return null! }
// Múltiples condiciones (And / Or) @query() findByFechaAndStatus(fecha: string, status: string): Promise<Reserva[]> { return null! } @query() findByStatusOrStatus(s1: string, s2: string): Promise<Reserva[]> { return null! }
// Operadores @query() findByPersonasGte(min: number): Promise<Reserva[]> { return null! } @query() findByStatusIn(statuses: string[]): Promise<Reserva[]> { return null! } @query() findByDeletedAtIsNull(): Promise<Reserva[]> { return null! }
// Ordenamiento @query() findByStatusOrderByFechaAsc(status: string): Promise<Reserva[]> { return null! } @query() findAllOrderByFechaDescPersonasAsc(): Promise<Reserva[]> { return null! }
// Límite @query() findByStatusLimit10(status: string): Promise<Reserva[]> { return null! }
// Conteo y existencia @query() countByStatus(status: string): Promise<number> { return null! } @query() existsByChatId(chatId: string): Promise<boolean> { return null! }
// Eliminación @query() deleteByStatus(status: string): Promise<void> { return null! }}Los parámetros del método siguen el orden de las condiciones de izquierda a derecha.
IsNull/IsNotNullno consumen parámetro.
Campos nativos (no-JSON)
id y createdAt mapean a columnas físicas (id, created_at) y usan el índice directamente:
@query() findOneById(id: string): Promise<E | null> { return null! } // WHERE id = $1@query() findByCreatedAtGte(fecha: Date): Promise<E[]> { return null! } // WHERE created_at >= $1Columnas adicionales indexadas (add.columns)
Define columnas físicas adicionales para campos que necesitan índices nativos PostgreSQL (ordenamiento numérico correcto, búsquedas eficientes):
@pgJsonRepository({ schema: 'tienda', table: 'producto', constructor: Producto, add: { columns: { status: { type: 'TEXT', value: (p) => p.data.status }, precio: { type: 'NUMERIC', value: (p) => p.data.precio }, } }})export class ProductoRepository extends PgJsonRepository<Producto> { @query() findByStatus(status: string): Promise<Producto[]> { return null! }}Las columnas se crean y migran automáticamente. Los valores se sincronizan en cada create() y update().
PgCrudRepository<E>
Clase base para repositorios con SQL personalizado. Úsala para consultas complejas (JOINs, subconsultas, agregaciones) que el DSL no puede expresar.
import { PgCrudRepository, singleton } from '@wabot-dev/framework'import { Pool } from 'pg'
@singleton()export class ProductoRepository extends PgCrudRepository<Producto> { constructor(pool: Pool) { super(pool, { schema: 'tienda', table: 'producto', constructor: Producto }) }
async findByChatId(chatId: string): Promise<Producto | null> { const items = await this.query( `SELECT ${this.columns} FROM ${this.table} WHERE data @> $1::jsonb LIMIT 1`, [JSON.stringify({ chatId })] ) return items[0] ?? null }}Métodos heredados
| Método | Descripción |
|---|---|
create(entity) | Inserta un nuevo registro. |
update(entity) | Actualiza un registro existente. |
delete(id) | Elimina por ID. |
find(id) | Busca por ID; retorna null si no existe. |
findOrThrow(id) | Busca por ID; lanza CustomError 404 si no existe. |
findByIds(ids[]) | Busca múltiples IDs. |
findAll() | Retorna todos los registros. |
Propiedades protegidas para SQL manual
| Propiedad | Descripción |
|---|---|
this.table | Nombre completo de la tabla con schema ("schema"."tabla"). |
this.columns | Lista de columnas para SELECT ("id", "created_at", "data", ...). |
this.pool | Instancia de Pool de pg. |
// Patrón de consulta manualprotected async query(sql: string, values: any[]): Promise<E[]>protected async exec(sql: string, values: any[]): Promise<void>Tabla y schema en PostgreSQL
Ambas clases base crean la tabla y las columnas automáticamente si no existen. El esquema de la tabla es:
CREATE TABLE IF NOT EXISTS "schema"."tabla" ( "id" TEXT PRIMARY KEY, "created_at" TIMESTAMP, "data" JSONB, -- columnas adicionales de add.columns)Las columnas nuevas se añaden con ALTER TABLE sin pérdida de datos.