Reten Docs
Audiencias

Validar

Valida una definición de audience criteria y ejecuta un dry-run contra BigQuery, desde el cuerpo (aún sin persistir) o por id de un criterio guardado; devuelve si es válida y el costo estimado, sin datos ni SQL.

Validar

Valida una definición de audience criteria y, si pasa todas las validaciones, ejecuta un dry-run contra BigQuery. Un mismo motor sirve dos entradas:

  • POST /api/audience-criterias/validate-from-body — valida una definición enviada en el cuerpo (aún no persistida).
  • POST /api/audience-criterias/:id/validate — valida un criterio ya guardado, referenciado por su id.

Ambos devuelven la misma forma de respuesta: IValidateAudienceCriteriaResult, con la validez y el costo estimado del query. La validación no devuelve filas ni el SQL generado: solo si la definición es ejecutable y cuántos bytes procesaría.

Auth: Requerida — permiso audience-criteria:view-sql (ambos endpoints).

Distingue dos caminos de fallo que este recurso trata de forma distinta:

  • La validación estructural y de negocio (DTO, catálogo, reglas de agregación, ventana temporal, etc.) ocurre antes del dry-run y, si falla, responde un error HTTP (400/404) — no llega al cuerpo de respuesta.
  • El dry-run de BigQuery es lo único que se refleja en el cuerpo como valid: false (con status 201).

Ver Validación estructural vs. dry-run.


POST /api/audience-criterias/validate-from-body

Valida 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 (CriteriaDefinitionDto): la definición del criterio, sin envoltorio.

CampoTipoRequeridoDescripción
typeAudienceCriteriaTypeEnumAUDIENCE | FILTER.
queryBlocksCreateQueryBlockDto[]Mínimo 1 elemento. Cada bloque ≈ un statement SQL completo.
composedWithCreateCompositionDto[]NoCompone esta definición con otras audiencias vía operaciones de conjunto.

A diferencia de Crear, la definición no lleva name ni description: es solo el árbol de consulta. Tampoco existe el campo format de Generar SQL —validar no emite SQL—. La validación del DTO es estricta, así que enviar name/description/format (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:

CampoTipoRequeridoDescripción
ordernumber (entero)Posición del bloque; entero ≥ 0.
setOperatorSetOperatorEnumNoCombina este bloque con el anterior. Ausente en el primer bloque; obligatorio en el resto (ver reglas).
fromFromDtoFuente de datos: tabla principal + joins.
selectSelectDtoProyección (columnas y/o agregaciones).
timeWindowTimeWindowDtoNoVentana temporal del bloque (unión discriminada por timeOperator).
whereWhereDtoÁrbol de predicados. Obligatorio aunque sea un grupo vacío.
groupByColumnRefDto[]NoColumnas de agrupación.
havingHavingDtoNoFiltro 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):

CampoTipoRequeridoDescripción
tablestringTabla del catálogo. No vacío.
columnstringColumna. 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)
CampoTipoRequeridoDescripción
tablestringTabla principal del catálogo. No vacío.
joinsJoinDto[]NoCruces con otras tablas. Default [].

Cada JoinDto:

CampoTipoRequeridoDescripción
typeJoinTypeEnumINNER | LEFT | RIGHT | FULL.
tablestringTabla a unir (del catálogo). No vacío.
onJoinPredicateDto[]Condiciones de unión.

Cada JoinPredicateDto es un par de columnas: { left: ColumnRefDto, right: ColumnRefDto }.

select — proyección (SelectDto)
CampoTipoRequeridoDescripción
distinctbooleanAplica SELECT DISTINCT.
items(SelectColumnItem | SelectAggregateItem)[]No vacío. Unión discriminada por el campo kind.

Los items se discriminan por kind:

  • Columnakind: "column":

    CampoTipoRequeridoDescripción
    kind"column"Discriminador.
    columnColumnRefDtoColumna proyectada.
    aliasstringNoDebe matchear ^[A-Za-z_][A-Za-z0-9_]*$.
  • Agregadokind: "aggregate":

    CampoTipoRequeridoDescripción
    kind"aggregate"Discriminador.
    aggregateAggregateDtoExpresión de agregación.
    aliasstringAquí el alias es obligatorio. Mismo regex de identificador.

AggregateDto:

CampoTipoRequeridoDescripción
aggregationOperatorAggregationOperatorEnumCOUNT | SUM | AVG | MIN | MAX.
columnColumnRefDto | "*"Columna a agregar, o el literal "*". El "*" solo tiene sentido para COUNT(*); con column distinto de "*" se valida como ColumnRefDto.
distinctbooleanNoAplica 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 }.
CampoTipoRequeridoDescripción
kind"group" | "predicate"Discriminador de nodo.
logicalOperatorLogicalOperatorEnum (AND | OR)GrupoEn un grupo: siempre obligatorio.
children(Grupo | Predicado)[]GrupoHijos del grupo.
columnColumnRefDtoHojaColumna del predicado.
comparisonOperatorComparisonOperatorEnumHojaOperador de comparación.
valuePredicateValueHojaDepende 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
OperadoresForma de valueEjemplo
IS_NULL, IS_NOT_NULLSin value (se omite)(se omite)
IN, NOT_INLista no vacía ScalarValue[]["CL", "MX"]
BETWEENPar [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.

timeOperatorCampos adicionales
ALL_TIME(ninguno) — sin acotar por tiempo.
IN_THE_LASTreference: ColumnRefDto, unit: TimeUnitEnum, value: entero > 0
IN_THE_FIRSTreference: ColumnRefDto, unit: TimeUnitEnum, value: entero > 0
SINCEreference: ColumnRefDto, from: string (YYYY-MM-DD)
BETWEENreference: 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 últimos value unit (p. ej. últimos 30 DAYS). Se recomputa en cada corrida.
  • IN_THE_FIRST — ventana relativa al origen del ciclo de vida del usuario (su fecha de alta): los primeros value unit de vida de cada usuario. Se ancla por usuario, no a una fecha fija.
  • SINCE — desde la fecha civil from (inclusiva) en adelante.
  • BETWEEN — entre from y to, 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:

CampoTipoRequeridoDescripción
operandCriteriaIdstring (UUID)Id de la otra audiencia (operando). Debe existir.
setOperatorSetOperatorEnumUNION_DISTINCT | UNION_ALL | INTERSECT_DISTINCT.

Ejemplo

Definición de una audiencia de usuarios con al menos una orden pagada en Chile o México.

curl -X POST https://api.reten.ai/api/audience-criterias/validate-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"
            }
          ]
        },
        "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"]
            }
          ]
        }
      }
    ]
  }'
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',
          },
        ],
      },
      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'],
          },
        ],
      },
    },
  ],
};

const response = await axios.post(
  'https://api.reten.ai/api/audience-criterias/validate-from-body',
  body,
  { headers: { Authorization: 'Bearer <token>', 'x-tenant-id': '<tenant-id>' } },
);
const result = response.data; // IValidateAudienceCriteriaResult

La respuesta se documenta en IValidateAudienceCriteriaResult, con un ejemplo válido y uno inválido.


POST /api/audience-criterias/:id/validate

Valida un criterio ya guardado, referenciado por su id. La definición no viaja en el cuerpo: se toma del criterio persistido en el tenant.

Auth: Requerida — permiso audience-criteria:view-sql

Al igual que validate-from-body, 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.

Parámetros de ruta

ParámetroTipoRequeridoDescripción
idUUIDId del audience criteria. Debe ser un UUID válido (si no, 400).

Este endpoint no recibe cuerpo ni query params.

Ejemplo

curl -X POST https://api.reten.ai/api/audience-criterias/f47ac10b-58cc-4372-a567-0e02b2c3d479/validate \
  -H "Authorization: Bearer <token>" \
  -H "x-tenant-id: <tenant-id>"
import axios from 'axios';

const id = 'f47ac10b-58cc-4372-a567-0e02b2c3d479';
const response = await axios.post(
  `https://api.reten.ai/api/audience-criterias/${id}/validate`,
  undefined, // sin cuerpo
  { headers: { Authorization: 'Bearer <token>', 'x-tenant-id': '<tenant-id>' } },
);
const result = response.data; // IValidateAudienceCriteriaResult

Respuesta — IValidateAudienceCriteriaResult

Status 201 Created en ambos endpoints. El cuerpo reporta el resultado del dry-run de BigQuery:

CampoTipoRequeridoDescripción
validbooleantrue si el dry-run tuvo éxito; false si BigQuery rechazó el query.
errorsstring[]NoPresente solo cuando valid: false. Trae el/los mensaje(s) de error de BigQuery.
bytesProcessednumberBytes que BigQuery estima procesar. En éxito, la estimación del dry-run; en fallo, 0.

El dry-run no ejecuta el query ni devuelve filas: solo lo valida contra BigQuery y estima su costo (bytesProcessed). Para obtener una muestra de filas reales use Previsualizar; para ver el SQL generado, Generar SQL.

errors es un arreglo por contrato, pero en la práctica el dry-run devuelve un único mensaje (el que reporta BigQuery). Discrimine siempre por valid antes de leer errors.

Ejemplo — válida (valid: true)

Respuesta 201 a una definición que compila y pasa el dry-run:

{
  "valid": true,
  "bytesProcessed": 10485760
}

Cuando valid es true, errors se omite y bytesProcessed trae la estimación de BigQuery (en este ejemplo, ~10 MB).

Ejemplo — inválida (valid: false)

La definición pasó la validación estructural y de negocio y se compiló a SQL, pero el dry-run de BigQuery la rechazó (p. ej. una columna del catálogo que aún no existe en la tabla real del dataset, o un query cuyo costo estimado supera el límite de bytes facturables). Aun así el status es 201:

{
  "valid": false,
  "errors": [
    "Unrecognized name: legacy_column at [3:8]"
  ],
  "bytesProcessed": 0
}

Con valid: false, errors trae el mensaje de BigQuery y bytesProcessed es 0.


Validación estructural vs. dry-run

Este es el punto clave del recurso: no todos los fallos llegan a errors[]. Una solicitud atraviesa etapas y cada una falla de forma distinta.

EtapaCómo fallaDónde se ve
1. Validación del DTO (estructura, tipos, enums)Error HTTP 400message (arreglo de strings); corta la solicitud.
2. Resolución del criterio (solo :id y composedWith)Error HTTP 404message (string); corta la solicitud.
3. Validación de negocio (catálogo, agregación, tiempo)Error HTTP 400message (string); corta la solicitud.
4. Dry-run de BigQueryvalid: false (status 201)Campo errors del cuerpo de respuesta.

En otras palabras:

  • Todo lo que ocurre antes del dry-run (etapas 1–3) que falla, responde un error HTTP y no produce un cuerpo IValidateAudienceCriteriaResult. Por ejemplo: una tabla fuera del catálogo, un AUDIENCE que no proyecta su columna de identidad, un timeWindow mal formado, un id inexistente o un operandCriteriaId que no existe.
  • valid: false es exclusivo del dry-run: la definición ya era estructuralmente válida y se compiló a SQL, pero BigQuery la rechazó al estimarla. El status sigue siendo 201.

No asuma que "criterio inválido" siempre llega como valid: false. La mayoría de los errores de forma y de negocio son errores HTTP 400/404 y nunca alcanzan el dry-run. valid: false significa específicamente que BigQuery rechazó un query que ya había pasado toda la validación local.

Reglas de validación

Antes del dry-run, la definición pasa por las mismas dos capas de validación que Crear, y ambas devuelven 400 Bad Request:

  1. Validación del DTO (estructura, tipos, enums, campos requeridos). message es un arreglo de strings y se rechazan propiedades desconocidas (incluidos name/description/format).
  2. Validación de negocio (la definición debe generar SQL válido y coherente contra el catálogo). message es un string único con la regla incumplida, y se detiene en el primer invariante violado.

La validación de negocio corre en ambos endpoints: validate-from-body valida la definición del cuerpo, y :id/validate valida el criterio guardado antes de compilarlo. El catálogo completo de reglas y sus mensajes exactos está en Crear → Reglas de validación.

Respuestas de error

Estos son los errores HTTP (distintos de valid: false, que viaja con status 201):

EstadoDescripción
400DTO inválido (campo faltante, tipo incorrecto, enum fuera de rango, propiedad desconocida como name/description/format) o una regla de negocio incumplida. En :id/validate, también si el id no es un UUID válido.
401Falta o es inválido el Authorization: Bearer.
403El usuario no tiene el permiso audience-criteria:view-sql, o el x-tenant-id no está autorizado.
404En :id/validate, no existe un audience criteria con ese id en el tenant. En validate-from-body, un operandCriteriaId de composedWith no existe.