Previsualizar
Ejecuta una audience criteria contra BigQuery y devuelve una muestra de filas reales más el total, desde el cuerpo (aún sin persistir) o por id de un criterio guardado.
Previsualizar
Ejecuta una audience criteria contra BigQuery (acotada por limit) y devuelve
una muestra de las filas reales que matchea más el total del universo. Un mismo
motor sirve dos entradas:
POST /api/audience-criterias/preview-from-body— ejecuta una definición enviada en el cuerpo (aún no persistida).POST /api/audience-criterias/:id/preview— ejecuta un criterio ya guardado, referenciado por suid.
Ambos devuelven la misma forma de respuesta:
IPreviewAudienceCriteriaResult.
A diferencia de Generar SQL, preview no devuelve SQL; y a diferencia de Validar (que solo hace un dry-run), preview sí trae datos: corre la consulta y te muestra filas.
Auth: Requerida — permiso audience-criteria:view-sql (ambos endpoints).
Preview consume cómputo en BigQuery en cada llamada (ejecuta la audiencia de verdad,
no un dry-run). Para acotar el costo, cada consulta se corre con maximumBytesBilled de
10 GB (10737418240 bytes) y un timeout de 30 s (jobTimeoutMs): una consulta
que exceda cualquiera de los dos límites se rechaza con 400 en vez de facturarse. Usá
un limit chico mientras iterás.
El SQL que se ejecuta apunta a BigQuery: referencia las tablas como
`<project>.<dataset>.<tabla>`, donde el <project> sale de la configuración de
ambiente del API y el <dataset> se resuelve a partir del tenant. Ninguno forma parte
del contrato de entrada.
POST /api/audience-criterias/preview-from-body
Ejecuta una definición enviada en el cuerpo, sin persistirla.
Auth: Requerida — permiso audience-criteria:view-sql
Este endpoint no declara un código explícito, por lo que NestJS responde
201 Created por defecto —no 200—, aunque no cree ningún recurso.
Cuerpo de la solicitud
Nivel raíz (PreviewFromBodyDto): la definición del criterio (CriteriaDefinitionDto)
más el campo limit.
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
type | AudienceCriteriaTypeEnum | Sí | AUDIENCE | FILTER. |
queryBlocks | CreateQueryBlockDto[] | Sí | Mínimo 1 elemento. Cada bloque ≈ un statement SQL completo. |
composedWith | CreateCompositionDto[] | No | Compone esta definición con otras audiencias vía operaciones de conjunto. |
limit | number (entero) | No | Tamaño de la muestra de rows. Entero 1..500. Default 100 (ver limit). |
A diferencia de Crear, la definición
no lleva name ni description: es solo el árbol de consulta más limit. La
validación es estricta, así que enviar name/description (u otra propiedad
desconocida) produce un 400.
Definición del criterio
Es el mismo árbol que recibe Crear, sin los
campos name/description. Se documenta aquí de forma autocontenida.
queryBlocks[] — bloque de consulta
Cada elemento de queryBlocks (CreateQueryBlockDto) es un statement SQL completo:
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
order | number (entero) | Sí | Posición del bloque; entero ≥ 0. |
setOperator | SetOperatorEnum | No | Combina este bloque con el anterior. Ausente en el primer bloque; obligatorio en el resto (ver reglas). |
from | FromDto | Sí | Fuente de datos: tabla principal + joins. |
select | SelectDto | Sí | Proyección (columnas y/o agregaciones). |
timeWindow | TimeWindowDto | No | Ventana temporal del bloque (unión discriminada por timeOperator). |
where | WhereDto | Sí | Árbol de predicados. Obligatorio aunque sea un grupo vacío. |
groupBy | ColumnRefDto[] | No | Columnas de agrupación. |
having | HavingDto | No | Filtro sobre agregados. |
El setOperator de un query block combina bloques dentro de la misma definición;
no debe confundirse con el setOperator de una composición,
que combina criterios enteros. Son dos niveles distintos de operación de conjunto.
ColumnRefDto — referencia a columna
Se usa en todo el árbol (select, filtros, group by, joins, agregados, ventana temporal):
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
table | string | Sí | Tabla del catálogo. No vacío. |
column | string | Sí | Columna. No vacío. Puede ser un path de STRUCT (p. ej. "pricing.final_amount"). |
Toda columna se cualifica siempre con su table para evitar ambigüedad.
from — fuente de datos (FromDto)
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
table | string | Sí | Tabla principal del catálogo. No vacío. |
joins | JoinDto[] | No | Cruces con otras tablas. Default []. |
Cada JoinDto:
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
type | JoinTypeEnum | Sí | INNER | LEFT | RIGHT | FULL. |
table | string | Sí | Tabla a unir (del catálogo). No vacío. |
on | JoinPredicateDto[] | Sí | Condiciones de unión. |
Cada JoinPredicateDto es un par de columnas: { left: ColumnRefDto, right: ColumnRefDto }.
select — proyección (SelectDto)
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
distinct | boolean | Sí | Aplica SELECT DISTINCT. |
items | (SelectColumnItem | SelectAggregateItem)[] | Sí | No vacío. Unión discriminada por el campo kind. |
Los items se discriminan por kind:
-
Columna —
kind: "column":Campo Tipo Requerido Descripción kind"column"Sí Discriminador. columnColumnRefDtoSí Columna proyectada. aliasstring No Debe matchear ^[A-Za-z_][A-Za-z0-9_]*$. -
Agregado —
kind: "aggregate":Campo Tipo Requerido Descripción kind"aggregate"Sí Discriminador. aggregateAggregateDtoSí Expresión de agregación. aliasstring Sí Aquí el alias es obligatorio. Mismo regex de identificador.
AggregateDto:
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
aggregationOperator | AggregationOperatorEnum | Sí | COUNT | SUM | AVG | MIN | MAX. |
column | ColumnRefDto | "*" | Sí | Columna a agregar, o el literal "*". El "*" solo tiene sentido para COUNT(*); con column distinto de "*" se valida como ColumnRefDto. |
distinct | boolean | No | Aplica DISTINCT dentro del agregado. |
where — árbol de predicados (WhereDto)
El where es un árbol recursivo. El nodo raíz es siempre un grupo:
- Grupo —
{ kind: "group", logicalOperator: LogicalOperatorEnum, children: (Predicado | Grupo)[] }. Se permiten grupos anidados (AND/OR a varios niveles). - Predicado (hoja) —
{ kind: "predicate", column: ColumnRefDto, comparisonOperator: ComparisonOperatorEnum, value?: PredicateValue }.
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
kind | "group" | "predicate" | Sí | Discriminador de nodo. |
logicalOperator | LogicalOperatorEnum (AND | OR) | Grupo | En un grupo: siempre obligatorio. |
children | (Grupo | Predicado)[] | Grupo | Hijos del grupo. |
column | ColumnRefDto | Hoja | Columna del predicado. |
comparisonOperator | ComparisonOperatorEnum | Hoja | Operador de comparación. |
value | PredicateValue | Hoja | Depende del operador (ver abajo). |
logicalOperator es obligatorio siempre en un grupo, pero solo tiene efecto con
2 o más hijos. Con menos de 2 hijos su valor es indiferente (genera el mismo SQL), pero
igual debe enviarse. Un where sin filtros se expresa como un grupo vacío:
{ "kind": "group", "logicalOperator": "AND", "children": [] }.
Forma de value según comparisonOperator
| Operadores | Forma de value | Ejemplo |
|---|---|---|
IS_NULL, IS_NOT_NULL | Sin value (se omite) | (se omite) |
IN, NOT_IN | Lista no vacía ScalarValue[] | ["CL", "MX"] |
BETWEEN | Par [min, max] | [10, 20] |
El resto (EQUALS, NOT_EQUALS, GREATER_THAN, LESS_THAN, GREATER_THAN_OR_EQUAL, LESS_THAN_OR_EQUAL, CONTAINS, NOT_CONTAINS, STARTS_WITH, ENDS_WITH) | Escalar único | "activo" · 42 |
ScalarValue = string | number | boolean. Además, cada operador debe ser
compatible con el tipo de la columna (ver reglas de validación).
groupBy — columnas de agrupación
Lista opcional de ColumnRefDto[]. Cuando el bloque agrega (declara groupBy o tiene un
agregado en el select), el groupBy debe coincidir exactamente con las columnas
no agregadas del select (ver reglas de validación).
having — filtro sobre agregados (HavingDto)
Misma estructura recursiva que where (grupos AND/OR anidados y misma regla de value
por operador), pero la hoja opera sobre un agregado, no sobre una columna:
- Predicado (hoja) —
{ kind: "predicate", aggregate: AggregateDto, comparisonOperator: ComparisonOperatorEnum, value?: PredicateValue }. - Grupo — idéntico al de
where.
timeWindow — ventana temporal (TimeWindowDto)
Unión discriminada por timeOperator. Acota el bloque a un período de tiempo sobre una
columna TIMESTAMP del catálogo (reference). Es un campo hermano del where, no un
nodo del árbol de filtros.
timeOperator | Campos adicionales |
|---|---|
ALL_TIME | (ninguno) — sin acotar por tiempo. |
IN_THE_LAST | reference: ColumnRefDto, unit: TimeUnitEnum, value: entero > 0 |
IN_THE_FIRST | reference: ColumnRefDto, unit: TimeUnitEnum, value: entero > 0 |
SINCE | reference: ColumnRefDto, from: string (YYYY-MM-DD) |
BETWEEN | reference: ColumnRefDto, from: string (YYYY-MM-DD), to: string (YYYY-MM-DD) |
TimeUnitEnum: DAYS | WEEKS | MONTHS | YEARS.
Semántica de negocio de cada operador:
ALL_TIME— sin ventana; considera todo el histórico.IN_THE_LAST— ventana rodante relativa a "ahora": los últimosvalueunit(p. ej. últimos30 DAYS). Se recomputa en cada corrida.IN_THE_FIRST— ventana relativa al origen del ciclo de vida del usuario (su fecha de alta): los primerosvalueunitde vida de cada usuario. Se ancla por usuario, no a una fecha fija.SINCE— desde la fecha civilfrom(inclusiva) en adelante.BETWEEN— entrefromyto, ambas fechas civiles inclusivas de su día completo.
Las fechas civiles YYYY-MM-DD (SINCE, BETWEEN) se resuelven al inicio del día en
la zona horaria del tenant. El detalle de zona horaria es interno; basta con enviar la
fecha civil.
composedWith[] — composición con otras audiencias
Cada elemento (CreateCompositionDto) combina esta definición con otra audiencia
vía una operación de conjunto:
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
operandCriteriaId | string (UUID) | Sí | Id de la otra audiencia (operando). Debe existir. |
setOperator | SetOperatorEnum | Sí | UNION_DISTINCT | UNION_ALL | INTERSECT_DISTINCT. |
Ejemplo
Audiencia de usuarios con al menos una orden pagada en Chile o México, proyectando
user_id y country. Se pide una muestra de 50 filas (limit).
curl -X POST https://api.reten.ai/api/audience-criterias/preview-from-body \
-H "Authorization: Bearer <token>" \
-H "x-tenant-id: <tenant-id>" \
-H "Content-Type: application/json" \
-d '{
"type": "AUDIENCE",
"queryBlocks": [
{
"order": 0,
"from": { "table": "orders", "joins": [] },
"select": {
"distinct": true,
"items": [
{
"kind": "column",
"column": { "table": "orders", "column": "user_id" },
"alias": "user_id"
},
{
"kind": "column",
"column": { "table": "orders", "column": "country" },
"alias": "country"
}
]
},
"where": {
"kind": "group",
"logicalOperator": "AND",
"children": [
{
"kind": "predicate",
"column": { "table": "orders", "column": "status" },
"comparisonOperator": "EQUALS",
"value": "paid"
},
{
"kind": "predicate",
"column": { "table": "orders", "column": "country" },
"comparisonOperator": "IN",
"value": ["CL", "MX"]
}
]
}
}
],
"limit": 50
}'import axios from 'axios';
const body = {
type: 'AUDIENCE',
queryBlocks: [
{
order: 0,
from: { table: 'orders', joins: [] },
select: {
distinct: true,
items: [
{
kind: 'column',
column: { table: 'orders', column: 'user_id' },
alias: 'user_id',
},
{
kind: 'column',
column: { table: 'orders', column: 'country' },
alias: 'country',
},
],
},
where: {
kind: 'group',
logicalOperator: 'AND',
children: [
{
kind: 'predicate',
column: { table: 'orders', column: 'status' },
comparisonOperator: 'EQUALS',
value: 'paid',
},
{
kind: 'predicate',
column: { table: 'orders', column: 'country' },
comparisonOperator: 'IN',
value: ['CL', 'MX'],
},
],
},
},
],
limit: 50, // opcional; por defecto 100
};
const response = await axios.post(
'https://api.reten.ai/api/audience-criterias/preview-from-body',
body,
{ headers: { Authorization: 'Bearer <token>', 'x-tenant-id': '<tenant-id>' } },
);
const preview = response.data; // IPreviewAudienceCriteriaResultLa respuesta se documenta en IPreviewAudienceCriteriaResult,
con un ejemplo de rows no vacío.
POST /api/audience-criterias/:id/preview
Ejecuta un criterio ya guardado, referenciado por su id. La definición no viaja en
el cuerpo: se toma del criterio persistido en el tenant. El cuerpo solo lleva limit.
Auth: Requerida — permiso audience-criteria:view-sql
Como el anterior, no declara un código explícito: NestJS responde 201 Created.
Parámetros de ruta
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
id | UUID | Sí | Id del audience criteria. Debe ser un UUID válido (si no, 400). |
Cuerpo de la solicitud
PreviewQueryDto — un único campo, opcional:
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
limit | number (entero) | No | Tamaño de la muestra de rows. Entero 1..500. Default 100 (ver limit). |
Un cuerpo vacío ({}) es válido: usa el limit por defecto (100).
Ejemplo
curl -X POST https://api.reten.ai/api/audience-criterias/f47ac10b-58cc-4372-a567-0e02b2c3d479/preview \
-H "Authorization: Bearer <token>" \
-H "x-tenant-id: <tenant-id>" \
-H "Content-Type: application/json" \
-d '{ "limit": 50 }'import axios from 'axios';
const id = 'f47ac10b-58cc-4372-a567-0e02b2c3d479';
const response = await axios.post(
`https://api.reten.ai/api/audience-criterias/${id}/preview`,
{ limit: 50 }, // opcional; por defecto 100
{ headers: { Authorization: 'Bearer <token>', 'x-tenant-id': '<tenant-id>' } },
);
const preview = response.data; // IPreviewAudienceCriteriaResultlimit — tamaño de la muestra
limit acota solo cuántas filas trae la respuesta en rows (la muestra que ves), no
el universo que matchea la audiencia.
- Rango: entero 1..500 (menor a 1 o mayor a 500 ⇒
400). - Default:
100(PREVIEW_ROW_LIMIT_DEFAULT) cuando se omite.
limit gobierna el tamaño de la muestra en rows; totalCount es el universo
completo que matchea la audiencia y no está acotado por limit. Preview corre dos
consultas sobre la misma definición: una SELECT ... LIMIT <limit> para la muestra y un
SELECT COUNT(*) para el total. Por eso podés ver rows.length = 50 con
totalCount = 4821: rows es un vistazo, totalCount es el tamaño real de la audiencia.
Respuesta — IPreviewAudienceCriteriaResult
| Campo | Tipo | Descripción |
|---|---|---|
rows | Record<string, unknown>[] | Muestra de filas reales, acotada por limit. Las columnas dependen del SELECT de la criteria (sus alias); por eso rows no tiene una forma fija. |
totalCount | number | Total de filas que matchea la audiencia, sin acotar por limit (el universo completo). |
bytesProcessed | number | Bytes procesados por BigQuery en la corrida. Suma los bytes de ambas consultas (muestra + conteo). |
Distinguí siempre rows (la muestra, como mucho limit filas) de totalCount (el
universo, sin límite). Si totalCount > rows.length, hay más filas de las que la
muestra alcanza a mostrar.
Ejemplo
Respuesta 201 al ejemplo de arriba (limit: 50). La audiencia matchea 4821 usuarios;
rows muestra los primeros 50 (aquí se recortan a 3 por brevedad), cada uno con las
columnas proyectadas (user_id, country):
{
"rows": [
{ "user_id": "u_9f3a1c", "country": "CL" },
{ "user_id": "u_2b7e04", "country": "MX" },
{ "user_id": "u_c081ff", "country": "CL" }
],
"totalCount": 4821,
"bytesProcessed": 5242880
}Las claves de cada objeto en rows (user_id, country) salen de los alias del
select: otra definición produciría otras columnas. totalCount (4821) es el universo
completo, mayor que las 50 filas de la muestra.
Reglas de validación
Antes de ejecutar, la definición pasa por las mismas dos capas de validación que
Crear, y ambas
devuelven 400 Bad Request:
- Validación del DTO (estructura, tipos, enums, campos requeridos,
limiten rango).messagees un arreglo de strings y se rechazan propiedades desconocidas (incluidosname/description). - Validación de negocio (la definición debe generar SQL válido y coherente contra el
catálogo).
messagees un string único con la regla incumplida.
La validación de negocio corre en ambos endpoints: preview-from-body valida la
definición del cuerpo, y :id/preview valida el criterio guardado antes de ejecutarlo. El
catálogo completo de reglas y sus mensajes exactos está en
Crear → Reglas de validación.
Si la definición supera las dos capas pero la ejecución en BigQuery falla —por
exceder el límite de maximumBytesBilled (10 GB), agotar el timeout de 30 s, o cualquier
otro error del motor— la respuesta también es 400, con un message del tipo
Query execution failed: <detalle>.
Respuestas de error
| Estado | Descripción |
|---|---|
400 | DTO inválido (campo faltante, tipo incorrecto, enum fuera de rango, limit fuera de 1..500, propiedad desconocida como name/description), una regla de negocio incumplida, o un fallo de ejecución en BigQuery (Query execution failed: ..., incluye exceder 10 GB o el timeout de 30 s). En :id/preview, también si el id no es un UUID válido. |
401 | Falta o es inválido el Authorization: Bearer. |
403 | El usuario no tiene el permiso audience-criteria:view-sql, o el x-tenant-id no está autorizado. |
404 | En :id/preview, no existe un audience criteria con ese id en el tenant. En preview-from-body, un operandCriteriaId de composedWith no existe. |