Skip to content

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)
}
}
PropiedadTipoDescripción
idstringUUID generado automáticamente.
createdAtDateFecha de creación.
dataDObjeto 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

CampoTipoRequeridoDescripción
tablestringNombre de la tabla PostgreSQL.
schemastringNoSchema (default: public).
constructorIConstructor<E>Clase de la entidad.
add.columnsobjectNoColumnas 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

PrefijoRetornaSQL
findPromise<E[]>SELECT ... FROM tabla
findOnePromise<E | null>SELECT ... LIMIT 1
countPromise<number>SELECT COUNT(*)
existsPromise<boolean>SELECT EXISTS(...)
deletePromise<void>DELETE FROM tabla

Operadores

El operador se infiere del sufijo del nombre del campo (antes del siguiente And/Or):

SufijoSQLParámetro
(ninguno)= $n1 valor
Not<> $n1 valor
LikeLIKE $n1 string (%valor%)
NotLikeNOT LIKE $n1 string
In= ANY($n)1 array
NotInNOT (= ANY($n))1 array
Gt / GreaterThan> $n1 número
Gte / GreaterThanEqual>= $n1 número
Lt / LessThan< $n1 número
Lte / LessThanEqual<= $n1 número
IsNullIS NULLsin parámetro
IsNotNullIS NOT NULLsin 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/IsNotNull no 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 >= $1

Columnas 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étodoDescripció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

PropiedadDescripción
this.tableNombre completo de la tabla con schema ("schema"."tabla").
this.columnsLista de columnas para SELECT ("id", "created_at", "data", ...).
this.poolInstancia de Pool de pg.
// Patrón de consulta manual
protected 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.