View Dependencies

As you already know, in the process of its work, FastAPI-JSONAPI interacts between application layers. Sometimes there are things that are necessary to process requests but are only computable at runtime. In order for ResourceManager and DataLayer to use such things, there is a mechanism called operation_dependencies.

The most common cases of such things are database session and access handling. The example below demonstrates some simple implementation of these ideas using sqlalchemy.

Example:

from typing import Optional, ClassVar

from fastapi import Depends, Header
from pydantic import BaseModel, ConfigDict
from sqlalchemy.ext.asyncio import AsyncSession
from typing_extensions import Annotated

from examples.api_for_sqlalchemy.models.db import DB
from fastapi_jsonapi.exceptions import Forbidden
from fastapi_jsonapi.misc.sqla.generics.base import ViewBaseGeneric
from fastapi_jsonapi.views import ViewBase, Operation, OperationConfig

db = DB(
    url="sqlite+aiosqlite:///tmp/db.sqlite3",
)


class SessionDependency(BaseModel):
    model_config = ConfigDict(
        arbitrary_types_allowed=True,
    )

    session: AsyncSession = Depends(db.session)


async def common_handler(view: ViewBase, dto: SessionDependency) -> dict:
    return {
        "session": dto.session,
    }


async def check_that_user_is_admin(x_auth: Annotated[str, Header()]):
    if x_auth != "admin":
        raise Forbidden(detail="Only admin user have permissions to this endpoint.")


class AdminOnlyPermission(BaseModel):
    is_admin: Optional[bool] = Depends(check_that_user_is_admin)


class View(ViewBaseGeneric):
    operation_dependencies: ClassVar[dict[Operation, OperationConfig]] = {
        Operation.ALL: OperationConfig(
            dependencies=SessionDependency,
            prepare_data_layer_kwargs=common_handler,
        ),
        Operation.GET: OperationConfig(
            dependencies=AdminOnlyPermission,
        ),
    }

In this example, the focus should be on the Operation and OperationConfig entities. By setting the operation_dependencies attribute, you can set FastAPI dependencies for endpoints, as well as manage the creation of additional kwargs needed to initialize the DataLayer.

Dependencies can be any Pydantic model containing Depends as default values. It’s really the same as if you defined the dependency session for the endpoint as:

from fastapi import FastAPI, Depends
from sqlalchemy.ext.asyncio import AsyncSession

app = FastAPI()


@app.get("/items")
def get_items(session: AsyncSession = Depends(async_session_dependency)):
    pass

Dependencies do not have to be used to generate DataLayer keys and can be used for any purpose, as is the case with the check_that_user_is_admin function, which is used to check permissions. In case the header “X-AUTH” is not equal to “admin”, the Forbidden response will be returned.

In this case, if you do not set the “X-AUTH” header, it will work like this

Request:

GET /users HTTP/1.1
Content-Type: application/vnd.api+json

Response:

HTTP/1.1 403 Forbidden
Content-Type: application/vnd.api+json

{
   "errors": [
      {
         "detail": "Only admin user have permissions to this endpoint",
         "status_code": 403,
         "title": "Forbidden"
      }
   ]
}

and when “X-AUTH” is set, it will work like this

Request:

GET /users HTTP/1.1
Content-Type: application/vnd.api+json
X-AUTH: admin

Response:

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "data": [
    {
      "attributes": {
        "name": "John"
      },
      "id": "1",
      "links": {
        "self": "/users/1"
      },
      "type": "user"
    }
  ],
  "jsonapi": {
    "version": "1.0"
  },
  "links": {
    "self": "http://localhost:5000/users"
  },
  "meta": {
    "count": 1
  }
}

Handlers

As noted above, dependencies can be used to create a kwargs for a DataLayer. To do this, you need to define prepare_data_layer_kwargs in OperationConfig. This is a callable object which can be synchronous or asynchronous.

Its signature should look like this

async def my_handler(view: ViewBase, dto: BaseModel) -> dict[str, Any]:
    pass

or this

async def my_handler(view: ViewBase) -> dict[str, Any]:
    pass

In the case of dto, it is an instance of the class corresponds to what is in OperationConfig.dependencies and should only be present in the function signature if dependencies is not None.

The OperationConfig.ALL method has special behavior. When declared, its dependencies will be passed to each endpoint regardless of the existence of other configs.

Explaining with a specific example, in the case when Operation.ALL is declared and it has dependencies, and also a method such as Operation.GET also has dependencies, the signature for the Operation.GET handler will be a union of dependencies

Example:

from typing import ClassVar

from fastapi import Depends
from pydantic import BaseModel

from fastapi_jsonapi.misc.sqla.generics.base import ViewBaseGeneric
from fastapi_jsonapi.views import ViewBase, Operation, OperationConfig


def one():
    return 1


def two():
    return 2


class CommonDependency(BaseModel):
    key_1: int = Depends(one)


class GetDependency(BaseModel):
    key_2: int = Depends(two)


class DependencyMix(CommonDependency, GetDependency):
    pass


def common_handler(view: ViewBase, dto: CommonDependency) -> dict:
    return {"key_1": dto.key_1}


def get_handler(view: ViewBase, dto: DependencyMix):
    return {"key_2": dto.key_2}


class View(ViewBaseGeneric):
    operation_dependencies: ClassVar = {
        Operation.ALL: OperationConfig(
            dependencies=CommonDependency,
            prepare_data_layer_kwargs=common_handler,
        ),
        Operation.GET: OperationConfig(
            dependencies=GetDependency,
            prepare_data_layer_kwargs=get_handler,
        ),
    }

In this case DataLayer.__init__ will get {"key_1": 1, "key_2": 2} as kwargs.

You can take advantage of this knowledge and do something with the key_1 value, because before entering the DataLayer, the results of both handlers are defined as:

dl_kwargs = common_handler(view, dto)
dl_kwargs.update(get_handler(view, dto))

You can override the value of key_1 in the handler

def get_handler(view: ViewBase, dto: DependencyMix):
    return {"key_1": 42, "key_2": dto.key_2}

or just overriding the dependency

def handler(view, dto):
    return 42

class GetDependency(BaseModel):
    key_1: int = Depends(handler)
    key_2: int = Depends(two)

In both cases DataLayer.__init__ will get {"key_1": 42, "key_2": 2} as kwargs