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}| Campo | Tipo | Descripción |
|---|---|---|
id? | string | UUID generado por el repositorio al crear. |
createdAt? | number | null | Epoch ms, asignado por el repositorio al crear. |
discardedAt? | number | null | Epoch 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) }}| Miembro | Tipo | Descripción |
|---|---|---|
id | string | Lanza error si la entidad aún no fue creada. |
createdAt | Date | Fecha de creación; lanza error si aún no fue creada. |
data | D (protegido) | Objeto con los datos de la entidad. |
update(partial) | — | Mezcla campos permitidos; rechaza id/createdAt/discardedAt. |
wasCreated() | boolean | true 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
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
table | string | Sí | Nombre de la tabla. |
schema | string | No | Schema PostgreSQL (default: public). |
constructor | IConstructor<E> | Sí | Clase de la entidad. |
add.columns | object | No | Columnas físicas adicionales — específico de PostgreSQL (ver abajo). |
Adaptadores
El runner del proyecto elige el adaptador por proceso, sin registro manual:
| Adaptador | Cuándo se usa | Almacenamiento |
|---|---|---|
PgJsonRepositoryAdapter | DATABASE_URL apunta a PostgreSQL | Cada fila: id, created_at y un blob data JSONB. |
MemoryRepositoryAdapter | Sin DATABASE_URL | RAM (con persistencia opcional en .wabot/). |
Métodos CRUD generados
| Método | Descripció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
| 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 |
findOneimplicaLIMIT 1— combinarlo conLimitlanza error.
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
@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/IsNotNullno consumen parámetro. El DSL no tiene paréntesis: aplica la precedencia de SQL (Andliga más fuerte queOr).
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 >= $1Los 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)ythis['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:
| 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
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.