feat(07-01): Alembic migration 0005 + SystemSettings ORM model
- Add backend/migrations/versions/0005_system_settings.py (revision 0005, down_revision 0004) - Creates system_settings table with 9 columns: id (UUID PK gen_random_uuid()), provider_id (String NOT NULL UNIQUE), api_key_enc (Text NULL), base_url (Text NULL), model_name (Text NOT NULL default ''), context_chars (Integer NOT NULL default 8000), is_active (Boolean NOT NULL default false), created_at/updated_at (TIMESTAMPTZ default now()) - UniqueConstraint uq_system_settings_provider_id on provider_id - Add SystemSettings ORM model to backend/db/models.py following CloudConnection style - downgrade() drops system_settings table cleanly
This commit is contained in:
@@ -335,3 +335,43 @@ class Group(Base):
|
|||||||
created_at: Mapped[datetime] = mapped_column(
|
created_at: Mapped[datetime] = mapped_column(
|
||||||
TIMESTAMP(timezone=True), nullable=False, server_default=func.now()
|
TIMESTAMP(timezone=True), nullable=False, server_default=func.now()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SystemSettings(Base):
|
||||||
|
"""AI provider configuration table — one row per provider.
|
||||||
|
|
||||||
|
Stores the active AI provider's API key (Fernet-encrypted), base URL,
|
||||||
|
model name, and context window size. Only one row has is_active=TRUE
|
||||||
|
at any given time; the admin panel flips all rows atomically.
|
||||||
|
|
||||||
|
Encryption (D-05):
|
||||||
|
api_key_enc is encrypted with HKDF/Fernet using info=b"ai-provider-settings"
|
||||||
|
for domain separation from cloud credentials (info=b"cloud-credentials").
|
||||||
|
Master key is settings.cloud_creds_key (env var CLOUD_CREDS_KEY).
|
||||||
|
See services/ai_config.py for the encryption helpers.
|
||||||
|
|
||||||
|
Design reference: 07-RESEARCH.md Pattern 1.
|
||||||
|
Migration: 0005_system_settings.py.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "system_settings"
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||||
|
)
|
||||||
|
provider_id: Mapped[str] = mapped_column(String, nullable=False, unique=True)
|
||||||
|
api_key_enc: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||||
|
base_url: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||||
|
model_name: Mapped[str] = mapped_column(Text, nullable=False, default="")
|
||||||
|
context_chars: Mapped[int] = mapped_column(Integer, nullable=False, default=8000)
|
||||||
|
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(
|
||||||
|
TIMESTAMP(timezone=True), nullable=False, server_default=func.now()
|
||||||
|
)
|
||||||
|
updated_at: Mapped[datetime] = mapped_column(
|
||||||
|
TIMESTAMP(timezone=True), nullable=False, server_default=func.now()
|
||||||
|
)
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint("provider_id", name="uq_system_settings_provider_id"),
|
||||||
|
)
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
"""Add system_settings table for AI provider configuration.
|
||||||
|
|
||||||
|
Revision ID: 0005
|
||||||
|
Revises: 0004
|
||||||
|
Create Date: 2026-06-04
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
1. Create system_settings table with one row per AI provider.
|
||||||
|
Stores: api_key_enc (Fernet-encrypted), base_url, model_name,
|
||||||
|
context_chars, is_active flag, and timestamps.
|
||||||
|
2. Add UNIQUE constraint on provider_id (uq_system_settings_provider_id)
|
||||||
|
to enforce one row per provider name.
|
||||||
|
|
||||||
|
Design notes (07-RESEARCH.md Pattern 1):
|
||||||
|
- One row per provider (not key-value store) — typed columns, enforced uniqueness.
|
||||||
|
- id column uses gen_random_uuid() server default.
|
||||||
|
- api_key_enc is Text nullable (NULL for local providers like Ollama/LMStudio).
|
||||||
|
- is_active = TRUE for the single active provider; all others FALSE.
|
||||||
|
- Encryption uses HKDF with info=b"ai-provider-settings" (domain separation from
|
||||||
|
cloud credentials which use info=b"cloud-credentials" — same master key, different
|
||||||
|
derived keys; see services/ai_config.py).
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "0005"
|
||||||
|
down_revision = "0004"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.create_table("system_settings",
|
||||||
|
sa.Column(
|
||||||
|
"id",
|
||||||
|
PG_UUID(as_uuid=True),
|
||||||
|
primary_key=True,
|
||||||
|
server_default=sa.text("gen_random_uuid()"),
|
||||||
|
),
|
||||||
|
sa.Column("provider_id", sa.String, nullable=False),
|
||||||
|
sa.Column("api_key_enc", sa.Text, nullable=True),
|
||||||
|
sa.Column("base_url", sa.Text, nullable=True),
|
||||||
|
sa.Column(
|
||||||
|
"model_name",
|
||||||
|
sa.Text,
|
||||||
|
nullable=False,
|
||||||
|
server_default="",
|
||||||
|
),
|
||||||
|
sa.Column(
|
||||||
|
"context_chars",
|
||||||
|
sa.Integer,
|
||||||
|
nullable=False,
|
||||||
|
server_default="8000",
|
||||||
|
),
|
||||||
|
sa.Column(
|
||||||
|
"is_active",
|
||||||
|
sa.Boolean,
|
||||||
|
nullable=False,
|
||||||
|
server_default="false",
|
||||||
|
),
|
||||||
|
sa.Column(
|
||||||
|
"created_at",
|
||||||
|
sa.TIMESTAMP(timezone=True),
|
||||||
|
nullable=False,
|
||||||
|
server_default=sa.text("now()"),
|
||||||
|
),
|
||||||
|
sa.Column(
|
||||||
|
"updated_at",
|
||||||
|
sa.TIMESTAMP(timezone=True),
|
||||||
|
nullable=False,
|
||||||
|
server_default=sa.text("now()"),
|
||||||
|
),
|
||||||
|
sa.UniqueConstraint("provider_id", name="uq_system_settings_provider_id"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_table("system_settings")
|
||||||
Reference in New Issue
Block a user