Atomic Operations
Atomic Operations allows to perform multiple “operations” in a linear and atomic manner. Operations are a serialized form of the mutations allowed in the base JSON:API specification.
Clients can send an array of operations in a single request. This extension guarantees that those operations will be processed in order and will either completely succeed or fail together.
What can I do?
Atomic operations extension supports these three actions:
add
- create a new objectupdate
- update any existing objectremove
- delete any existing object
You can send one or more atomic operations in one request.
If anything fails in one of the operations, everything will be rolled back.
Note
Only SQLAlchemy data layer supports atomic operations right now. Feel free to send PRs to add support for other data layers.
Configuration
You need to include atomic router:
from fastapi import FastAPI
from fastapi_jsonapi.atomic import AtomicOperations
def add_routes(app: FastAPI):
atomic = AtomicOperations()
app.include_router(atomic.router)
Default path for atomic operations is /operations
There’s a way to customize url path, you can also pass your custom APIRouter:
from fastapi import APIRouter
from fastapi_jsonapi.atomic import AtomicOperations
my_router = APIRouter(prefix="/qwerty", tags=["Atomic Operations"])
AtomicOperations(
# you can pass custom url path
url_path="/atomic",
# also you can pass your custom router
router=my_router,
)
Create some objects
Create two objects, they are not linked anyhow:
Request:
POST /operations HTTP/1.1
Content-Type: application/vnd.api+json
{
"atomic:operations": [
{
"op": "add",
"data": {
"type": "computer",
"attributes": {
"name": "Commodore"
}
}
},
{
"op": "add",
"data": {
"type": "user",
"attributes": {
"first_name": "Kate",
"last_name": "Grey"
}
}
}
]
}
Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"atomic:results": [
{
"data": {
"attributes": {
"name": "Commodore"
},
"id": "4",
"type": "computer"
},
"meta": null
},
{
"data": {
"attributes": {
"age": null,
"email": null,
"first_name": "Kate",
"last_name": "Grey",
"status": "active"
},
"id": "5",
"type": "user"
},
"meta": null
}
]
}
Update object
Update details
Atomic operations array has to contain at least one operation. Body in each atomic action has to be as in other regular requests. For example, update any existing object:
POST /operations HTTP/1.1
Content-Type: application/vnd.api+json
{
"atomic:operations": [
{
"op": "update",
"data": {
"id": "5",
"type": "user",
"attributes": {
"last_name": "White",
"email": "kate@example.com"
}
}
}
]
}
Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"atomic:results": [
{
"data": {
"attributes": {
"age": null,
"email": "kate@example.com",
"first_name": "Kate",
"last_name": "White",
"status": "active"
},
"id": "5",
"type": "user"
},
"meta": null
}
]
}
Update details and relationships
Warning
There may be issues when updating to-many relationships. This feature is not fully-tested yet.
Update already any existing computer and link it to any existing user:
Request:
POST /operations HTTP/1.1
Content-Type: application/vnd.api+json
{
"atomic:operations": [
{
"op": "update",
"data": {
"id": "4",
"type": "computer",
"attributes": {
"name": "Commodore PET"
},
"relationships": {
"user": {
"data": {
"id": "5",
"type": "user"
}
}
}
}
}
]
}
Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"atomic:results": [
{
"data": {
"attributes": {
"name": "Commodore PET"
},
"id": "4",
"type": "computer"
},
"meta": null
}
]
}
You can check that details and relationships are updated by fetching the object and related objects:
Request:
GET /computers/4?include=user HTTP/1.1
Content-Type: application/vnd.api+json
Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"data": {
"attributes": {
"name": "Commodore PET"
},
"id": "4",
"relationships": {
"user": {
"data": {
"id": "5",
"type": "user"
}
}
},
"type": "computer"
},
"included": [
{
"attributes": {
"age": null,
"email": "kate@example.com",
"first_name": "Kate",
"last_name": "White",
"status": "active"
},
"id": "5",
"type": "user"
}
],
"jsonapi": {
"version": "1.0"
},
"meta": null
}
Remove object
Operations include remove object action
You can mix any actions, for example you can create, update, remove at the same time:
Request:
POST /operations HTTP/1.1
Content-Type: application/vnd.api+json
{
"atomic:operations": [
{
"op": "add",
"data": {
"type": "computer",
"attributes": {
"name": "Liza"
},
"relationships": {
"user": {
"data": {
"id": "1",
"type": "user"
}
}
}
}
},
{
"op": "update",
"data": {
"id": "2",
"type": "user_bio",
"attributes": {
"birth_city": "Saint Petersburg",
"favourite_movies": "\"The Good, the Bad and the Ugly\", \"Once Upon a Time in America\""
}
}
},
{
"op": "remove",
"ref": {
"id": "2",
"type": "child"
}
}
]
}
Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"atomic:results": [
{
"data": {
"attributes": {
"name": "Liza"
},
"id": "5",
"type": "computer"
},
"meta": null
},
{
"data": {
"attributes": {
"birth_city": "Saint Petersburg",
"favourite_movies": "\"The Good, the Bad and the Ugly\", \"Once Upon a Time in America\"",
"keys_to_ids_list": null,
},
"id": "2",
"type": "user_bio"
},
"meta": null
},
{
"data": null,
"meta": null
}
]
}
All operations remove objects
If all actions are to delete objects, empty response will be returned:
Request:
POST /operations HTTP/1.1
Content-Type: application/vnd.api+json
{
"atomic:operations": [
{
"op": "remove",
"ref": {
"id": "6",
"type": "computer"
}
},
{
"op": "remove",
"ref": {
"id": "7",
"type": "computer"
}
}
]
}
Response:
HTTP/1.1 204 No Content
Local identifier
Sometimes you need to create an object, create another object and link it to the first one:
Create user and create bio for this user:
Request:
POST /operations HTTP/1.1
Content-Type: application/vnd.api+json
{
"atomic:operations":[
{
"op":"add",
"data":{
"lid":"some-local-id",
"type":"user",
"attributes":{
"first_name":"Bob",
"last_name":"Pink"
}
}
},
{
"op":"add",
"data":{
"type":"user_bio",
"attributes":{
"birth_city":"Moscow",
"favourite_movies":"Jaws, Alien"
},
"relationships":{
"user":{
"data":{
"lid":"some-local-id",
"type":"user"
}
}
}
}
}
]
}
Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"atomic:results": [
{
"data": {
"attributes": {
"age": null,
"email": null,
"first_name": "Bob",
"last_name": "Pink",
"status": "active"
},
"id": "7",
"type": "user"
},
"meta": null
},
{
"data": {
"attributes": {
"birth_city": "Moscow",
"favourite_movies": "Jaws, Alien"
},
"id": "2",
"type": "user_bio"
},
"meta": null
}
]
}
Many to many with local identifier
If you have many-to-many association (examples with many-to-many), atomic operations should look like this:
Request:
POST /operations HTTP/1.1
Content-Type: application/vnd.api+json
{
"atomic:operations":[
{
"op":"add",
"data":{
"lid":"new-parent",
"type":"parent",
"attributes":{
"name":"David Newton"
}
}
},
{
"op":"add",
"data":{
"lid":"new-child",
"type":"child",
"attributes":{
"name":"James Owen"
}
}
},
{
"op":"add",
"data":{
"type":"parent-to-child-association",
"attributes":{
"extra_data":"Lay piece happy box."
},
"relationships":{
"parent":{
"data":{
"lid":"new-parent",
"type":"parent"
}
},
"child":{
"data":{
"lid":"new-child",
"type":"child"
}
}
}
}
}
]
}
Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"atomic:results": [
{
"data": {
"attributes": {
"name": "David Newton"
},
"id": "1",
"type": "parent"
},
"meta": null
},
{
"data": {
"attributes": {
"name": "James Owen"
},
"id": "1",
"type": "child"
},
"meta": null
},
{
"data": {
"attributes": {
"extra_data": "Lay piece happy box."
},
"id": "1",
"type": "parent-to-child-association"
},
"meta": null
}
]
}
Check that objects and relationships were created. Pass includes in the url path, like this
/parent-to-child-association/1?include=parent,child
Request:
GET /parent-to-child-association/1?include=parent%2Cchild HTTP/1.1
Content-Type: application/vnd.api+json
Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"data": {
"attributes": {
"extra_data": "Lay piece happy box."
},
"id": "1",
"relationships": {
"child": {
"data": {
"id": "1",
"type": "child"
}
},
"parent": {
"data": {
"id": "1",
"type": "parent"
}
}
},
"type": "parent-to-child-association"
},
"included": [
{
"attributes": {
"name": "James Owen"
},
"id": "1",
"type": "child"
},
{
"attributes": {
"name": "David Newton"
},
"id": "1",
"type": "parent"
}
],
"jsonapi": {
"version": "1.0"
},
"meta": null
}
Errors
If any action on the operations list fails, everything will be rolled back and an error will be returned. Example:
Request:
POST /operations HTTP/1.1
Content-Type: application/vnd.api+json
{
"atomic:operations": [
{
"op": "add",
"data": {
"type": "computer",
"attributes": {
"name": "Commodore"
}
}
},
{
"op": "update",
"data": {
"type": "user_bio",
"attributes": {
"favourite_movies": "Saw"
}
}
}
]
}
Response:
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
{
"detail": {
"data": {
"attributes": {
"favourite_movies": "Saw"
},
"id": null,
"lid": null,
"relationships": null,
"type": "user_bio"
},
"errors": [
{
"loc": [
"data",
"attributes",
"birth_city"
],
"msg": "field required",
"type": "value_error.missing"
},
{
"loc": [
"data",
"id"
],
"msg": "none is not an allowed value",
"type": "type_error.none.not_allowed"
}
],
"message": "Validation error on operation update",
"ref": null
}
}
Since update
action requires id
field and user-bio update schema requires birth_city
field,
the app rollbacks all actions and computer is not saved in DB (and user-bio is not updated).
Error is not in JSON:API style yet, PRs are welcome.
Notes
Note
See examples for SQLAlchemy in the repo, all examples are based on it.
Note
Atomic Operations provide current_atomic_operation
context variable.
Usage example can be found in tests test_current_atomic_operation.
Warning
Field “href” is not supported yet. Resource can be referenced only by the “type” field.
Relationships resources are not implemented yet, so updating relationships directly through atomic operations is not supported too (see skipped tests).
Includes in the response body are not supported (and not planned, until you PR it)