Use capability-based module boundaries

This commit is contained in:
2026-07-10 17:33:53 +02:00
parent 15d5613f9b
commit 15aa4f3df3
6 changed files with 26 additions and 10 deletions

View File

@@ -29,7 +29,7 @@ class CalendarCollection(Base, TimestampMixin):
) )
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
tenant_id: Mapped[str] = mapped_column(ForeignKey("tenancy_tenants.id", ondelete="CASCADE"), nullable=False, index=True) tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
slug: Mapped[str] = mapped_column(String(100), nullable=False) slug: Mapped[str] = mapped_column(String(100), nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False) name: Mapped[str] = mapped_column(String(255), nullable=False)
description: Mapped[str | None] = mapped_column(Text) description: Mapped[str | None] = mapped_column(Text)
@@ -56,7 +56,7 @@ class CalendarEvent(Base, TimestampMixin):
) )
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
tenant_id: Mapped[str] = mapped_column(ForeignKey("tenancy_tenants.id", ondelete="CASCADE"), nullable=False, index=True) tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
calendar_id: Mapped[str] = mapped_column(ForeignKey("calendar_collections.id", ondelete="CASCADE"), nullable=False, index=True) calendar_id: Mapped[str] = mapped_column(ForeignKey("calendar_collections.id", ondelete="CASCADE"), nullable=False, index=True)
uid: Mapped[str] = mapped_column(String(255), nullable=False, index=True) uid: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
recurrence_id: Mapped[str | None] = mapped_column(String(255), nullable=True, index=True) recurrence_id: Mapped[str | None] = mapped_column(String(255), nullable=True, index=True)
@@ -110,7 +110,7 @@ class CalendarSyncSource(Base, TimestampMixin):
) )
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
tenant_id: Mapped[str] = mapped_column(ForeignKey("tenancy_tenants.id", ondelete="CASCADE"), nullable=False, index=True) tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
calendar_id: Mapped[str] = mapped_column(ForeignKey("calendar_collections.id", ondelete="CASCADE"), nullable=False, index=True) calendar_id: Mapped[str] = mapped_column(ForeignKey("calendar_collections.id", ondelete="CASCADE"), nullable=False, index=True)
source_kind: Mapped[str] = mapped_column(String(30), default="caldav", nullable=False, index=True) source_kind: Mapped[str] = mapped_column(String(30), default="caldav", nullable=False, index=True)
collection_url: Mapped[str] = mapped_column(String(1000), nullable=False) collection_url: Mapped[str] = mapped_column(String(1000), nullable=False)
@@ -142,7 +142,7 @@ class CalendarSyncCredential(Base, TimestampMixin):
) )
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
tenant_id: Mapped[str] = mapped_column(ForeignKey("tenancy_tenants.id", ondelete="CASCADE"), nullable=False, index=True) tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
credential_kind: Mapped[str] = mapped_column(String(30), nullable=False, index=True) credential_kind: Mapped[str] = mapped_column(String(30), nullable=False, index=True)
label: Mapped[str | None] = mapped_column(String(255)) label: Mapped[str | None] = mapped_column(String(255))
secret_encrypted: Mapped[str | None] = mapped_column(Text) secret_encrypted: Mapped[str | None] = mapped_column(Text)

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
from pathlib import Path from pathlib import Path
from govoplan_calendar.backend.db import models as calendar_models # noqa: F401 - populate Calendar ORM metadata from govoplan_calendar.backend.db import models as calendar_models # noqa: F401 - populate Calendar ORM metadata
from govoplan_core.core.access import CAPABILITY_AUTH_PERMISSION_EVALUATOR, CAPABILITY_AUTH_PRINCIPAL_RESOLVER
from govoplan_core.core.module_guards import drop_table_retirement_provider, persistent_table_uninstall_guard from govoplan_core.core.module_guards import drop_table_retirement_provider, persistent_table_uninstall_guard
from govoplan_core.core.modules import FrontendModule, MigrationSpec, ModuleContext, ModuleManifest, NavItem, PermissionDefinition, RoleTemplate from govoplan_core.core.modules import FrontendModule, MigrationSpec, ModuleContext, ModuleManifest, NavItem, PermissionDefinition, RoleTemplate
from govoplan_core.db.base import Base from govoplan_core.db.base import Base
@@ -88,7 +89,7 @@ manifest = ModuleManifest(
id="calendar", id="calendar",
name="Calendar", name="Calendar",
version="0.1.6", version="0.1.6",
dependencies=("access",), required_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR),
optional_dependencies=("mail", "tasks", "scheduling", "appointments", "workflow", "notifications", "dms", "connectors"), optional_dependencies=("mail", "tasks", "scheduling", "appointments", "workflow", "notifications", "dms", "connectors"),
permissions=PERMISSIONS, permissions=PERMISSIONS,
route_factory=_calendar_router, route_factory=_calendar_router,

View File

@@ -16,9 +16,14 @@ branch_labels = None
depends_on = None depends_on = None
def _scope_fk_target(tables: set[str]) -> str:
return "core_scopes.id" if "core_scopes" in tables else "tenancy_tenants.id"
def upgrade() -> None: def upgrade() -> None:
inspector = sa.inspect(op.get_bind()) inspector = sa.inspect(op.get_bind())
tables = set(inspector.get_table_names()) tables = set(inspector.get_table_names())
scope_fk_target = _scope_fk_target(tables)
if "calendar_collections" not in tables: if "calendar_collections" not in tables:
op.create_table( op.create_table(
"calendar_collections", "calendar_collections",
@@ -39,7 +44,7 @@ def upgrade() -> None:
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["created_by_user_id"], ["access_users.id"], name=op.f("fk_calendar_collections_created_by_user_id_users"), ondelete="SET NULL"), sa.ForeignKeyConstraint(["created_by_user_id"], ["access_users.id"], name=op.f("fk_calendar_collections_created_by_user_id_users"), ondelete="SET NULL"),
sa.ForeignKeyConstraint(["tenant_id"], ["tenancy_tenants.id"], name=op.f("fk_calendar_collections_tenant_id_tenants"), ondelete="CASCADE"), sa.ForeignKeyConstraint(["tenant_id"], [scope_fk_target], name=op.f("fk_calendar_collections_tenant_id_scopes"), ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id", name=op.f("pk_calendar_collections")), sa.PrimaryKeyConstraint("id", name=op.f("pk_calendar_collections")),
) )
op.create_index(op.f("ix_calendar_collections_tenant_id"), "calendar_collections", ["tenant_id"], unique=False) op.create_index(op.f("ix_calendar_collections_tenant_id"), "calendar_collections", ["tenant_id"], unique=False)
@@ -101,7 +106,7 @@ def upgrade() -> None:
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["calendar_id"], ["calendar_collections.id"], name=op.f("fk_calendar_events_calendar_id_calendar_collections"), ondelete="CASCADE"), sa.ForeignKeyConstraint(["calendar_id"], ["calendar_collections.id"], name=op.f("fk_calendar_events_calendar_id_calendar_collections"), ondelete="CASCADE"),
sa.ForeignKeyConstraint(["created_by_user_id"], ["access_users.id"], name=op.f("fk_calendar_events_created_by_user_id_users"), ondelete="SET NULL"), sa.ForeignKeyConstraint(["created_by_user_id"], ["access_users.id"], name=op.f("fk_calendar_events_created_by_user_id_users"), ondelete="SET NULL"),
sa.ForeignKeyConstraint(["tenant_id"], ["tenancy_tenants.id"], name=op.f("fk_calendar_events_tenant_id_tenants"), ondelete="CASCADE"), sa.ForeignKeyConstraint(["tenant_id"], [scope_fk_target], name=op.f("fk_calendar_events_tenant_id_scopes"), ondelete="CASCADE"),
sa.ForeignKeyConstraint(["updated_by_user_id"], ["access_users.id"], name=op.f("fk_calendar_events_updated_by_user_id_users"), ondelete="SET NULL"), sa.ForeignKeyConstraint(["updated_by_user_id"], ["access_users.id"], name=op.f("fk_calendar_events_updated_by_user_id_users"), ondelete="SET NULL"),
sa.PrimaryKeyConstraint("id", name=op.f("pk_calendar_events")), sa.PrimaryKeyConstraint("id", name=op.f("pk_calendar_events")),
sa.UniqueConstraint("calendar_id", "uid", "recurrence_id", name="uq_calendar_events_calendar_uid_recurrence"), sa.UniqueConstraint("calendar_id", "uid", "recurrence_id", name="uq_calendar_events_calendar_uid_recurrence"),

View File

@@ -16,11 +16,16 @@ branch_labels = None
depends_on = None depends_on = None
def _scope_fk_target(tables: set[str]) -> str:
return "core_scopes.id" if "core_scopes" in tables else "tenancy_tenants.id"
def upgrade() -> None: def upgrade() -> None:
inspector = sa.inspect(op.get_bind()) inspector = sa.inspect(op.get_bind())
tables = set(inspector.get_table_names()) tables = set(inspector.get_table_names())
if "calendar_sync_sources" in tables: if "calendar_sync_sources" in tables:
return return
scope_fk_target = _scope_fk_target(tables)
op.create_table( op.create_table(
"calendar_sync_sources", "calendar_sync_sources",
sa.Column("id", sa.String(length=36), nullable=False), sa.Column("id", sa.String(length=36), nullable=False),
@@ -42,7 +47,7 @@ def upgrade() -> None:
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["calendar_id"], ["calendar_collections.id"], name=op.f("fk_calendar_sync_sources_calendar_id_calendar_collections"), ondelete="CASCADE"), sa.ForeignKeyConstraint(["calendar_id"], ["calendar_collections.id"], name=op.f("fk_calendar_sync_sources_calendar_id_calendar_collections"), ondelete="CASCADE"),
sa.ForeignKeyConstraint(["tenant_id"], ["tenancy_tenants.id"], name=op.f("fk_calendar_sync_sources_tenant_id_tenants"), ondelete="CASCADE"), sa.ForeignKeyConstraint(["tenant_id"], [scope_fk_target], name=op.f("fk_calendar_sync_sources_tenant_id_scopes"), ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id", name=op.f("pk_calendar_sync_sources")), sa.PrimaryKeyConstraint("id", name=op.f("pk_calendar_sync_sources")),
) )
for column in ("tenant_id", "calendar_id", "source_kind", "deleted_at"): for column in ("tenant_id", "calendar_id", "source_kind", "deleted_at"):

View File

@@ -16,10 +16,15 @@ branch_labels = None
depends_on = None depends_on = None
def _scope_fk_target(tables: set[str]) -> str:
return "core_scopes.id" if "core_scopes" in tables else "tenancy_tenants.id"
def upgrade() -> None: def upgrade() -> None:
bind = op.get_bind() bind = op.get_bind()
inspector = sa.inspect(bind) inspector = sa.inspect(bind)
tables = set(inspector.get_table_names()) tables = set(inspector.get_table_names())
scope_fk_target = _scope_fk_target(tables)
if "calendar_sync_sources" in tables: if "calendar_sync_sources" in tables:
columns = {column["name"] for column in inspector.get_columns("calendar_sync_sources")} columns = {column["name"] for column in inspector.get_columns("calendar_sync_sources")}
add_column_if_missing(columns, "sync_enabled", sa.Column("sync_enabled", sa.Boolean(), server_default=sa.true(), nullable=False)) add_column_if_missing(columns, "sync_enabled", sa.Column("sync_enabled", sa.Boolean(), server_default=sa.true(), nullable=False))
@@ -48,7 +53,7 @@ def upgrade() -> None:
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["created_by_user_id"], ["access_users.id"], name=op.f("fk_calendar_sync_credentials_created_by_user_id_users"), ondelete="SET NULL"), sa.ForeignKeyConstraint(["created_by_user_id"], ["access_users.id"], name=op.f("fk_calendar_sync_credentials_created_by_user_id_users"), ondelete="SET NULL"),
sa.ForeignKeyConstraint(["tenant_id"], ["tenancy_tenants.id"], name=op.f("fk_calendar_sync_credentials_tenant_id_tenants"), ondelete="CASCADE"), sa.ForeignKeyConstraint(["tenant_id"], [scope_fk_target], name=op.f("fk_calendar_sync_credentials_tenant_id_scopes"), ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id", name=op.f("pk_calendar_sync_credentials")), sa.PrimaryKeyConstraint("id", name=op.f("pk_calendar_sync_credentials")),
) )
for column in ("tenant_id", "credential_kind", "created_by_user_id", "deleted_at"): for column in ("tenant_id", "credential_kind", "created_by_user_id", "deleted_at"):

View File

@@ -5,7 +5,7 @@ from datetime import datetime
from fastapi import APIRouter, Body, Depends, HTTPException, Query, Response, status from fastapi import APIRouter, Body, Depends, HTTPException, Query, Response, status
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from govoplan_access.backend.auth.dependencies import ApiPrincipal, get_api_principal, has_scope from govoplan_core.auth import ApiPrincipal, get_api_principal, has_scope
from govoplan_core.api.v1.schemas import DeltaDeletedItem from govoplan_core.api.v1.schemas import DeltaDeletedItem
from govoplan_core.core.change_sequence import decode_sequence_watermark, encode_sequence_watermark, max_sequence_id, sequence_entries_since, sequence_watermark_is_expired from govoplan_core.core.change_sequence import decode_sequence_watermark, encode_sequence_watermark, max_sequence_id, sequence_entries_since, sequence_watermark_is_expired
from govoplan_calendar.backend.db.models import CalendarEvent from govoplan_calendar.backend.db.models import CalendarEvent