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 object

  • update - update any existing object

  • remove - 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)