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_ido con suautor. - 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¶
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
fielddel error lleva la posición en la lista:3.nombrees el camponombredel cuarto elemento. - Con
owner(), una fila que no sería del usuario es un 403 condetails.reason: "OWNER"cuyofielddice cuál:3es el cuarto elemento, y3.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:
createcon el<Clase>Create,updatecon la clave y el<Clase>UpdateValues,deletecon la clave,update_manyconfilteryvaluescomo enPATCH /{tabla}(sin relaciones),delete_manycon los filtros deDELETE /{tabla}. Mismas validaciones, mismo RLS, mismas reglas de propietario y mismos triggers. keyes 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 uncreate. Vale envalues, enkeyy enfilter.- Los masivos sin filtro exigen
"confirm_all": trueen su operación, ydelete_manyrechaza 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 susvalues, el de su tabla; se comprueban todos antes de escribir nada. - Respuesta:
dataes 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
nulltampoco lo acepta aquí. - Las relaciones de uno a muchos se editan anidadas:
create,update(con la clave de cada fila y susvalues) ydelete(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 losincludesde la consulta y con el mismo RLS que cualquier lectura —lo que devolvería unGETdespués, sin hacerlo—, y la respuesta llevaPreference-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¶
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.