feat: implement three-tier module classification and framework layer
Module Classification: - Core (4): core, tenancy, cms, customers - always enabled - Optional (7): payments, billing, inventory, orders, marketplace, analytics, messaging - Internal (2): dev-tools, monitoring - admin-only Key Changes: - Rename platform-admin module to tenancy - Promote CMS and Customers to core modules - Create new payments module (gateway abstractions) - Add billing→payments and orders→payments dependencies - Mark dev-tools and monitoring as internal modules New Infrastructure: - app/modules/events.py: Module event bus (ENABLED, DISABLED, STARTUP, SHUTDOWN) - app/modules/migrations.py: Module-specific migration discovery - app/core/observability.py: Health checks, Prometheus metrics, Sentry integration Enhanced ModuleDefinition: - version, is_internal, permissions - config_schema, default_config - migrations_path - Lifecycle hooks: on_enable, on_disable, on_startup, health_check New Registry Functions: - get_optional_module_codes(), get_internal_module_codes() - is_core_module(), is_internal_module() - get_modules_by_tier(), get_module_tier() Migrations: - zc*: Rename platform-admin to tenancy - zd*: Ensure CMS and Customers enabled for all platforms Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -213,6 +213,41 @@ if config.config_file_name is not None:
|
||||
target_metadata = Base.metadata
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# MODULE MIGRATIONS DISCOVERY
|
||||
# ============================================================================
|
||||
# Discover migration paths from self-contained modules.
|
||||
# Each module can have its own migrations/ directory.
|
||||
# ============================================================================
|
||||
|
||||
def get_version_locations() -> list[str]:
|
||||
"""
|
||||
Get all version locations including module migrations.
|
||||
|
||||
Returns:
|
||||
List of paths to migration version directories
|
||||
"""
|
||||
try:
|
||||
from app.modules.migrations import get_all_migration_paths
|
||||
|
||||
paths = get_all_migration_paths()
|
||||
locations = [str(p) for p in paths if p.exists()]
|
||||
|
||||
if len(locations) > 1:
|
||||
print(f"[ALEMBIC] Found {len(locations)} migration locations:")
|
||||
for loc in locations:
|
||||
print(f" - {loc}")
|
||||
|
||||
return locations
|
||||
except ImportError:
|
||||
# Fallback if module migrations not available
|
||||
return ["alembic/versions"]
|
||||
|
||||
|
||||
# Get version locations for multi-directory support
|
||||
version_locations = get_version_locations()
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
"""
|
||||
Run migrations in 'offline' mode.
|
||||
@@ -229,6 +264,7 @@ def run_migrations_offline() -> None:
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={"paramstyle": "named"},
|
||||
version_locations=version_locations,
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
@@ -249,7 +285,11 @@ def run_migrations_online() -> None:
|
||||
)
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(connection=connection, target_metadata=target_metadata)
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
version_locations=version_locations,
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
# alembic/versions/zc2m3n4o5p6q7_rename_platform_admin_to_tenancy.py
|
||||
"""Rename platform-admin module to tenancy.
|
||||
|
||||
Revision ID: zc2m3n4o5p6q7
|
||||
Revises: zb1l2m3n4o5p6
|
||||
Create Date: 2026-01-27 10:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "zc2m3n4o5p6q7"
|
||||
down_revision = "zb1l2m3n4o5p6"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Rename platform-admin to tenancy in platform_modules table."""
|
||||
# Update module_code in platform_modules junction table
|
||||
op.execute(
|
||||
"""
|
||||
UPDATE platform_modules
|
||||
SET module_code = 'tenancy'
|
||||
WHERE module_code = 'platform-admin'
|
||||
"""
|
||||
)
|
||||
|
||||
# Also update any JSON settings that might reference the old module code
|
||||
# This handles Platform.settings["enabled_modules"] for legacy data
|
||||
op.execute(
|
||||
"""
|
||||
UPDATE platforms
|
||||
SET settings = jsonb_set(
|
||||
settings,
|
||||
'{enabled_modules}',
|
||||
(
|
||||
SELECT COALESCE(
|
||||
jsonb_agg(
|
||||
CASE
|
||||
WHEN elem = 'platform-admin' THEN 'tenancy'
|
||||
ELSE elem
|
||||
END
|
||||
),
|
||||
'[]'::jsonb
|
||||
)
|
||||
FROM jsonb_array_elements_text(
|
||||
COALESCE(settings->'enabled_modules', '[]'::jsonb)
|
||||
) AS elem
|
||||
)
|
||||
)
|
||||
WHERE settings ? 'enabled_modules'
|
||||
AND settings->'enabled_modules' @> '"platform-admin"'
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Revert tenancy back to platform-admin."""
|
||||
# Revert module_code in platform_modules junction table
|
||||
op.execute(
|
||||
"""
|
||||
UPDATE platform_modules
|
||||
SET module_code = 'platform-admin'
|
||||
WHERE module_code = 'tenancy'
|
||||
"""
|
||||
)
|
||||
|
||||
# Revert JSON settings
|
||||
op.execute(
|
||||
"""
|
||||
UPDATE platforms
|
||||
SET settings = jsonb_set(
|
||||
settings,
|
||||
'{enabled_modules}',
|
||||
(
|
||||
SELECT COALESCE(
|
||||
jsonb_agg(
|
||||
CASE
|
||||
WHEN elem = 'tenancy' THEN 'platform-admin'
|
||||
ELSE elem
|
||||
END
|
||||
),
|
||||
'[]'::jsonb
|
||||
)
|
||||
FROM jsonb_array_elements_text(
|
||||
COALESCE(settings->'enabled_modules', '[]'::jsonb)
|
||||
) AS elem
|
||||
)
|
||||
)
|
||||
WHERE settings ? 'enabled_modules'
|
||||
AND settings->'enabled_modules' @> '"tenancy"'
|
||||
"""
|
||||
)
|
||||
@@ -0,0 +1,95 @@
|
||||
# alembic/versions/zd3n4o5p6q7r8_promote_cms_customers_to_core.py
|
||||
"""Promote CMS and Customers modules to core.
|
||||
|
||||
Revision ID: zd3n4o5p6q7r8
|
||||
Revises: zc2m3n4o5p6q7
|
||||
Create Date: 2026-01-27 10:10:00.000000
|
||||
|
||||
This migration ensures that CMS and Customers modules are enabled for all platforms,
|
||||
since they are now core modules that cannot be disabled.
|
||||
"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "zd3n4o5p6q7r8"
|
||||
down_revision = "zc2m3n4o5p6q7"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Enable CMS and Customers modules for all platforms."""
|
||||
connection = op.get_bind()
|
||||
|
||||
# Get all platform IDs
|
||||
platforms = connection.execute(
|
||||
sa.text("SELECT id FROM platforms")
|
||||
).fetchall()
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
core_modules = ["cms", "customers"]
|
||||
|
||||
for (platform_id,) in platforms:
|
||||
for module_code in core_modules:
|
||||
# Check if record exists
|
||||
existing = connection.execute(
|
||||
sa.text(
|
||||
"""
|
||||
SELECT id FROM platform_modules
|
||||
WHERE platform_id = :platform_id AND module_code = :module_code
|
||||
"""
|
||||
),
|
||||
{"platform_id": platform_id, "module_code": module_code},
|
||||
).fetchone()
|
||||
|
||||
if existing:
|
||||
# Update to enabled
|
||||
connection.execute(
|
||||
sa.text(
|
||||
"""
|
||||
UPDATE platform_modules
|
||||
SET is_enabled = true, enabled_at = :now
|
||||
WHERE platform_id = :platform_id AND module_code = :module_code
|
||||
"""
|
||||
),
|
||||
{"platform_id": platform_id, "module_code": module_code, "now": now},
|
||||
)
|
||||
else:
|
||||
# Insert new enabled record
|
||||
connection.execute(
|
||||
sa.text(
|
||||
"""
|
||||
INSERT INTO platform_modules (platform_id, module_code, is_enabled, enabled_at, config)
|
||||
VALUES (:platform_id, :module_code, true, :now, '{}')
|
||||
"""
|
||||
),
|
||||
{"platform_id": platform_id, "module_code": module_code, "now": now},
|
||||
)
|
||||
|
||||
# Also update JSON settings to include CMS and Customers if not present
|
||||
for module_code in core_modules:
|
||||
op.execute(
|
||||
f"""
|
||||
UPDATE platforms
|
||||
SET settings = jsonb_set(
|
||||
COALESCE(settings, '{{}}'::jsonb),
|
||||
'{{enabled_modules}}',
|
||||
COALESCE(settings->'enabled_modules', '[]'::jsonb) || '"{module_code}"'::jsonb
|
||||
)
|
||||
WHERE settings ? 'enabled_modules'
|
||||
AND NOT (settings->'enabled_modules' @> '"{module_code}"')
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""
|
||||
Note: This doesn't actually disable CMS and Customers since that would
|
||||
break functionality. It just removes the explicit enabling done by upgrade.
|
||||
"""
|
||||
# No-op: We don't want to disable core modules
|
||||
pass
|
||||
Reference in New Issue
Block a user