Actualizar Audience Criteria
Actualiza parcialmente un audience criteria por id. Solo se modifican los campos enviados; incluir queryBlocks reemplaza por completo la definición (bloques y composiciones).
PATCH /api/audience-criterias/:id
Actualiza un Audience Criteria existente. Es una actualización parcial: solo se modifican los campos presentes en el cuerpo; los omitidos quedan intactos. La operación es transaccional — si se reemplaza la definición, los cambios se aplican juntos o no se aplica ninguno.
Auth: Requerida — permiso audience-criteria:update
Parámetros de ruta
| Parámetro | Tipo | Descripción |
|---|---|---|
id | UUID | ID del audience criteria. |
Semántica de actualización (léase primero)
El comportamiento del PATCH no es un merge campo a campo del árbol. Se resume así:
- Solo se actualiza lo enviado. Un campo omitido no se toca.
name— si se envía, no puede ser vacío; actualiza el nombre.description— enviar un string la cambia; enviarnullla limpia (quedanull); omitirla la deja igual.type— enviar un enum válido lo cambia; omitirlo lo deja igual (ver el aviso detype: null).
updatedByse fija al usuario que edita;updatedAtse refresca automáticamente.
queryBlocks es un reemplazo total, no un merge. Si el cuerpo incluye
queryBlocks, el servicio borra todos los query blocks (y todas las composiciones)
existentes del criterio y los recrea desde cero con lo enviado. No se hace diff ni
se conservan los bloques previos. Si omites queryBlocks, la definición
(bloques + composiciones) queda intacta y solo se actualizan name / description / type.
Enviar queryBlocks sin composedWith borra las composiciones. El reemplazo elimina
bloques y composiciones juntos, y las composiciones solo se recrean si composedWith
trae al menos un elemento. Para conservar las composiciones al re-enviar queryBlocks,
hay que re-enviar también composedWith.
Corolario: composedWith por sí solo es un no-op. Las composiciones únicamente se
tocan cuando también se envía queryBlocks; enviar composedWith sin queryBlocks no
modifica las composiciones existentes.
La validación de negocio es condicional. El árbol de la definición se revalida
(con la zona horaria del tenant) solo si se envía queryBlocks. Si no se envía,
name / description / type se actualizan sin revalidar la definición. La
revalidación usa el type efectivo (type del cuerpo si viene; si no, el type
actual del criterio), de modo que las reglas de un AUDIENCE (p. ej. proyectar la columna
de identidad) se siguen aplicando aunque no cambies el type.
No envíes type: null. La columna type es NOT NULL. Un null explícito se
escribiría tal cual y provocaría un error del servidor. Para no cambiar el tipo,
omite el campo; para cambiarlo, envía un enum válido (AUDIENCE | FILTER).
Cuerpo de la solicitud
Nivel raíz (UpdateAudienceCriteriaDto). Todos los campos son opcionales:
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
name | string | No | Si se envía, no puede ser vacío. |
description | string | null | No | String para cambiarla, null para limpiarla. Omitir la deja igual. |
type | AudienceCriteriaTypeEnum | No | AUDIENCE | FILTER. No enviar null (ver aviso arriba). |
queryBlocks | CreateQueryBlockDto[] | No | Si se incluye, reemplaza toda la definición. Mismo shape que en Crear. |
composedWith | CreateCompositionDto[] | No | Solo surte efecto si también se envía queryBlocks (ver avisos). |
A diferencia de Crear, aquí queryBlocks
no exige un mínimo de un elemento a nivel de DTO (no lleva la restricción de tamaño
mínimo). La obligatoriedad de tener una definición válida la impone la validación de
negocio cuando se envía queryBlocks.
Cuando se envía queryBlocks y/o composedWith, usan exactamente los mismos DTOs que
Crear (CreateQueryBlockDto / CreateCompositionDto). El sub-árbol se documenta completo a
continuación.
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 del mismo criterio;
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 (hoy, 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 este criterio 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. |
Recuerda: composedWith solo se aplica cuando también se envía queryBlocks (ver
Semántica de actualización).
Reglas de validación
La solicitud pasa por dos capas de validación, ambas devuelven 400 Bad Request:
- Validación del DTO (estructura, tipos, enums, campos requeridos del sub-árbol). En
este caso
messagees un arreglo de strings y se rechazan propiedades desconocidas. Corre siempre. - Validación de negocio (el criterio debe generar SQL válido y coherente contra el
catálogo). En este caso
messagees un string único con el mensaje de la regla incumplida y se detiene en el primer invariante violado. Solo corre si el cuerpo incluyequeryBlocks, y usa eltypeefectivo (del cuerpo o, si se omite, el actual del criterio).
Las reglas de negocio de la definición son idénticas a las de
Crear (catálogo de
tablas/columnas, "*" solo con COUNT, cobertura de GROUP BY, contexto de HAVING,
identidad del AUDIENCE, setOperator por posición del bloque, compatibilidad de operador
por tipo, forma de value, reglas del timeWindow, etc.).
Reglas específicas de composedWith (también 400, verificadas al recrear las
composiciones cuando se envía queryBlocks):
| Regla | Mensaje |
|---|---|
| Un criterio no puede referenciarse a sí mismo. | An audience criteria cannot reference itself |
| El operando debe existir. | Operand audience criteria <id> not found or deleted |
| No se permiten referencias circulares entre criterios. | Circular reference detected |
Ejemplos
Actualización parcial (solo metadatos)
Renombrar el criterio y limpiar su descripción, sin tocar la definición
(queryBlocks / composedWith se omiten, así que bloques y composiciones quedan intactos):
curl -X PATCH https://api.reten.ai/api/audience-criterias/a1b2c3d4-5678-4abc-9def-1234567890ab \
-H "Authorization: Bearer <token>" \
-H "x-tenant-id: <tenant-id>" \
-H "Content-Type: application/json" \
-d '{
"name": "Compradores VIP (renombrado)",
"description": null
}'import axios from 'axios';
const id = 'a1b2c3d4-5678-4abc-9def-1234567890ab';
const response = await axios.patch(
`https://api.reten.ai/api/audience-criterias/${id}`,
{ name: 'Compradores VIP (renombrado)', description: null },
{ headers: { Authorization: 'Bearer <token>', 'x-tenant-id': '<tenant-id>' } },
);
const updated = response.data;Reemplazo de la definición
Al incluir queryBlocks se reemplaza toda la definición. Aquí también se re-envía
composedWith para conservar la composición (si se omitiera, la composición se borraría):
curl -X PATCH https://api.reten.ai/api/audience-criterias/a1b2c3d4-5678-4abc-9def-1234567890ab \
-H "Authorization: Bearer <token>" \
-H "x-tenant-id: <tenant-id>" \
-H "Content-Type: application/json" \
-d '{
"name": "Compradores VIP (últimos 60 días)",
"queryBlocks": [
{
"order": 0,
"from": { "table": "event_transactions", "joins": [] },
"select": {
"distinct": false,
"items": [
{ "kind": "column", "column": { "table": "event_transactions", "column": "user_id" } },
{
"kind": "aggregate",
"aggregate": {
"aggregationOperator": "SUM",
"column": { "table": "event_transactions", "column": "pricing.final_amount" }
},
"alias": "total_gastado"
}
]
},
"timeWindow": {
"timeOperator": "IN_THE_LAST",
"reference": { "table": "event_transactions", "column": "event_date" },
"unit": "DAYS",
"value": 60
},
"where": {
"kind": "group",
"logicalOperator": "AND",
"children": [
{
"kind": "predicate",
"column": { "table": "event_transactions", "column": "status" },
"comparisonOperator": "EQUALS",
"value": "completed"
}
]
},
"groupBy": [
{ "table": "event_transactions", "column": "user_id" }
],
"having": {
"kind": "group",
"logicalOperator": "AND",
"children": [
{
"kind": "predicate",
"aggregate": {
"aggregationOperator": "SUM",
"column": { "table": "event_transactions", "column": "pricing.final_amount" }
},
"comparisonOperator": "GREATER_THAN",
"value": 100000
}
]
}
}
],
"composedWith": [
{
"operandCriteriaId": "b2c3d4e5-6789-4abc-9def-0123456789ab",
"setOperator": "INTERSECT_DISTINCT"
}
]
}'import axios from 'axios';
const id = 'a1b2c3d4-5678-4abc-9def-1234567890ab';
const body = {
name: 'Compradores VIP (últimos 60 días)',
queryBlocks: [
{
order: 0,
from: { table: 'event_transactions', joins: [] },
select: {
distinct: false,
items: [
{ kind: 'column', column: { table: 'event_transactions', column: 'user_id' } },
{
kind: 'aggregate',
aggregate: {
aggregationOperator: 'SUM',
column: { table: 'event_transactions', column: 'pricing.final_amount' },
},
alias: 'total_gastado',
},
],
},
timeWindow: {
timeOperator: 'IN_THE_LAST',
reference: { table: 'event_transactions', column: 'event_date' },
unit: 'DAYS',
value: 60,
},
where: {
kind: 'group',
logicalOperator: 'AND',
children: [
{
kind: 'predicate',
column: { table: 'event_transactions', column: 'status' },
comparisonOperator: 'EQUALS',
value: 'completed',
},
],
},
groupBy: [{ table: 'event_transactions', column: 'user_id' }],
having: {
kind: 'group',
logicalOperator: 'AND',
children: [
{
kind: 'predicate',
aggregate: {
aggregationOperator: 'SUM',
column: { table: 'event_transactions', column: 'pricing.final_amount' },
},
comparisonOperator: 'GREATER_THAN',
value: 100000,
},
],
},
},
],
composedWith: [
{
operandCriteriaId: 'b2c3d4e5-6789-4abc-9def-0123456789ab',
setOperator: 'INTERSECT_DISTINCT',
},
],
};
const response = await axios.patch(
`https://api.reten.ai/api/audience-criterias/${id}`,
body,
{ headers: { Authorization: 'Bearer <token>', 'x-tenant-id': '<tenant-id>' } },
);
const updated = response.data;Respuesta 200 OK
Devuelve el criterio ya actualizado como IAudienceCriteriaDetail — la misma forma
que GET /audience-criterias/:id y que
Crear. Es el recurso base más el árbol
completo: queryBlocks[] (con su id y order resueltos, ordenados por order
ascendente) y compositions[] (con id y operandCriteriaName).
Campos base (IAudienceCriteria) — todos camelCase; los instantes se serializan a ISO 8601:
| Campo | Tipo | Descripción |
|---|---|---|
id | string (UUID) | Id del criterio. |
name | string | Nombre (ya actualizado si se envió). |
description | string | null | null si está vacía. |
type | AudienceCriteriaTypeEnum | AUDIENCE | FILTER. |
isActive | boolean | Si el criterio está activo. |
ownerId | string (UUID) | Usuario creador (no cambia en la edición). |
updatedBy | string (UUID) | Usuario que realizó esta edición. |
createdAt | string (ISO 8601) | Fecha de creación. |
updatedAt | string (ISO 8601) | Se refresca con la edición. |
Relaciones anidadas:
queryBlocks[]— cada bloque añade suid(UUID) y devuelve el árbol tal como quedó (from,select,timeWindow,where,groupBy,having). Los campos opcionales ausentes se devuelven comonull(setOperator,timeWindow,groupBy,having) yfrom.joinscomo[]si no había joins.compositions[]— cada composición:id(UUID),operandCriteriaId,operandCriteriaName(nombre del operando resuelto;""si no resuelve) ysetOperator.
{
"id": "a1b2c3d4-5678-4abc-9def-1234567890ab",
"name": "Compradores VIP (últimos 60 días)",
"description": null,
"type": "AUDIENCE",
"isActive": true,
"ownerId": "0f0e0d0c-0b0a-4090-8070-605040302010",
"updatedBy": "1a2b3c4d-5e6f-4071-8092-a0b0c0d0e0f0",
"createdAt": "2026-07-15T14:32:07.481Z",
"updatedAt": "2026-07-15T16:05:44.902Z",
"queryBlocks": [
{
"id": "e5f6a7b8-9012-4abc-9def-567890abcdef",
"order": 0,
"setOperator": null,
"from": { "table": "event_transactions", "joins": [] },
"select": {
"distinct": false,
"items": [
{ "kind": "column", "column": { "table": "event_transactions", "column": "user_id" } },
{
"kind": "aggregate",
"aggregate": {
"aggregationOperator": "SUM",
"column": { "table": "event_transactions", "column": "pricing.final_amount" }
},
"alias": "total_gastado"
}
]
},
"timeWindow": {
"timeOperator": "IN_THE_LAST",
"reference": { "table": "event_transactions", "column": "event_date" },
"unit": "DAYS",
"value": 60
},
"where": {
"kind": "group",
"logicalOperator": "AND",
"children": [
{
"kind": "predicate",
"column": { "table": "event_transactions", "column": "status" },
"comparisonOperator": "EQUALS",
"value": "completed"
}
]
},
"groupBy": [
{ "table": "event_transactions", "column": "user_id" }
],
"having": {
"kind": "group",
"logicalOperator": "AND",
"children": [
{
"kind": "predicate",
"aggregate": {
"aggregationOperator": "SUM",
"column": { "table": "event_transactions", "column": "pricing.final_amount" }
},
"comparisonOperator": "GREATER_THAN",
"value": 100000
}
]
}
}
],
"compositions": [
{
"id": "f6a7b8c9-0123-4abc-9def-67890abcdef0",
"operandCriteriaId": "b2c3d4e5-6789-4abc-9def-0123456789ab",
"operandCriteriaName": "Usuarios activos",
"setOperator": "INTERSECT_DISTINCT"
}
]
}Respuestas de error
| Estado | Descripción |
|---|---|
400 | DTO inválido (tipo incorrecto, enum fuera de rango, propiedad desconocida, name vacío) o, cuando se envía queryBlocks, una regla de negocio incumplida (ver reglas de validación). |
401 | Falta o es inválido el Authorization: Bearer. |
403 | El usuario no tiene el permiso audience-criteria:update, o el x-tenant-id no está autorizado. |
404 | No existe un audience criteria con ese id en el tenant. |
Crear Audience Criteria
Crea un criterio de audiencia a partir de su definición completa (bloques de consulta, filtros, ventana temporal, agregaciones y composiciones).
Eliminar Audiencia
Archiva (soft delete) un audience criteria; responde 204 sin cuerpo. Falla con 400 si otro criterio lo referencia como operando.