Saltar a contenido

Escritura

Crear

POST /public/autor
{
  "nombre": "Ana",
  "publicaciones": [
    { "titulo": "Primera", "estado": "publicada" },
    { "titulo": "Segunda" }
  ]
}

Responde 201 con el registro creado, relaciones incluidas. Lo que se puede enviar es el DTO <Clase>Create de tai-sql:

  • Obligatorio lo que no tiene default ni admite null.
  • Opcional lo que tiene default, lo anulable y las claves foráneas, porque la fila relacionada puede venir anidada: una publicación se crea con su autor_id o con su autor.
  • Un campo que el DTO no conoce es un 422: los DTOs no admiten campos de más.
  • Las columnas calculadas, las autoincrementales y los campos operativos no se envían.

server_now=True no se envía

Una columna con column(server_now=True) la rellena la base de datos con NOW(): al crear es opcional. Con tai-sql anterior a la 0.7.13 el DTO la exigía y había que enviarla.

Crear varios a la vez

POST /public/autor/bulk
[ { "nombre": "Ana" }, { "nombre": "Luis" } ]

Responde 201 con la lista de registros creados, en el mismo orden que la petición. Cada elemento es el mismo DTO <Clase>Create de arriba, relaciones anidadas incluidas. Las columnas vectoriales vuelven a null, como en las lecturas: se guardan, pero no se devuelven.

  • Es todo o nada: van en una sola transacción, así que si uno falla —un duplicado, una clave foránea, un trigger, una fila que no es del usuario— no se crea ninguno.
  • Entre 1 y 1000 por petición: una lista vacía o de más de mil es un 422, antes de tocar la base de datos. Para más registros, se envían en varias peticiones.
  • En un 422 de validación, el field del error lleva la posición en la lista: 3.nombre es el campo nombre del cuarto elemento.
  • Con owner(), una fila que no sería del usuario es un 403 con details.reason: "OWNER" cuyo field dice cuál: 3 es el cuarto elemento, y 3.etiquetas, lo que se creó anidado en él.
  • Pide el mismo permiso que crear uno: <tabla>-create, y el de las tablas en las que escriban sus relaciones anidadas.

Varias tablas en una transacción

POST /public/_tx
{ "operations": [
  { "op": "create", "table": "autor", "values": { "nombre": "Ana" } },
  { "op": "create", "table": "publicacion", "values": { "titulo": "Primera", "autor_id": { "$ref": "0.id" } } },
  { "op": "update", "table": "publicacion", "key": 12, "values": { "estado": "retirada" } },
  { "op": "delete", "table": "etiqueta_publicacion", "key": { "publicacion_id": 12, "etiqueta": "vieja" } },
  { "op": "update_many", "table": "publicacion", "filter": { "autor_id": 3 }, "values": { "puntuacion": 0 } },
  { "op": "delete_many", "table": "publicacion", "filter": { "estado": "borrador" } }
] }

Un endpoint por schema que aplica las operaciones en orden y en una sola transacción: si una falla, no se aplica ninguna. Es lo que hay que usar cuando una escritura depende de otra —el movimiento de stock y el consumo que lo referencia—, en vez de encadenar peticiones y deshacer a mano si la segunda falla.

  • Cada operación es la de su endpoint: create con el <Clase>Create, update con la clave y el <Clase>UpdateValues, delete con la clave, update_many con filter y values como en PATCH /{tabla} (sin relaciones), delete_many con los filtros de DELETE /{tabla}. Mismas validaciones, mismo RLS, mismas reglas de propietario y mismos triggers.
  • key es el valor de la clave primaria, o un objeto con sus columnas si es compuesta.
  • { "$ref": "<i>.<campo>" } toma una columna del registro que creó la operación <i>, que tiene que ser anterior y un create. Vale en values, en key y en filter.
  • Los masivos sin filtro exigen "confirm_all": true en su operación, y delete_many rechaza un filtro que su endpoint no tiene: borraría de más.
  • Permisos: cada operación pide el de su endpoint (<tabla>-create, -update, -delete), y las escrituras anidadas de sus values, el de su tabla; se comprueban todos antes de escribir nada.
  • Respuesta: data es la lista de resultados, en orden: el registro creado o las filas afectadas.
  • Errores: el de la operación que falló, con su índice en field: operations.1.values.titulo (un campo que no vale), operations.2 (un 404, un 409, un 403 o el 422 de un trigger).
  • Hasta 100 operaciones. Solo tablas del schema: una vista no se escribe.

Actualizar un registro

PATCH /public/publicacion/12
{ "titulo": "Otro título", "etiquetas": { "create": [ { "etiqueta": "nueva" } ], "delete": [ { "publicacion_id": 12, "etiqueta": "vieja" } ] } }
  • Se envía solo lo que cambia. Lo que no admite null tampoco lo acepta aquí.
  • Las relaciones de uno a muchos se editan anidadas: create, update (con la clave de cada fila y sus values) y delete (la clave de cada fila; un objeto con sus columnas si es compuesta). Ver Escrituras anidadas: cada una pide el permiso de su tabla.
  • Devuelve cuántas filas cambiaron (0 o 1), no el registro.
  • Con Prefer: return=representation, devuelve el registro actualizado, leído en la misma transacción con los includes de la consulta y con el mismo RLS que cualquier lectura —lo que devolvería un GET después, sin hacerlo—, y la respuesta lleva Preference-Applied: return=representation:
PATCH /public/publicacion/12?includes=autor&includes=etiquetas
Prefer: return=representation
{ "titulo": "Otro título" }

Si el cambio deja la fila fuera del alcance del usuario (una regla de RLS que ya no la alcanza), no se puede devolver: data es el entero, sin Preference-Applied. El cliente TypeScript lo pide con update(clave, valores, { returning: true, includes }). - Si la fila no existe o el RLS no la deja ver: 404.

Escrituras anidadas

Lo que el cuerpo escribe en otras tablas —las filas relacionadas de un <Clase>Create, y los create/update/delete de un <Clase>UpdateValues— se trata como si fuera a su propio endpoint, a cualquier profundidad y en los cuatro caminos (POST, POST /bulk, PATCH /{tabla}/{clave} y _tx):

Qué se comprueba Si falla
Permiso el de la tabla hija: <hija>-create, -update o -delete (o sudo, <hija>-admin, allresources-<acción>), además del del padre. Antes de escribir nada 403 INSUFFICIENT_PERMISSIONS, details.required_permission
RLS y propietario un update o delete anidado tiene que alcanzar la fila con las reglas del usuario 404 RECORD_NOT_FOUND, como si no existiera (tampoco se distingue de una que no existe)
Propietario lo que se crea o se modifica anidado tiene que quedar del usuario 403 con details.reason: "OWNER"
Que cuelgue del padre un update o delete anidado nombra una hija de la fila que se actualiza: sus columnas de la relación (publicacion_id) valen lo que las de esa fila, a cualquier profundidad y con claves compuestas 404 RECORD_NOT_FOUND, como fuera del RLS

El field del error dice qué escritura fue: etiquetas.create.0, publicaciones.update.1.values.etiquetas.delete.0, 3.etiquetas.0 en /bulk, operations.2.values.etiquetas.0 en _tx (el 403 de propietario de algo creado lleva la relación sin posición: etiquetas.create). Todo va en la transacción del padre: si algo falla, no queda nada, tampoco el padre.

Una fila anidada no puede crear a su vez la fila de la que cuelga ({"etiquetas": {"create": [{"publicacion": {…}}]}}): es un 422 al validar el cuerpo, con la posición y la relación en el mensaje (etiquetas.create[0]: … no puede traer 'publicacion') y, en field, el modelo que lo rechaza (body en un POST, etiquetas.create en un PATCH). Y el PATCH masivo no lleva escrituras anidadas (ver abajo).

Un update anidado tampoco mueve la hija a otro padre: cambiar la relación ({"etiquetas": {"update": [{…, "values": {"publicacion_id": 2}}]}}) es un 422 VALIDATION_ERROR con field en esa columna (etiquetas.update.0.values.publicacion_id). Reenviar el valor que ya tiene vale. Para moverla, el PATCH de su propia tabla.

El RLS de las hijas y que cuelguen del padre los aplica el DAO de tai-sql (0.8.0 en adelante), que recibe las reglas del usuario y las baja a cada escritura anidada; sin eso, PATCH /publicacion/1 con {"etiquetas": {"delete": [{"publicacion_id": 2, "etiqueta": "x"}]}} borraba la etiqueta de la publicación 2.

Cambio de comportamiento: hasta tai-api 0.6.2 bastaba el permiso del padre

Antes, con receta-update se creaban, cambiaban y borraban paso_receta dentro del PATCH de la receta sin ningún permiso sobre paso_receta, y un update/delete anidado no comprobaba el RLS de la fila hija. Al actualizar, un frontend que hacía escrituras anidadas con un rol sin los permisos de las tablas hijas recibe 403. Hay que añadirlos al role() en rbac/<realm>.py —<hija>-create, -update, -delete, o <hija>-admin— y hacer tai-api rbac push. Para encontrarlos: el field del 403 dice la relación y details.required_permission, el permiso; las rules del proyecto (endpoints-<schema>.md) listan las tablas hijas de cada endpoint.

Actualizar o borrar muchos

PATCH  /public/publicacion              { "filter": { "autor_id": 3 }, "values": { "estado": "retirada" } }
DELETE /public/publicacion?autor_id=3

Sin ningún filtro se rechazan con un 422, salvo que la petición diga ?confirm_all=true. Todos los filtros son opcionales, así que una petición vacía sería un DELETE FROM tabla; operar sobre todas las filas sigue siendo posible, pero tiene que pedirse. Con autorización, «todas» son las que el RLS del usuario alcanza.

El PATCH masivo no lleva relaciones. Su cuerpo es <Clase>UpdateMany: values son las columnas (<Clase>UpdateManyValues), sin las escrituras anidadas de PATCH /{tabla}/{clave}. Un anidado nombra los hijos de un registro y un masivo tiene un filtro, no un registro: mandarlos es un 422 sobre values (field: "values.etiquetas"). Para los hijos, el PATCH por clave de cada padre, o la tabla hija directamente.

Un in_ con la lista vacía no alcanza ninguna fila. {"filter": {"in_estado": []}, …} pide las filas cuyo estado es uno de cero valores: el PATCH responde 0 y no toca nada, con confirm_all o sin él (lo mismo update_many y delete_many en _tx). Hasta tai-api 0.6.2 el DAO trataba la lista vacía como «sin filtro» y la comprobación de filtro vacío no lo paraba —había una clave—: se actualizaban todas las filas del usuario. En la query string no pasa: una lista vacía no viaja.

Borrar un registro

DELETE /public/publicacion/12     → data: 1

Lo protegido

encrypt=True hash=True
Se escribe en claro, y se guarda cifrado en claro, y se guarda hasheado
Se lee descifrado no se puede: no aparece en la respuesta
Se filtra no no

Toda escritura —crear, actualizar, las masivas— pasa por el mismo camino que cifra o hashea: no hay ninguna que guarde el valor en claro.