Skip to content

Referencia: Entidades y Repositorios

IEntityData

Interfaz base que deben extender las interfaces de datos de tus entidades. Aporta los campos estándar — el repositorio los completa en create():

import { IEntityData } from '@wabot-dev/framework'
export interface IProductoData extends IEntityData {
nombre: string
precio: number
categoria: string
}
CampoTipoDescripción
id?stringUUID generado por el repositorio al crear.
createdAt?number | nullEpoch ms, asignado por el repositorio al crear.
discardedAt?number | nullEpoch ms de descarte (borrado lógico).

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)
}
}
MiembroTipoDescripción
idstringLanza error si la entidad aún no fue creada.
createdAtDateFecha de creación; lanza error si aún no fue creada.
dataD (protegido)Objeto con los datos de la entidad.
update(partial)Mezcla campos permitidos; rechaza id/createdAt/discardedAt.
wasCreated()booleantrue si ya tiene id y createdAt.
validate()Ejecuta los validadores decorados de la entidad.

@repository(config) + CrudRepository

Enfoque recomendado. El decorador configura el repositorio, genera las implementaciones CRUD y las consultas declaradas con @query(). Aplica @singleton() automáticamente.

import { CrudRepository, query, repository } from '@wabot-dev/framework'
@repository({ schema: 'tienda', table: 'producto', constructor: Producto })
export class ProductoRepository extends CrudRepository<Producto> {
@query() declare findByCategoria: (categoria: string) => Promise<Producto[]>
@query() declare findOneById: (id: string) => Promise<Producto | null>
}

Config

CampoTipoRequeridoDescripción
tablestringNombre de la tabla.
schemastringNoSchema PostgreSQL (default: public).
constructorIConstructor<E>Clase de la entidad.
add.columnsobjectNoColumnas físicas adicionales — específico de PostgreSQL (ver abajo).

Adaptadores

El runner del proyecto elige el adaptador por proceso, sin registro manual:

AdaptadorCuándo se usaAlmacenamiento
PgJsonRepositoryAdapterDATABASE_URL apunta a PostgreSQLCada fila: id, created_at y un blob data JSONB.
MemoryRepositoryAdapterSin DATABASE_URLRAM (con persistencia opcional en .wabot/).

Métodos CRUD generados

MétodoDescripción
create(entity)Inserta; asigna id y createdAt.
update(entity)Actualiza un registro existente.
delete(entity)Elimina la entidad.
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.

@query() — DSL de consultas automáticas

Marca una propiedad declare para generación automática de la consulta a partir de su nombre. Solo escribes la firma; la implementación la genera el decorador (SQL bajo PostgreSQL, filtrado en memoria bajo el adaptador in-memory).

@query() declare findByStatus: (status: string) => Promise<Reserva[]>

Estructura del nombre

<prefijo>[By<condiciones> | All][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

findOne implica LIMIT 1 — combinarlo con Limit lanza error.

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

@repository({ schema: 'app', table: 'reserva', constructor: Reserva })
export class ReservaRepository extends CrudRepository<Reserva> {
// Búsquedas simples
@query() declare findByStatus: (status: string) => Promise<Reserva[]>
@query() declare findOneByConfirmationCode: (code: string) => Promise<Reserva | null>
// Múltiples condiciones (And / Or)
@query() declare findByFechaAndStatus: (fecha: string, status: string) => Promise<Reserva[]>
@query() declare findByStatusOrStatus: (s1: string, s2: string) => Promise<Reserva[]>
// Operadores
@query() declare findByPersonasGte: (min: number) => Promise<Reserva[]>
@query() declare findByStatusIn: (statuses: string[]) => Promise<Reserva[]>
@query() declare findByDiscardedAtIsNull: () => Promise<Reserva[]>
// Ordenamiento
@query() declare findByStatusOrderByFechaAsc: (status: string) => Promise<Reserva[]>
@query() declare findAllOrderByFechaDescPersonasAsc: () => Promise<Reserva[]>
// Límite
@query() declare findByStatusLimit10: (status: string) => Promise<Reserva[]>
// Conteo y existencia
@query() declare countByStatus: (status: string) => Promise<number>
@query() declare existsByChatId: (chatId: string) => Promise<boolean>
// Eliminación
@query() declare deleteByStatus: (status: string) => Promise<void>
}

Los parámetros del método siguen el orden de las condiciones de izquierda a derecha. IsNull/IsNotNull no consumen parámetro. El DSL no tiene paréntesis: aplica la precedencia de SQL (And liga más fuerte que Or).

Campos nativos (no-JSON)

id y createdAt mapean a columnas físicas (id, created_at) y usan el índice directamente:

@query() declare findOneById: (id: string) => Promise<E | null> // WHERE id = $1
@query() declare findByCreatedAtGte: (fecha: Date) => Promise<E[]> // WHERE created_at >= $1

Los demás campos se extraen del JSON (data->>'campo'), que es texto — un ORDER BY sobre un campo numérico del JSON ordena lexicográficamente. Para hot paths usa add.columns.

Columnas adicionales indexadas (add.columns)

Campo específico del adaptador PostgreSQL (el de memoria lo ignora). Define columnas físicas para campos que necesitan índices nativos:

import { CrudRepository, IPgRepositoryConfig, query, repository } from '@wabot-dev/framework'
const config: IPgRepositoryConfig<Producto> = {
schema: 'tienda',
table: 'producto',
constructor: Producto,
add: {
columns: {
status: { type: 'TEXT', value: (p) => p.status },
precio: { type: 'NUMERIC', value: (p) => p.precio },
},
},
}
@repository(config)
export class ProductoRepository extends CrudRepository<Producto> {
@query() declare findByStatus: (status: string) => Promise<Producto[]>
}

Las columnas se crean y migran automáticamente. Los valores se sincronizan en cada create() y update().


@queryExtension() — consultas por adaptador

Para consultas que el DSL no puede expresar (JOINs, agregaciones, rutas JSON), declara la firma con @queryExtension() y provee una implementación por adaptador:

import {
CrudRepository, memExtension, MemoryRepositoryExtension,
pgExtension, PgRepositoryExtension, query, queryExtension, repository,
} from '@wabot-dev/framework'
export interface IReservaRepositoryExtensions {
findSolapadas(fecha: string, mesa: number): Promise<Reserva[]>
}
@repository({ table: 'reserva', constructor: Reserva })
export class ReservaRepository
extends CrudRepository<Reserva, IReservaRepositoryExtensions>
implements IReservaRepositoryExtensions
{
@queryExtension() declare findSolapadas: (fecha: string, mesa: number) => Promise<Reserva[]>
}
@pgExtension(ReservaRepository)
export class ReservaPgQueries
extends PgRepositoryExtension<Reserva>
implements IReservaRepositoryExtensions
{
async findSolapadas(fecha: string, mesa: number) {
return this['query'](
`SELECT ${this['columns']} FROM ${this['table']}
WHERE data->>'fecha' = $1 AND data->>'mesa' = $2`,
[fecha, String(mesa)],
)
}
}
@memExtension(ReservaRepository)
export class ReservaMemoryQueries
extends MemoryRepositoryExtension<Reserva>
implements IReservaRepositoryExtensions
{
async findSolapadas(fecha: string, mesa: number) {
return [...this.items.values()]
.filter((r) => r['data'].fecha === fecha && r['data'].mesa === mesa)
.map((r) => this.clone(r))
}
}
  • La clase de extensión debe extender PgRepositoryExtension<E> / MemoryRepositoryExtension<E>; el decorador lanza error si no.
  • Si solo registras @pgExtension, invocar la extensión bajo el adaptador en memoria lanza error (y viceversa).
  • Dentro de una extensión Pg dispones de this['table'], this['columns'], this['query'](sql, params) (retorna entidades), this['exec'](sql, params) y this['pool'] (acceso crudo para agregaciones).

PgCrudRepository<E> (bajo nivel)

Clase base PostgreSQL para repositorios totalmente manuales (sin @repository). Útil para infraestructura propia; en aplicaciones normales prefiere @repository.

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 findOneByChatId(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
}
}

Hereda los mismos métodos CRUD (create, update, delete, find, findOrThrow, findByIds, findAll) y expone 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

La tabla y sus columnas se crean automáticamente si no existen:

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.