Files
finance/backend/alembic/versions/20260911_0001_initial_schema.py
T
k1nq d582f15681 feat: add tests for import polling, login page, money utilities, and query keys
- Implemented tests for import polling functionality, ensuring correct behavior for active and terminal import statuses.
- Created tests for the login page, validating user interactions and error handling.
- Added unit tests for money utility functions, covering parsing, formatting, and summation of monetary values.
- Developed tests for query keys, ensuring proper scoping and invalidation roots.

chore: setup testing environment and configuration

- Added setup file for testing with Vitest and Testing Library.
- Configured TypeScript and Vite for the frontend project.
- Created Docker and Nginx configurations for local development and production environments.
- Established a PostgreSQL initialization script for setting up application roles.

docs: add README and environment configuration for infra

- Included README for the infra directory, detailing local development setup and service descriptions.
- Created an example environment configuration file for local development with placeholders for sensitive information.
2026-09-14 09:26:33 +05:00

86 lines
2.6 KiB
Python

"""Initial modular-monolith schema and tenant policies."""
from alembic import op
from app.db import models as _models # noqa: F401
from app.db.base import Base
revision = "0001"
down_revision = None
branch_labels = None
depends_on = None
TENANT_TABLES = (
"accounts",
"operations",
"postings",
"categories",
"budget_periods",
"plan_assignments",
"expected_incomes",
"expected_income_matches",
"funding_movements",
"period_closures",
"income_sources",
"payees",
"payee_aliases",
"categorization_rules",
"statement_documents",
"parse_runs",
"source_records",
"source_record_links",
"dedup_candidates",
"data_exports",
)
def upgrade() -> None:
bind = op.get_bind()
Base.metadata.create_all(bind=bind)
if bind.dialect.name != "postgresql":
return
for table in TENANT_TABLES:
op.execute(f"ALTER TABLE {table} ENABLE ROW LEVEL SECURITY")
op.execute(f"ALTER TABLE {table} FORCE ROW LEVEL SECURITY")
op.execute(
f"CREATE POLICY tenant_isolation ON {table} "
"USING (space_id = NULLIF(current_setting('app.space_id', true), '')::uuid) "
"WITH CHECK (space_id = NULLIF(current_setting('app.space_id', true), '')::uuid)"
)
op.execute(
"""
CREATE FUNCTION prevent_last_owner_removal() RETURNS trigger AS $$
BEGIN
IF OLD.role = 'owner' AND (TG_OP = 'DELETE' OR NEW.role <> 'owner') AND
NOT EXISTS (
SELECT 1 FROM memberships
WHERE space_id = OLD.space_id AND role = 'owner' AND id <> OLD.id
) THEN
RAISE EXCEPTION 'financial space must retain an owner';
END IF;
RETURN CASE WHEN TG_OP = 'DELETE' THEN OLD ELSE NEW END;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER memberships_keep_owner
BEFORE UPDATE OR DELETE ON memberships
FOR EACH ROW EXECUTE FUNCTION prevent_last_owner_removal();
"""
)
op.execute(
"""
CREATE FUNCTION audit_events_append_only() RETURNS trigger AS $$
BEGIN RAISE EXCEPTION 'audit events are append-only'; END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER audit_events_no_mutation
BEFORE UPDATE OR DELETE ON audit_events
FOR EACH ROW EXECUTE FUNCTION audit_events_append_only();
"""
)
def downgrade() -> None:
bind = op.get_bind()
Base.metadata.drop_all(bind=bind)
if bind.dialect.name == "postgresql":
op.execute("DROP FUNCTION IF EXISTS prevent_last_owner_removal()")
op.execute("DROP FUNCTION IF EXISTS audit_events_append_only()")