feat(admin, miniapp): 优化数据库迁移逻辑与小程序图片资源处理
- 为后端Admin新增migration_compat工具,支持离线迁移时的基线资源检查,重构全部alembic迁移脚本适配离线模式,避免重复创建已存在的数据库表和字段 - 完善初始数据库迁移脚本,添加默认部门种子数据,新增ConciergeAdvisor详情字段从JSON到JSONB的类型转换迁移 - 小程序端新增图片资源解析工具resolveNetworkImage,实现本地静态资源到CDN的自动映射,添加图片加载容错降级机制 - 完善HeroSlide类型定义,新增actionLabel字段,优化首页轮播组件的文本渲染与错误处理逻辑 - 新增对应测试用例验证迁移路径正确性与前端数据处理逻辑 - 新增临时 brainstorm UI对比草稿与开发临时状态文件
This commit is contained in:
1 parent
96bcc7c032
commit
4e04feda5b
27 files changed
+2695
-377
No files matched your search
@@ -5,7 +5,10 @@ Revises:
|
||||
Create Date: 2026-06-30
|
||||
"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from alembic import context, op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import inspect
|
||||
from app.database import Base
|
||||
from app import models # noqa: F401
|
||||
@@ -16,6 +19,41 @@ down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
DEFAULT_DEPT_ID = "00000000-0000-0000-0000-000000000001"
|
||||
|
||||
|
||||
def _seed_default_department(bind) -> None:
|
||||
department_table = sa.table(
|
||||
"AdminDepartment",
|
||||
sa.column("id", sa.String()),
|
||||
sa.column("name", sa.String()),
|
||||
sa.column("sortOrder", sa.Integer()),
|
||||
sa.column("isActive", sa.Boolean()),
|
||||
sa.column("createdAt", sa.DateTime()),
|
||||
sa.column("updatedAt", sa.DateTime()),
|
||||
)
|
||||
if context.is_offline_mode():
|
||||
should_seed = True
|
||||
else:
|
||||
should_seed = bind.execute(
|
||||
sa.text('SELECT 1 FROM "AdminDepartment" LIMIT 1')
|
||||
).first() is None
|
||||
if should_seed:
|
||||
timestamp = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||
op.bulk_insert(
|
||||
department_table,
|
||||
[
|
||||
{
|
||||
"id": DEFAULT_DEPT_ID,
|
||||
"name": "默认部门",
|
||||
"sortOrder": 0,
|
||||
"isActive": True,
|
||||
"createdAt": timestamp,
|
||||
"updatedAt": timestamp,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
@@ -26,6 +64,8 @@ def upgrade() -> None:
|
||||
for table in Base.metadata.sorted_tables:
|
||||
if table.name not in existing_tables:
|
||||
table.create(bind)
|
||||
if "AdminDepartment" in Base.metadata.tables:
|
||||
_seed_default_department(bind)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
@@ -10,6 +10,8 @@ from alembic import context, op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import inspect
|
||||
|
||||
from app.migration_compat import baseline_table_names
|
||||
|
||||
|
||||
revision = "0015_wanfa"
|
||||
down_revision = "0001_initial_schema"
|
||||
@@ -99,8 +101,11 @@ def _seed_wanfa() -> None:
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
existing_tables = set(inspect(bind).get_table_names())
|
||||
if context.is_offline_mode():
|
||||
existing_tables = baseline_table_names()
|
||||
else:
|
||||
bind = op.get_bind()
|
||||
existing_tables = set(inspect(bind).get_table_names())
|
||||
|
||||
if "WanfaCategory" not in existing_tables:
|
||||
op.create_table(
|
||||
|
||||
@@ -5,11 +5,14 @@ Revises: 0015_wanfa
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
import json
|
||||
|
||||
from alembic import context, op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import inspect
|
||||
|
||||
from app.migration_compat import baseline_table_names
|
||||
|
||||
|
||||
revision = "0016_concierge_advisors"
|
||||
down_revision = "0015_wanfa"
|
||||
@@ -60,14 +63,23 @@ SEED_ADVISORS = (
|
||||
)
|
||||
|
||||
|
||||
def is_offline_mode() -> bool:
|
||||
try:
|
||||
return context.is_offline_mode()
|
||||
except (NameError, AttributeError):
|
||||
return False
|
||||
|
||||
|
||||
def _seed_advisors() -> None:
|
||||
offline = is_offline_mode()
|
||||
details_type = sa.Text() if offline else sa.JSON()
|
||||
table = sa.table(
|
||||
"ConciergeAdvisor",
|
||||
sa.column("id", sa.String()),
|
||||
sa.column("avatar", sa.String()),
|
||||
sa.column("name", sa.String()),
|
||||
sa.column("role", sa.String()),
|
||||
sa.column("details", sa.JSON()),
|
||||
sa.column("details", details_type),
|
||||
sa.column("qrImage", sa.String()),
|
||||
sa.column("isActive", sa.Boolean()),
|
||||
sa.column("sortOrder", sa.Integer()),
|
||||
@@ -75,15 +87,18 @@ def _seed_advisors() -> None:
|
||||
sa.column("updatedAt", sa.DateTime()),
|
||||
)
|
||||
timestamp = datetime(2026, 1, 1)
|
||||
op.bulk_insert(
|
||||
table,
|
||||
[{**advisor, "createdAt": timestamp, "updatedAt": timestamp} for advisor in SEED_ADVISORS],
|
||||
)
|
||||
rows = [{**advisor, "createdAt": timestamp, "updatedAt": timestamp} for advisor in SEED_ADVISORS]
|
||||
if offline:
|
||||
rows = [{**advisor, "details": json.dumps(advisor["details"], ensure_ascii=False)} for advisor in rows]
|
||||
op.bulk_insert(table, rows)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
existing_tables = set(inspect(bind).get_table_names())
|
||||
if is_offline_mode():
|
||||
existing_tables = baseline_table_names()
|
||||
else:
|
||||
bind = op.get_bind()
|
||||
existing_tables = set(inspect(bind).get_table_names())
|
||||
if "ConciergeAdvisor" not in existing_tables:
|
||||
op.create_table(
|
||||
"ConciergeAdvisor",
|
||||
@@ -102,7 +117,7 @@ def upgrade() -> None:
|
||||
op.create_index("ix_ConciergeAdvisor_isActive", "ConciergeAdvisor", ["isActive"])
|
||||
op.create_index("ix_ConciergeAdvisor_sortOrder", "ConciergeAdvisor", ["sortOrder"])
|
||||
|
||||
if context.is_offline_mode():
|
||||
if is_offline_mode():
|
||||
_seed_advisors()
|
||||
return
|
||||
|
||||
|
||||
@@ -5,12 +5,15 @@ Revises: 0016_concierge_advisors
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
import json
|
||||
|
||||
from alembic import context, op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import inspect
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
from app.migration_compat import baseline_column_names, baseline_table_names
|
||||
|
||||
|
||||
revision = "0017_home_content"
|
||||
down_revision = "0016_concierge_advisors"
|
||||
@@ -142,6 +145,13 @@ SEED_WILD_ARCHIVES = (
|
||||
|
||||
|
||||
def _create_table(table_name: str, columns: list[sa.Column]) -> None:
|
||||
if is_offline_mode():
|
||||
if table_name in baseline_table_names():
|
||||
return
|
||||
op.create_table(table_name, *columns, sa.PrimaryKeyConstraint("id"))
|
||||
op.create_index(f"ix_{table_name}_isActive", table_name, ["isActive"])
|
||||
op.create_index(f"ix_{table_name}_sortOrder", table_name, ["sortOrder"])
|
||||
return
|
||||
bind = op.get_bind()
|
||||
if table_name in set(inspect(bind).get_table_names()):
|
||||
return
|
||||
@@ -178,6 +188,11 @@ def _seed_table(table_name: str, columns: list[sa.Column], rows: tuple[dict, ...
|
||||
column_types = {column.name: column.type for column in columns}
|
||||
if is_offline_mode():
|
||||
available_columns = set(column_types)
|
||||
if table_name in baseline_table_names():
|
||||
available_columns.update(baseline_column_names(table_name))
|
||||
for name, column_type in OPTIONAL_SEED_COLUMN_TYPES.items():
|
||||
if name in available_columns:
|
||||
column_types.setdefault(name, column_type)
|
||||
else:
|
||||
bind = op.get_bind()
|
||||
current_columns = inspect(bind).get_columns(table_name)
|
||||
@@ -200,7 +215,31 @@ def _seed_table(table_name: str, columns: list[sa.Column], rows: tuple[dict, ...
|
||||
],
|
||||
)
|
||||
timestamp = datetime(2026, 1, 1)
|
||||
op.bulk_insert(table, prepare_seed_rows(rows, seed_column_names, timestamp))
|
||||
seed_rows = prepare_seed_rows(rows, seed_column_names, timestamp)
|
||||
if is_offline_mode():
|
||||
seed_rows = [
|
||||
{
|
||||
**row,
|
||||
**{
|
||||
name: json.dumps(row[name], ensure_ascii=False)
|
||||
for name in ("detailParagraphs", "images")
|
||||
if name in row
|
||||
},
|
||||
}
|
||||
for row in seed_rows
|
||||
]
|
||||
for name in ("detailParagraphs", "images"):
|
||||
if name in column_types:
|
||||
column_types[name] = sa.Text()
|
||||
table = sa.table(
|
||||
table_name,
|
||||
*[
|
||||
sa.column(name, column_types[name])
|
||||
for name in column_types
|
||||
if name in seed_column_names
|
||||
],
|
||||
)
|
||||
op.bulk_insert(table, seed_rows)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
@@ -4,10 +4,12 @@ Revision ID: 0018_home_wanfa_recommendations
|
||||
Revises: 0017_home_content
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
from alembic import context, op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import inspect
|
||||
|
||||
from app.migration_compat import baseline_table_names
|
||||
|
||||
|
||||
revision = "0018_home_wanfa_recommendations"
|
||||
down_revision = "0017_home_content"
|
||||
@@ -15,7 +17,16 @@ branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def is_offline_mode() -> bool:
|
||||
try:
|
||||
return context.is_offline_mode()
|
||||
except (NameError, AttributeError):
|
||||
return False
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
if "HomeWanfaRecommendation" in baseline_table_names() and is_offline_mode():
|
||||
return
|
||||
bind = op.get_bind()
|
||||
if "HomeWanfaRecommendation" in set(inspect(bind).get_table_names()):
|
||||
return
|
||||
|
||||
@@ -9,6 +9,8 @@ import sqlalchemy as sa
|
||||
from sqlalchemy import inspect
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
from app.migration_compat import baseline_column_names
|
||||
|
||||
|
||||
revision = "0019_home_wild_archive_images"
|
||||
down_revision = "0018_home_wanfa_recommendations"
|
||||
@@ -29,7 +31,7 @@ def is_offline_mode() -> bool:
|
||||
|
||||
def upgrade() -> None:
|
||||
if is_offline_mode():
|
||||
add_images_column = True
|
||||
add_images_column = "images" not in baseline_column_names("HomeWildArchive")
|
||||
else:
|
||||
bind = op.get_bind()
|
||||
existing_columns = {column["name"] for column in inspect(bind).get_columns("HomeWildArchive")}
|
||||
|
||||
@@ -9,6 +9,8 @@ import sqlalchemy as sa
|
||||
from sqlalchemy import inspect
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
from app.migration_compat import baseline_column_names
|
||||
|
||||
|
||||
revision = "0020_home_team_building_details"
|
||||
down_revision = "0019_home_wild_archive_images"
|
||||
@@ -32,7 +34,9 @@ def is_offline_mode() -> bool:
|
||||
|
||||
def upgrade() -> None:
|
||||
if is_offline_mode():
|
||||
columns_to_add = DETAIL_COLUMNS
|
||||
columns_to_add = missing_detail_columns(
|
||||
baseline_column_names("HomeTeamBuilding")
|
||||
)
|
||||
else:
|
||||
bind = op.get_bind()
|
||||
existing_columns = {column["name"] for column in inspect(bind).get_columns("HomeTeamBuilding")}
|
||||
|
||||
@@ -11,6 +11,8 @@ import sqlalchemy as sa
|
||||
from sqlalchemy import inspect
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
from app.migration_compat import baseline_table_names
|
||||
|
||||
|
||||
revision = "0021_detail_records"
|
||||
down_revision = "0020_home_team_building_details"
|
||||
@@ -86,7 +88,7 @@ def _create_detail_table() -> None:
|
||||
|
||||
def upgrade() -> None:
|
||||
if is_offline_mode():
|
||||
table_exists = False
|
||||
table_exists = "DetailRecord" in baseline_table_names()
|
||||
existing_index_names = set()
|
||||
else:
|
||||
bind = op.get_bind()
|
||||
|
||||
@@ -8,6 +8,8 @@ from alembic import context, op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import inspect
|
||||
|
||||
from app.migration_compat import baseline_column_names
|
||||
|
||||
|
||||
revision = "0023_detail_concierge_advisor"
|
||||
down_revision = "0022_opaque_ids"
|
||||
@@ -28,7 +30,7 @@ def is_offline_mode() -> bool:
|
||||
|
||||
def upgrade() -> None:
|
||||
if is_offline_mode():
|
||||
should_add = True
|
||||
should_add = "conciergeAdvisorId" not in baseline_column_names("DetailRecord")
|
||||
else:
|
||||
existing_columns = {
|
||||
column["name"]
|
||||
|
||||
@@ -5,6 +5,13 @@ import sqlalchemy as sa
|
||||
from sqlalchemy import inspect
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
from app.migration_compat import (
|
||||
baseline_column_names,
|
||||
baseline_foreign_keys,
|
||||
baseline_index_names,
|
||||
baseline_table_names,
|
||||
)
|
||||
|
||||
|
||||
revision = "0024_vehicle_demand"
|
||||
down_revision = "0023_detail_concierge_advisor"
|
||||
@@ -55,10 +62,10 @@ def is_offline_mode() -> bool:
|
||||
|
||||
def upgrade() -> None:
|
||||
if is_offline_mode():
|
||||
existing_lead_columns = set()
|
||||
existing_lead_indexes = set()
|
||||
existing_lead_foreign_keys = []
|
||||
vehicle_service_table_exists = False
|
||||
existing_lead_columns = baseline_column_names("Lead")
|
||||
existing_lead_indexes = baseline_index_names("Lead")
|
||||
existing_lead_foreign_keys = baseline_foreign_keys("Lead")
|
||||
vehicle_service_table_exists = "VehicleServiceConfig" in baseline_table_names()
|
||||
existing_vehicle_service_indexes = set()
|
||||
else:
|
||||
inspector = inspect(op.get_bind())
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from alembic import op
|
||||
from alembic import context, op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import inspect
|
||||
|
||||
|
||||
revision = "0025_admin_rbac"
|
||||
@@ -37,10 +38,40 @@ MENU_ROWS = (
|
||||
)
|
||||
|
||||
|
||||
def is_offline_mode() -> bool:
|
||||
try:
|
||||
return context.is_offline_mode()
|
||||
except (NameError, AttributeError):
|
||||
return False
|
||||
|
||||
|
||||
def _existing_table_names() -> set[str]:
|
||||
if is_offline_mode():
|
||||
# The initial baseline creates the current RBAC tables before this
|
||||
# revision runs, but an offline connection cannot inspect them.
|
||||
return set(RBAC_TABLES)
|
||||
return set(inspect(op.get_bind()).get_table_names())
|
||||
|
||||
|
||||
def _create_table_if_missing(existing_tables: set[str], table_name: str, *elements) -> None:
|
||||
if table_name in existing_tables:
|
||||
return
|
||||
op.create_table(table_name, *elements)
|
||||
existing_tables.add(table_name)
|
||||
|
||||
|
||||
def _table_has_rows(table_name: str) -> bool:
|
||||
if is_offline_mode():
|
||||
return False
|
||||
return op.get_bind().execute(sa.text(f'SELECT 1 FROM "{table_name}" LIMIT 1')).first() is not None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
now = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||
existing_tables = _existing_table_names()
|
||||
|
||||
op.create_table(
|
||||
_create_table_if_missing(
|
||||
existing_tables,
|
||||
"AdminRole",
|
||||
sa.Column("id", sa.String(), nullable=False),
|
||||
sa.Column("name", sa.String(), nullable=False),
|
||||
@@ -53,7 +84,8 @@ def upgrade() -> None:
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("code", name="uq_AdminRole_code"),
|
||||
)
|
||||
op.create_table(
|
||||
_create_table_if_missing(
|
||||
existing_tables,
|
||||
"AdminDepartment",
|
||||
sa.Column("id", sa.String(), nullable=False),
|
||||
sa.Column("parentId", sa.String(), nullable=True),
|
||||
@@ -65,7 +97,8 @@ def upgrade() -> None:
|
||||
sa.ForeignKeyConstraint(["parentId"], ["AdminDepartment.id"], ondelete="SET NULL"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_table(
|
||||
_create_table_if_missing(
|
||||
existing_tables,
|
||||
"AdminMenu",
|
||||
sa.Column("id", sa.String(), nullable=False),
|
||||
sa.Column("parentId", sa.String(), nullable=True),
|
||||
@@ -84,7 +117,8 @@ def upgrade() -> None:
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("permissionCode", name="uq_AdminMenu_permissionCode"),
|
||||
)
|
||||
op.create_table(
|
||||
_create_table_if_missing(
|
||||
existing_tables,
|
||||
"AdminUserRole",
|
||||
sa.Column("userId", sa.String(), nullable=False),
|
||||
sa.Column("roleId", sa.String(), nullable=False),
|
||||
@@ -93,7 +127,8 @@ def upgrade() -> None:
|
||||
sa.ForeignKeyConstraint(["userId"], ["AdminUser.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("userId", "roleId"),
|
||||
)
|
||||
op.create_table(
|
||||
_create_table_if_missing(
|
||||
existing_tables,
|
||||
"AdminRoleMenu",
|
||||
sa.Column("roleId", sa.String(), nullable=False),
|
||||
sa.Column("menuId", sa.String(), nullable=False),
|
||||
@@ -102,7 +137,8 @@ def upgrade() -> None:
|
||||
sa.ForeignKeyConstraint(["roleId"], ["AdminRole.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("roleId", "menuId"),
|
||||
)
|
||||
op.create_table(
|
||||
_create_table_if_missing(
|
||||
existing_tables,
|
||||
"AdminUserDepartment",
|
||||
sa.Column("userId", sa.String(), nullable=False),
|
||||
sa.Column("deptId", sa.String(), nullable=False),
|
||||
@@ -111,7 +147,8 @@ def upgrade() -> None:
|
||||
sa.ForeignKeyConstraint(["userId"], ["AdminUser.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("userId", "deptId"),
|
||||
)
|
||||
op.create_table(
|
||||
_create_table_if_missing(
|
||||
existing_tables,
|
||||
"AdminRoleDepartment",
|
||||
sa.Column("roleId", sa.String(), nullable=False),
|
||||
sa.Column("deptId", sa.String(), nullable=False),
|
||||
@@ -124,15 +161,18 @@ def upgrade() -> None:
|
||||
role_table = sa.table("AdminRole", sa.column("id", sa.String), sa.column("name", sa.String), sa.column("code", sa.String), sa.column("dataScope", sa.String), sa.column("isActive", sa.Boolean), sa.column("createdAt", sa.DateTime), sa.column("updatedAt", sa.DateTime))
|
||||
dept_table = sa.table("AdminDepartment", sa.column("id", sa.String), sa.column("name", sa.String), sa.column("sortOrder", sa.Integer), sa.column("isActive", sa.Boolean), sa.column("createdAt", sa.DateTime), sa.column("updatedAt", sa.DateTime))
|
||||
menu_table = sa.table("AdminMenu", sa.column("id", sa.String), sa.column("name", sa.String), sa.column("type", sa.String), sa.column("path", sa.String), sa.column("componentKey", sa.String), sa.column("permissionCode", sa.String), sa.column("sortOrder", sa.Integer), sa.column("isVisible", sa.Boolean), sa.column("isActive", sa.Boolean), sa.column("createdAt", sa.DateTime), sa.column("updatedAt", sa.DateTime))
|
||||
op.bulk_insert(role_table, [{"id": SUPER_ROLE_ID, "name": "超级管理员", "code": "super_admin", "dataScope": "all", "isActive": True, "createdAt": now, "updatedAt": now}])
|
||||
op.bulk_insert(dept_table, [{"id": DEFAULT_DEPT_ID, "name": "默认部门", "sortOrder": 0, "isActive": True, "createdAt": now, "updatedAt": now}])
|
||||
op.bulk_insert(
|
||||
menu_table,
|
||||
[
|
||||
{"id": menu_id, "name": name, "type": menu_type, "path": path, "componentKey": component, "permissionCode": permission, "sortOrder": sort_order, "isVisible": True, "isActive": True, "createdAt": now, "updatedAt": now}
|
||||
for menu_id, name, menu_type, path, component, permission, sort_order in MENU_ROWS
|
||||
],
|
||||
)
|
||||
if not _table_has_rows("AdminRole"):
|
||||
op.bulk_insert(role_table, [{"id": SUPER_ROLE_ID, "name": "超级管理员", "code": "super_admin", "dataScope": "all", "isActive": True, "createdAt": now, "updatedAt": now}])
|
||||
if not _table_has_rows("AdminDepartment"):
|
||||
op.bulk_insert(dept_table, [{"id": DEFAULT_DEPT_ID, "name": "默认部门", "sortOrder": 0, "isActive": True, "createdAt": now, "updatedAt": now}])
|
||||
if not _table_has_rows("AdminMenu"):
|
||||
op.bulk_insert(
|
||||
menu_table,
|
||||
[
|
||||
{"id": menu_id, "name": name, "type": menu_type, "path": path, "componentKey": component, "permissionCode": permission, "sortOrder": sort_order, "isVisible": True, "isActive": True, "createdAt": now, "updatedAt": now}
|
||||
for menu_id, name, menu_type, path, component, permission, sort_order in MENU_ROWS
|
||||
],
|
||||
)
|
||||
op.execute(sa.text('INSERT INTO "AdminRoleMenu" ("roleId", "menuId", "createdAt") SELECT :role_id, "id", :created_at FROM "AdminMenu" ON CONFLICT DO NOTHING').bindparams(role_id=SUPER_ROLE_ID, created_at=now))
|
||||
op.execute(sa.text('INSERT INTO "AdminRoleDepartment" ("roleId", "deptId", "createdAt") VALUES (:role_id, :dept_id, :created_at) ON CONFLICT DO NOTHING').bindparams(role_id=SUPER_ROLE_ID, dept_id=DEFAULT_DEPT_ID, created_at=now))
|
||||
op.execute(sa.text('INSERT INTO "AdminUserRole" ("userId", "roleId", "createdAt") SELECT "id", :role_id, :created_at FROM "AdminUser" ON CONFLICT DO NOTHING').bindparams(role_id=SUPER_ROLE_ID, created_at=now))
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"""Add department and creator ownership to operator-managed resources."""
|
||||
|
||||
from alembic import op
|
||||
from alembic import context, op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import inspect
|
||||
|
||||
|
||||
revision = "0026_admin_ownership"
|
||||
@@ -31,56 +32,140 @@ OWNED_TABLES = (
|
||||
)
|
||||
|
||||
|
||||
def is_offline_mode() -> bool:
|
||||
try:
|
||||
return context.is_offline_mode()
|
||||
except (NameError, AttributeError):
|
||||
return False
|
||||
|
||||
|
||||
def _schema_snapshot() -> tuple[set[str], dict[str, dict[str, dict]], dict[str, set[str]], dict[str, list[dict]]]:
|
||||
if is_offline_mode():
|
||||
return set(), {}, {}, {}
|
||||
|
||||
inspector = inspect(op.get_bind())
|
||||
table_names = set(inspector.get_table_names())
|
||||
columns = {
|
||||
table_name: {column["name"]: column for column in inspector.get_columns(table_name)}
|
||||
for table_name in table_names
|
||||
}
|
||||
indexes = {
|
||||
table_name: {
|
||||
index["name"]
|
||||
for index in inspector.get_indexes(table_name)
|
||||
if index.get("name")
|
||||
}
|
||||
for table_name in table_names
|
||||
}
|
||||
foreign_keys = {
|
||||
table_name: inspector.get_foreign_keys(table_name)
|
||||
for table_name in table_names
|
||||
}
|
||||
return table_names, columns, indexes, foreign_keys
|
||||
|
||||
|
||||
def _has_foreign_key(
|
||||
foreign_keys: list[dict],
|
||||
constrained_column: str,
|
||||
referred_table: str,
|
||||
) -> bool:
|
||||
return any(
|
||||
foreign_key.get("constrained_columns") == [constrained_column]
|
||||
and foreign_key.get("referred_table") == referred_table
|
||||
and foreign_key.get("referred_columns") == ["id"]
|
||||
for foreign_key in foreign_keys
|
||||
)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
table_names, columns, indexes, foreign_keys = _schema_snapshot()
|
||||
|
||||
for table_name in OWNED_TABLES:
|
||||
op.add_column(
|
||||
table_name,
|
||||
sa.Column(
|
||||
"deptId",
|
||||
sa.String(),
|
||||
nullable=True,
|
||||
server_default=sa.text(f"'{DEFAULT_DEPT_ID}'"),
|
||||
),
|
||||
)
|
||||
op.add_column(table_name, sa.Column("createdById", sa.String(), nullable=True))
|
||||
op.create_index(f"ix_{table_name}_deptId", table_name, ["deptId"])
|
||||
op.create_index(f"ix_{table_name}_createdById", table_name, ["createdById"])
|
||||
op.create_foreign_key(
|
||||
f"fk_{table_name}_deptId",
|
||||
table_name,
|
||||
"AdminDepartment",
|
||||
["deptId"],
|
||||
["id"],
|
||||
ondelete="RESTRICT",
|
||||
)
|
||||
op.create_foreign_key(
|
||||
f"fk_{table_name}_createdById",
|
||||
table_name,
|
||||
"AdminUser",
|
||||
["createdById"],
|
||||
["id"],
|
||||
ondelete="SET NULL",
|
||||
)
|
||||
if table_name not in table_names:
|
||||
continue
|
||||
|
||||
table_columns = columns[table_name]
|
||||
added_dept_column = "deptId" not in table_columns
|
||||
added_creator_column = "createdById" not in table_columns
|
||||
if added_dept_column:
|
||||
op.add_column(
|
||||
table_name,
|
||||
sa.Column(
|
||||
"deptId",
|
||||
sa.String(),
|
||||
nullable=True,
|
||||
server_default=sa.text(f"'{DEFAULT_DEPT_ID}'"),
|
||||
),
|
||||
)
|
||||
table_columns["deptId"] = {"name": "deptId", "nullable": True}
|
||||
if added_creator_column:
|
||||
op.add_column(table_name, sa.Column("createdById", sa.String(), nullable=True))
|
||||
table_columns["createdById"] = {"name": "createdById", "nullable": True}
|
||||
|
||||
table_indexes = indexes[table_name]
|
||||
if f"ix_{table_name}_deptId" not in table_indexes:
|
||||
op.create_index(f"ix_{table_name}_deptId", table_name, ["deptId"])
|
||||
table_indexes.add(f"ix_{table_name}_deptId")
|
||||
if f"ix_{table_name}_createdById" not in table_indexes:
|
||||
op.create_index(f"ix_{table_name}_createdById", table_name, ["createdById"])
|
||||
table_indexes.add(f"ix_{table_name}_createdById")
|
||||
|
||||
table_foreign_keys = foreign_keys[table_name]
|
||||
if not _has_foreign_key(table_foreign_keys, "deptId", "AdminDepartment"):
|
||||
op.create_foreign_key(
|
||||
f"fk_{table_name}_deptId",
|
||||
table_name,
|
||||
"AdminDepartment",
|
||||
["deptId"],
|
||||
["id"],
|
||||
ondelete="RESTRICT",
|
||||
)
|
||||
table_foreign_keys.append(
|
||||
{
|
||||
"constrained_columns": ["deptId"],
|
||||
"referred_table": "AdminDepartment",
|
||||
"referred_columns": ["id"],
|
||||
}
|
||||
)
|
||||
if not _has_foreign_key(table_foreign_keys, "createdById", "AdminUser"):
|
||||
op.create_foreign_key(
|
||||
f"fk_{table_name}_createdById",
|
||||
table_name,
|
||||
"AdminUser",
|
||||
["createdById"],
|
||||
["id"],
|
||||
ondelete="SET NULL",
|
||||
)
|
||||
table_foreign_keys.append(
|
||||
{
|
||||
"constrained_columns": ["createdById"],
|
||||
"referred_table": "AdminUser",
|
||||
"referred_columns": ["id"],
|
||||
}
|
||||
)
|
||||
quoted_table = f'"{table_name}"'
|
||||
op.execute(
|
||||
sa.text(f'UPDATE {quoted_table} SET "deptId" = :dept_id WHERE "deptId" IS NULL').bindparams(
|
||||
dept_id=DEFAULT_DEPT_ID
|
||||
needs_dept_backfill = added_dept_column or table_columns["deptId"].get("nullable") is True
|
||||
if needs_dept_backfill:
|
||||
op.execute(
|
||||
sa.text(f'UPDATE {quoted_table} SET "deptId" = :dept_id WHERE "deptId" IS NULL').bindparams(
|
||||
dept_id=DEFAULT_DEPT_ID
|
||||
)
|
||||
)
|
||||
)
|
||||
op.alter_column(
|
||||
table_name,
|
||||
"deptId",
|
||||
existing_type=sa.String(),
|
||||
nullable=False,
|
||||
server_default=sa.text(f"'{DEFAULT_DEPT_ID}'"),
|
||||
)
|
||||
op.execute(
|
||||
sa.text(
|
||||
f'UPDATE {quoted_table} SET "createdById" = '
|
||||
'(SELECT "id" FROM "AdminUser" ORDER BY "createdAt" ASC LIMIT 1) '
|
||||
'WHERE "createdById" IS NULL'
|
||||
op.alter_column(
|
||||
table_name,
|
||||
"deptId",
|
||||
existing_type=sa.String(),
|
||||
nullable=False,
|
||||
server_default=sa.text(f"'{DEFAULT_DEPT_ID}'"),
|
||||
)
|
||||
if added_creator_column:
|
||||
op.execute(
|
||||
sa.text(
|
||||
f'UPDATE {quoted_table} SET "createdById" = '
|
||||
'(SELECT "id" FROM "AdminUser" ORDER BY "createdAt" ASC LIMIT 1) '
|
||||
'WHERE "createdById" IS NULL'
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"""Create the media asset table required by admin uploads."""
|
||||
|
||||
from alembic import op
|
||||
from alembic import context, op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import inspect
|
||||
|
||||
from app.migration_compat import baseline_table_names
|
||||
|
||||
|
||||
revision = "0034_media_assets"
|
||||
down_revision = "0033_remove_site_versions"
|
||||
@@ -13,7 +15,16 @@ depends_on = None
|
||||
DEFAULT_DEPT_ID = "00000000-0000-0000-0000-000000000001"
|
||||
|
||||
|
||||
def is_offline_mode() -> bool:
|
||||
try:
|
||||
return context.is_offline_mode()
|
||||
except (NameError, AttributeError):
|
||||
return False
|
||||
|
||||
|
||||
def _table_names() -> set[str]:
|
||||
if is_offline_mode():
|
||||
return baseline_table_names()
|
||||
return set(inspect(op.get_bind()).get_table_names())
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Align the concierge details column with the PostgreSQL JSONB model type."""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
|
||||
revision = "0035_concierge_details_jsonb"
|
||||
down_revision = "0034_media_assets"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.alter_column(
|
||||
"ConciergeAdvisor",
|
||||
"details",
|
||||
existing_type=sa.JSON(),
|
||||
type_=postgresql.JSONB(astext_type=sa.Text()),
|
||||
existing_nullable=False,
|
||||
postgresql_using='"details"::jsonb',
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.alter_column(
|
||||
"ConciergeAdvisor",
|
||||
"details",
|
||||
existing_type=postgresql.JSONB(astext_type=sa.Text()),
|
||||
type_=sa.JSON(),
|
||||
existing_nullable=False,
|
||||
postgresql_using='"details"::json',
|
||||
)
|
||||
Reference in new issue
Block a user