d582f15681
- 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.
86 lines
2.6 KiB
Python
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()")
|