Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ we use a Postgres docker container to generate migrations against.
At runtime the task receives database connection details through environment variables
(`DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD`) sourced from AWS Secrets Manager.

After making any changes to the database models, after ensuring that Docker is runnning on your local machine,
After making any changes to the database models, after ensuring that Docker is runnning on your local machine,
run the `generate_migrations.py` script to create migrations:

```shell
Expand Down Expand Up @@ -120,7 +120,7 @@ rm database.db
```

2. Run the FastAPI app once to create the database

```bash
uv run fastapi dev
```
Expand All @@ -139,6 +139,26 @@ uv run python run_scheduler.py --immediate
uv run fastapi dev main.py
```

# Admin dashboard

We use [Starlette Admin](https://jowilf.github.io/starlette-admin/) to provide an admin
dashboard for the database. You can access it at `<backend-url>/db-admin/`.
Currently this is **view-only**, and only used to view the DB entries for debugging.

The admin dashboard is **only enabled if additional variables are set in the environment (`.env` or environment variables)**:

```
ADMIN_CLIENT_ID
ADMIN_CLIENT_SECRET
AUTH0_CUSTOM_DOMAIN
```

These should be the client ID and secret for an Auth0 application that
allows a callback at `<backend-url>/db-admin/auth/auth0`

**⚠️ Currently, this does not work with the application used for
the rest of the backend (not clear why) - it needs to be a separate Auth0 application.**

# Job scheduler

We use the `apscheduler` library to schedule recurring jobs. Currently
Expand Down
226 changes: 0 additions & 226 deletions db/admin.py

This file was deleted.

25 changes: 25 additions & 0 deletions db/admin_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from typing import Optional

from pydantic import field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict


class AdminSettings(BaseSettings):
"""
Settings used by starlette-admin, which currently requires a separate
Auth0 app to that used by the main backend service
"""
auth0_custom_domain: Optional[str] = None
admin_client_id: Optional[str] = None
admin_client_secret: Optional[str] = None
model_config = SettingsConfigDict(env_file=".env", extra="ignore")

@field_validator("auth0_custom_domain", mode="before")
def strip_trailing_slash(cls, value: str) -> str:
if isinstance(value, str):
return value.rstrip("/")
return value


def get_admin_settings() -> AdminSettings:
return AdminSettings()
22 changes: 13 additions & 9 deletions db/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

from typing import Any, ClassVar

from sqlalchemy import MetaData, event, select
from sqlalchemy import MetaData, event, select, true
from sqlalchemy import inspect as sa_inspect
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session as SASession
from sqlalchemy.orm import with_loader_criteria
from sqlalchemy.orm import class_mapper, with_loader_criteria
from sqlalchemy.sql import expression
from sqlmodel import Field, Session, SQLModel

Expand Down Expand Up @@ -137,12 +137,16 @@ def _identity_dict_from_instance(instance: SoftDeleteModel) -> dict[str, Any] |
return identity


def _soft_delete_filter(cls) -> Any:
mapper = sa_inspect(cls, raiseerr=False)
if mapper is None:
return expression.true()
column = mapper.c.is_deleted
return column.is_(False)
def _soft_delete_filter(entity_cls) -> Any:
try:
# ensure this is a mapped class and has the column
class_mapper(entity_cls)
col = getattr(entity_cls, "is_deleted", None)
if col is not None:
return col.is_(False)
except Exception:
pass
return true()


@event.listens_for(SASession, "before_flush")
Expand Down Expand Up @@ -178,7 +182,7 @@ def _filter_soft_deleted(execute_state) -> None:
execute_state.statement = execute_state.statement.options(
with_loader_criteria(
SoftDeleteModel,
lambda cls: _soft_delete_filter(cls),
_soft_delete_filter,
include_aliases=True,
)
)
Expand Down
Loading