diff --git a/alembic/versions/0012_fc3b_app_setting.py b/alembic/versions/0012_fc3b_app_setting.py new file mode 100644 index 0000000..42c06eb --- /dev/null +++ b/alembic/versions/0012_fc3b_app_setting.py @@ -0,0 +1,36 @@ +"""fc3b: app_setting key/value table + +Revision ID: 0012 +Revises: 0011 +Create Date: 2026-05-20 + +A simple key/value table for small app settings that don't fit +ImportSettings. Initially seeds only `extension_api_key` (done in +create_app on first boot — not in the migration, to keep it +deterministic and independent of randomness). +""" +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +revision: str = "0012" +down_revision: Union[str, None] = "0011" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "app_setting", + sa.Column("key", sa.String(length=64), primary_key=True), + sa.Column("value", sa.Text(), nullable=False), + sa.Column( + "updated_at", sa.DateTime(timezone=True), + nullable=False, server_default=sa.func.now(), + ), + ) + + +def downgrade() -> None: + op.drop_table("app_setting") diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index 25c2d12..609e2ff 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -1,5 +1,6 @@ """All ORM models. Import this module to make every model visible to Alembic.""" +from .app_setting import AppSetting from .artist import Artist from .base import Base from .credential import Credential @@ -22,6 +23,7 @@ from .tag_suggestion_rejection import TagSuggestionRejection __all__ = [ "Base", + "AppSetting", "Artist", "Source", "Credential", diff --git a/backend/app/models/app_setting.py b/backend/app/models/app_setting.py new file mode 100644 index 0000000..771843d --- /dev/null +++ b/backend/app/models/app_setting.py @@ -0,0 +1,24 @@ +"""AppSetting — small key/value app config (extension API key, etc.). + +For settings that need DB persistence + runtime mutation. Use +`ImportSettings` for the structured import-tuning settings; this +table is for one-off scalar values keyed by string. +""" + +from datetime import datetime + +from sqlalchemy import DateTime, String, Text, func +from sqlalchemy.orm import Mapped, mapped_column + +from .base import Base + + +class AppSetting(Base): + __tablename__ = "app_setting" + + key: Mapped[str] = mapped_column(String(64), primary_key=True) + value: Mapped[str] = mapped_column(Text, nullable=False) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), nullable=False, + server_default=func.now(), onupdate=func.now(), + ) diff --git a/tests/test_migration_0012.py b/tests/test_migration_0012.py new file mode 100644 index 0000000..a993ab4 --- /dev/null +++ b/tests/test_migration_0012.py @@ -0,0 +1,32 @@ +import pytest +from sqlalchemy import select + +from backend.app.models import AppSetting + +pytestmark = pytest.mark.integration + + +@pytest.mark.asyncio +async def test_app_setting_table_round_trip(db): + db.add(AppSetting(key="extension_api_key", value="abc123")) + await db.flush() + row = (await db.execute( + select(AppSetting).where(AppSetting.key == "extension_api_key") + )).scalar_one() + assert row.value == "abc123" + assert row.updated_at is not None + + +@pytest.mark.asyncio +async def test_app_setting_upsert(db): + db.add(AppSetting(key="k", value="v1")) + await db.flush() + row = (await db.execute( + select(AppSetting).where(AppSetting.key == "k") + )).scalar_one() + row.value = "v2" + await db.flush() + again = (await db.execute( + select(AppSetting.value).where(AppSetting.key == "k") + )).scalar_one() + assert again == "v2"