fix(module-key): make module keys standalone MD format with system_key relation

This commit is contained in:
Chris
2026-03-30 20:02:17 +08:00
parent c4266b7da5
commit 0cd863f9c2
7 changed files with 77 additions and 48 deletions

View File

@@ -29,10 +29,17 @@ def _resolve_module_id(db: Session, system_key: str, module_key: str | None) ->
if not system: if not system:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="system_not_found") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="system_not_found")
target_module_key = f"{system_key}.{module_key}" if module_key else f"{system_key}.__system__" target_module_key = module_key if module_key else f"__system__{system_key}"
module = modules_repo.get_by_key(target_module_key) module = modules_repo.get_by_key(target_module_key)
if module and module.system_key != system_key:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="module_system_mismatch")
if not module: if not module:
module = modules_repo.create(module_key=target_module_key, name=target_module_key, status="active") module = modules_repo.create(
module_key=target_module_key,
system_key=system_key,
name=target_module_key,
status="active",
)
return module.id return module.id

View File

@@ -51,10 +51,17 @@ def _resolve_module_id(db: Session, system_key: str, module_key: str | None) ->
system = systems_repo.get_by_key(system_key) system = systems_repo.get_by_key(system_key)
if not system: if not system:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="system_not_found") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="system_not_found")
target_module_key = f"{system_key}.{module_key}" if module_key else f"{system_key}.__system__" target_module_key = module_key if module_key else f"__system__{system_key}"
module = modules_repo.get_by_key(target_module_key) module = modules_repo.get_by_key(target_module_key)
if module and module.system_key != system_key:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="module_system_mismatch")
if not module: if not module:
module = modules_repo.create(module_key=target_module_key, name=target_module_key, status="active") module = modules_repo.create(
module_key=target_module_key,
system_key=system_key,
name=target_module_key,
status="active",
)
return module.id return module.id
@@ -77,8 +84,6 @@ def _resolve_scope_ids(db: Session, scope_type: str, scope_id: str) -> tuple[str
def _split_module_key(payload_module: str | None) -> str: def _split_module_key(payload_module: str | None) -> str:
if not payload_module: if not payload_module:
return "__system__" return "__system__"
if "." in payload_module:
return payload_module.split(".", 1)[1]
return payload_module return payload_module
@@ -164,11 +169,12 @@ def list_modules(
items, total = modules_repo.list(limit=limit, offset=offset) items, total = modules_repo.list(limit=limit, offset=offset)
out = [] out = []
for i in items: for i in items:
system_key = i.module_key.split(".", 1)[0] if "." in i.module_key else None if i.module_key.startswith("__system__"):
continue
out.append( out.append(
ModuleItem( ModuleItem(
id=i.id, id=i.id,
system_key=system_key, system_key=i.system_key,
module_key=i.module_key, module_key=i.module_key,
name=i.name, name=i.name,
status=i.status, status=i.status,
@@ -188,14 +194,14 @@ def create_module(
system = systems_repo.get_by_key(payload.system_key) system = systems_repo.get_by_key(payload.system_key)
if not system: if not system:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="system_not_found") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="system_not_found")
leaf_module_key = _generate_unique_key("MD", lambda k: modules_repo.get_by_key(f"{payload.system_key}.{k}")) leaf_module_key = _generate_unique_key("MD", modules_repo.get_by_key)
full_module_key = f"{payload.system_key}.{leaf_module_key}"
row = modules_repo.create( row = modules_repo.create(
module_key=full_module_key, module_key=leaf_module_key,
system_key=payload.system_key,
name=payload.name, name=payload.name,
status=payload.status, status=payload.status,
) )
return ModuleItem(id=row.id, system_key=payload.system_key, module_key=row.module_key, name=row.name, status=row.status) return ModuleItem(id=row.id, system_key=row.system_key, module_key=row.module_key, name=row.name, status=row.status)
@router.patch("/modules/{module_key}") @router.patch("/modules/{module_key}")
@@ -210,8 +216,7 @@ def update_module(
if not row: if not row:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="module_not_found") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="module_not_found")
row = modules_repo.update(row, name=payload.name, status=payload.status) row = modules_repo.update(row, name=payload.name, status=payload.status)
system_key = row.module_key.split(".", 1)[0] if "." in row.module_key else None return ModuleItem(id=row.id, system_key=row.system_key, module_key=row.module_key, name=row.name, status=row.status)
return ModuleItem(id=row.id, system_key=system_key, module_key=row.module_key, name=row.name, status=row.status)
@router.get("/systems/{system_key}/groups") @router.get("/systems/{system_key}/groups")
@@ -268,8 +273,7 @@ def list_module_groups(
module = modules_repo.get_by_key(module_key) module = modules_repo.get_by_key(module_key)
if not module: if not module:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="module_not_found") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="module_not_found")
system_key, module_name = module.module_key.split(".", 1) if "." in module.module_key else ("", module.module_key) groups = groups_repo.list_module_groups(module.system_key, module.module_key)
groups = groups_repo.list_module_groups(system_key, module_name)
return { return {
"items": [ "items": [
GroupRelationItem(group_key=g.group_key, group_name=g.name, status=g.status).model_dump() GroupRelationItem(group_key=g.group_key, group_name=g.name, status=g.status).model_dump()
@@ -289,8 +293,7 @@ def list_module_members(
module = modules_repo.get_by_key(module_key) module = modules_repo.get_by_key(module_key)
if not module: if not module:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="module_not_found") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="module_not_found")
system_key, module_name = module.module_key.split(".", 1) if "." in module.module_key else ("", module.module_key) members = groups_repo.list_module_members(module.system_key, module.module_key)
members = groups_repo.list_module_members(system_key, module_name)
return { return {
"items": [ "items": [
MemberRelationItem( MemberRelationItem(
@@ -614,7 +617,7 @@ def list_permission_group_permissions(
PermissionGroupPermissionItem( PermissionGroupPermissionItem(
id=r.id, id=r.id,
system=r.system, system=r.system,
module=("" if r.module == "__system__" else (r.module if "." in r.module else f"{r.system}.{r.module}")), module="" if r.module == "__system__" else r.module,
action=r.action, action=r.action,
scope_type=r.scope_type, scope_type=r.scope_type,
scope_id=r.scope_id, scope_id=r.scope_id,
@@ -636,7 +639,14 @@ def get_permission_group_bindings(
if not group: if not group:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="group_not_found") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="group_not_found")
snapshot = repo.get_group_binding_snapshot(group.id, group_key) snapshot = repo.get_group_binding_snapshot(group.id, group_key)
return GroupBindingSnapshot(**snapshot) return GroupBindingSnapshot(
group_key=snapshot["group_key"],
site_keys=snapshot["site_keys"],
system_keys=snapshot["system_keys"],
module_keys=[k.split("|", 1)[1] if "|" in k else k for k in snapshot["module_keys"]],
member_subs=snapshot["member_subs"],
actions=snapshot["actions"],
)
@router.put("/permission-groups/{group_key}/bindings", response_model=GroupBindingSnapshot) @router.put("/permission-groups/{group_key}/bindings", response_model=GroupBindingSnapshot)
@@ -669,21 +679,32 @@ def replace_permission_group_bindings(
if missing_systems: if missing_systems:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"system_not_found:{','.join(missing_systems)}") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"system_not_found:{','.join(missing_systems)}")
valid_modules = {m.module_key for m in modules_repo.list(limit=10000, offset=0)[0]} all_modules = modules_repo.list(limit=10000, offset=0)[0]
valid_modules = {m.module_key for m in all_modules}
module_system_lookup = {m.module_key: m.system_key for m in all_modules}
missing_modules = [k for k in module_keys if k not in valid_modules] missing_modules = [k for k in module_keys if k not in valid_modules]
if missing_modules: if missing_modules:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"module_not_found:{','.join(missing_modules)}") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"module_not_found:{','.join(missing_modules)}")
module_pairs = [f"{module_system_lookup[m]}|{m}" for m in module_keys]
repo.replace_group_bindings( repo.replace_group_bindings(
group_id=group.id, group_id=group.id,
site_keys=site_keys, site_keys=site_keys,
system_keys=system_keys, system_keys=system_keys,
module_keys=module_keys, module_keys=module_pairs,
member_subs=payload.member_subs, member_subs=payload.member_subs,
actions=payload.actions, actions=payload.actions,
) )
snapshot = repo.get_group_binding_snapshot(group.id, group_key) snapshot = repo.get_group_binding_snapshot(group.id, group_key)
return GroupBindingSnapshot(**snapshot) return GroupBindingSnapshot(
group_key=snapshot["group_key"],
site_keys=snapshot["site_keys"],
system_keys=snapshot["system_keys"],
module_keys=[k.split("|", 1)[1] if "|" in k else k for k in snapshot["module_keys"]],
member_subs=snapshot["member_subs"],
actions=snapshot["actions"],
)
@router.post("/permission-groups", response_model=PermissionGroupItem) @router.post("/permission-groups", response_model=PermissionGroupItem)

View File

@@ -1,7 +1,7 @@
from datetime import datetime from datetime import datetime
from uuid import uuid4 from uuid import uuid4
from sqlalchemy import DateTime, String, func from sqlalchemy import DateTime, ForeignKey, String, func
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
@@ -12,6 +12,9 @@ class Module(Base):
__tablename__ = "modules" __tablename__ = "modules"
id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4())) id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4()))
system_key: Mapped[str] = mapped_column(
String(128), ForeignKey("systems.system_key", ondelete="CASCADE"), nullable=False, index=True
)
module_key: Mapped[str] = mapped_column(String(128), unique=True, nullable=False, index=True) module_key: Mapped[str] = mapped_column(String(128), unique=True, nullable=False, index=True)
name: Mapped[str] = mapped_column(String(255), nullable=False) name: Mapped[str] = mapped_column(String(255), nullable=False)
status: Mapped[str] = mapped_column(String(16), nullable=False, default="active") status: Mapped[str] = mapped_column(String(16), nullable=False, default="active")

View File

@@ -18,8 +18,8 @@ class ModulesRepository:
stmt = stmt.order_by(Module.created_at.desc()).limit(limit).offset(offset) stmt = stmt.order_by(Module.created_at.desc()).limit(limit).offset(offset)
return list(self.db.scalars(stmt).all()), int(self.db.scalar(count_stmt) or 0) return list(self.db.scalars(stmt).all()), int(self.db.scalar(count_stmt) or 0)
def create(self, module_key: str, name: str, status: str = "active") -> Module: def create(self, module_key: str, system_key: str, name: str, status: str = "active") -> Module:
item = Module(module_key=module_key, name=name, status=status) item = Module(module_key=module_key, system_key=system_key, name=name, status=status)
self.db.add(item) self.db.add(item)
self.db.commit() self.db.commit()
self.db.refresh(item) self.db.refresh(item)

View File

@@ -1,7 +1,5 @@
from __future__ import annotations from __future__ import annotations
from collections import defaultdict
from sqlalchemy import delete, func, select from sqlalchemy import delete, func, select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
@@ -142,17 +140,16 @@ class PermissionGroupsRepository:
normalized_actions = [a for a in list(dict.fromkeys(actions)) if a in {"view", "edit"}] normalized_actions = [a for a in list(dict.fromkeys(actions)) if a in {"view", "edit"}]
normalized_member_subs = list(dict.fromkeys([s for s in member_subs if s])) normalized_member_subs = list(dict.fromkeys([s for s in member_subs if s]))
modules_by_system: dict[str, list[str]] = defaultdict(list)
for full_module_key in list(dict.fromkeys([m for m in module_keys if m])):
if "." not in full_module_key:
continue
system_key, module_name = full_module_key.split(".", 1)
if module_name == "__system__":
continue
modules_by_system[system_key].append(module_name)
normalized_systems = set([s for s in system_keys if s]) normalized_systems = set([s for s in system_keys if s])
normalized_systems.update(modules_by_system.keys()) module_pairs = []
for pair in module_keys:
if "|" not in pair:
continue
system_key, module_key = pair.split("|", 1)
if not system_key or not module_key:
continue
module_pairs.append((system_key, module_key))
normalized_systems.add(system_key)
self.db.execute(delete(PermissionGroupPermission).where(PermissionGroupPermission.group_id == group_id)) self.db.execute(delete(PermissionGroupPermission).where(PermissionGroupPermission.group_id == group_id))
self.db.execute(delete(PermissionGroupMember).where(PermissionGroupMember.group_id == group_id)) self.db.execute(delete(PermissionGroupMember).where(PermissionGroupMember.group_id == group_id))
@@ -163,7 +160,7 @@ class PermissionGroupsRepository:
for site_key in normalized_sites: for site_key in normalized_sites:
for action in normalized_actions: for action in normalized_actions:
for system_key in sorted(normalized_systems): for system_key in sorted(normalized_systems):
module_names = modules_by_system.get(system_key) or ["__system__"] module_names = [m for s, m in module_pairs if s == system_key] or ["__system__"]
for module_name in module_names: for module_name in module_names:
self.db.add( self.db.add(
PermissionGroupPermission( PermissionGroupPermission(
@@ -185,7 +182,7 @@ class PermissionGroupsRepository:
actions = sorted({p.action for p in permissions if p.action in {"view", "edit"}}) actions = sorted({p.action for p in permissions if p.action in {"view", "edit"}})
module_keys = sorted( module_keys = sorted(
{ {
p.module if "." in p.module else f"{p.system}.{p.module}" f"{p.system}|{p.module}"
for p in permissions for p in permissions
if p.module and p.module != "__system__" if p.module and p.module != "__system__"
} }

View File

@@ -21,6 +21,7 @@ class PermissionsRepository:
UserScopePermission.scope_type, UserScopePermission.scope_type,
Company.company_key, Company.company_key,
Site.site_key, Site.site_key,
Module.system_key,
Module.module_key, Module.module_key,
UserScopePermission.action, UserScopePermission.action,
) )
@@ -55,13 +56,10 @@ class PermissionsRepository:
if source == "group": if source == "group":
_, scope_type, scope_id, system_key, module_key, action = row _, scope_type, scope_id, system_key, module_key, action = row
if module_key == "__system__": if module_key == "__system__":
module_key = f"{system_key}.__system__" module_key = f"__system__{system_key}"
elif module_key and "." not in module_key:
module_key = f"{system_key}.{module_key}"
else: else:
_, scope_type, company_key, site_key, module_key, action = row _, scope_type, company_key, site_key, system_key, module_key, action = row
scope_id = company_key if scope_type == "company" else site_key scope_id = company_key if scope_type == "company" else site_key
system_key = module_key.split(".", 1)[0] if isinstance(module_key, str) and "." in module_key else None
key = (scope_type, scope_id or "", system_key, module_key, action) key = (scope_type, scope_id or "", system_key, module_key, action)
if key in dedup: if key in dedup:
continue continue
@@ -146,6 +144,7 @@ class PermissionsRepository:
UserScopePermission.scope_type, UserScopePermission.scope_type,
Company.company_key, Company.company_key,
Site.site_key, Site.site_key,
Module.system_key,
Module.module_key, Module.module_key,
UserScopePermission.action, UserScopePermission.action,
UserScopePermission.created_at, UserScopePermission.created_at,
@@ -200,14 +199,14 @@ class PermissionsRepository:
row_scope_type, row_scope_type,
company_key, company_key,
site_key, site_key,
system_key,
module_key, module_key,
action, action,
created_at, created_at,
) = row ) = row
scope_id = company_key if row_scope_type == "company" else site_key scope_id = company_key if row_scope_type == "company" else site_key
system_key = module_key.split(".", 1)[0] if isinstance(module_key, str) and "." in module_key else None module_name = module_key
module_name = module_key.split(".", 1)[1] if isinstance(module_key, str) and "." in module_key else module_key if isinstance(module_name, str) and module_name.startswith("__system__"):
if module_name == "__system__":
module_name = None module_name = None
items.append( items.append(
{ {

View File

@@ -67,6 +67,7 @@ CREATE TABLE systems (
CREATE TABLE modules ( CREATE TABLE modules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
system_key TEXT NOT NULL REFERENCES systems(system_key) ON DELETE CASCADE,
module_key TEXT NOT NULL UNIQUE, module_key TEXT NOT NULL UNIQUE,
name TEXT NOT NULL, name TEXT NOT NULL,
status VARCHAR(16) NOT NULL DEFAULT 'active', status VARCHAR(16) NOT NULL DEFAULT 'active',
@@ -156,6 +157,7 @@ CREATE INDEX idx_pgp_scope_site ON permission_group_permissions(scope_id);
CREATE INDEX idx_api_clients_status ON api_clients(status); CREATE INDEX idx_api_clients_status ON api_clients(status);
CREATE INDEX idx_api_clients_expires_at ON api_clients(expires_at); CREATE INDEX idx_api_clients_expires_at ON api_clients(expires_at);
CREATE INDEX idx_systems_system_key ON systems(system_key); CREATE INDEX idx_systems_system_key ON systems(system_key);
CREATE INDEX idx_modules_system_key ON modules(system_key);
CREATE INDEX idx_modules_module_key ON modules(module_key); CREATE INDEX idx_modules_module_key ON modules(module_key);
COMMIT; COMMIT;