Skip to content

Commit 2b56168

Browse files
authored
Refactor foreign keys and relationships to pure logic (#901)
* Refactor foreign keys and relationships to pure logic * Revert of some changes * More revert * Update the user paginate * Update user create and update * Update dept select and delete * Rename the join query functions * Update select_join_serialize doc and README * Fix typo in README * Update the user delete * Update the user social * Update the dict plugin crud * Update the dict plugin version * Bump dependencies and pre-commits * Update the code generator plugin crud * Update the menu crud * Update the role crud * Update the data scope and rule crud * Restore get_paginated to get_select * Update the code generator plugin version * Add the py version in pre-commit * Remove the plugin include parameter config * Add more cache cleaning TODO * Rename get_with_relation to get_join * Add the user cache clear * Fix known compatibility issues * Update the version number to 1.11.0 * Fix lint * Optimize select_join_serialize logic * Delete cache cleanup comments * Update the oauth2 plugin version * Fix user-role table cleanup when user update
1 parent b925581 commit 2b56168

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+1051
-767
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ venv/
77
.python-version
88
.ruff_cache/
99
.pytest_cache/
10+
.claude/

.pre-commit-config.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
default_language_version:
2+
python: '>= 3.10'
3+
14
repos:
25
- repo: https://github.com/pre-commit/pre-commit-hooks
36
rev: v6.0.0
47
hooks:
58
- id: end-of-file-fixer
9+
- id: check-json
610
- id: check-yaml
711
- id: check-toml
812

backend/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,10 @@
5050

5151
4. Format and Lint
5252

53-
Auto-formatting and lint via `pre-commit`
53+
Auto-formatting and lint via `prek`
5454

5555
```shell
56-
pre-commit run --all-files
56+
prek run --all-files
5757
```
5858

5959
5. Commit and push
@@ -78,6 +78,6 @@
7878

7979
- `scripts/format.sh`: Perform ruff format check
8080

81-
- `scripts/lint.sh`: Perform pre-commit formatting
81+
- `scripts/lint.sh`: Perform prek formatting
8282

8383
- `scripts/export.sh`: Execute uv export dependency package

backend/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from backend.common.i18n import i18n
22

3-
__version__ = '1.10.4'
3+
__version__ = '1.11.0'
44

55

66
# 初始化 i18n

backend/app/admin/crud/crud_data_rule.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ async def get_select(self, name: str | None) -> Select:
3333
if name is not None:
3434
filters['name__like'] = f'%{name}%'
3535

36-
return await self.select_order('id', load_strategies={'scopes': 'noload'}, **filters)
36+
return await self.select_order('id', **filters)
3737

3838
async def get_by_name(self, db: AsyncSession, name: str) -> DataRule | None:
3939
"""

backend/app/admin/crud/crud_data_scope.py

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
from collections.abc import Sequence
2+
from typing import Any
23

3-
from sqlalchemy import Select, select
4+
from sqlalchemy import Select, delete, insert
45
from sqlalchemy.ext.asyncio import AsyncSession
5-
from sqlalchemy_crud_plus import CRUDPlus
6+
from sqlalchemy_crud_plus import CRUDPlus, JoinConfig
67

78
from backend.app.admin.model import DataRule, DataScope
8-
from backend.app.admin.schema.data_scope import CreateDataScopeParam, UpdateDataScopeParam, UpdateDataScopeRuleParam
9+
from backend.app.admin.model.m2m import data_scope_rule
10+
from backend.app.admin.schema.data_scope import (
11+
CreateDataScopeParam,
12+
CreateDataScopeRuleParam,
13+
UpdateDataScopeParam,
14+
UpdateDataScopeRuleParam,
15+
)
16+
from backend.utils.serializers import select_join_serialize
917

1018

1119
class CRUDDataScope(CRUDPlus[DataScope]):
@@ -31,15 +39,24 @@ async def get_by_name(self, db: AsyncSession, name: str) -> DataScope | None:
3139
"""
3240
return await self.select_model_by_column(db, name=name)
3341

34-
async def get_with_relation(self, db: AsyncSession, pk: int) -> DataScope:
42+
async def get_join(self, db: AsyncSession, pk: int) -> Any:
3543
"""
3644
获取数据范围关联数据
3745
3846
:param db: 数据库会话
3947
:param pk: 范围 ID
4048
:return:
4149
"""
42-
return await self.select_model(db, pk, load_strategies=['rules'])
50+
result = await self.select_models(
51+
db,
52+
id=pk,
53+
join_conditions=[
54+
JoinConfig(model=data_scope_rule, join_on=data_scope_rule.c.data_scope_id == self.model.id),
55+
JoinConfig(model=DataRule, join_on=DataRule.id == data_scope_rule.c.data_rule_id, fill_result=True),
56+
],
57+
)
58+
59+
return select_join_serialize(result, relationships=['DataScope-m2m-DataRule:rules'])
4360

4461
async def get_all(self, db: AsyncSession) -> Sequence[DataScope]:
4562
"""
@@ -65,7 +82,7 @@ async def get_select(self, name: str | None, status: int | None) -> Select:
6582
if status is not None:
6683
filters['status'] = status
6784

68-
return await self.select_order('id', load_strategies={'rules': 'noload', 'roles': 'noload'}, **filters)
85+
return await self.select_order('id', **filters)
6986

7087
async def create(self, db: AsyncSession, obj: CreateDataScopeParam) -> None:
7188
"""
@@ -88,7 +105,8 @@ async def update(self, db: AsyncSession, pk: int, obj: UpdateDataScopeParam) ->
88105
"""
89106
return await self.update_model(db, pk, obj)
90107

91-
async def update_rules(self, db: AsyncSession, pk: int, rule_ids: UpdateDataScopeRuleParam) -> int:
108+
@staticmethod
109+
async def update_rules(db: AsyncSession, pk: int, rule_ids: UpdateDataScopeRuleParam) -> int:
92110
"""
93111
更新数据范围规则
94112
@@ -97,11 +115,16 @@ async def update_rules(self, db: AsyncSession, pk: int, rule_ids: UpdateDataScop
97115
:param rule_ids: 数据规则 ID 列表
98116
:return:
99117
"""
100-
current_data_scope = await self.get_with_relation(db, pk)
101-
stmt = select(DataRule).where(DataRule.id.in_(rule_ids.rules))
102-
rules = await db.execute(stmt)
103-
current_data_scope.rules = rules.scalars().all()
104-
return len(current_data_scope.rules)
118+
data_scope_rule_stmt = delete(data_scope_rule).where(data_scope_rule.c.data_scope_id == pk)
119+
await db.execute(data_scope_rule_stmt)
120+
121+
data_scope_rule_data = [
122+
CreateDataScopeRuleParam(data_scope_id=pk, data_rule_id=rule_id).model_dump() for rule_id in rule_ids.rules
123+
]
124+
data_scope_rule_stmt = insert(data_scope_rule)
125+
await db.execute(data_scope_rule_stmt, data_scope_rule_data)
126+
127+
return len(rule_ids.rules)
105128

106129
async def delete(self, db: AsyncSession, pks: list[int]) -> int:
107130
"""

backend/app/admin/crud/crud_dept.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
from collections.abc import Sequence
2+
from typing import Any
23

34
from sqlalchemy.ext.asyncio import AsyncSession
4-
from sqlalchemy_crud_plus import CRUDPlus
5+
from sqlalchemy_crud_plus import CRUDPlus, JoinConfig
56

6-
from backend.app.admin.model import Dept
7+
from backend.app.admin.model import Dept, User
78
from backend.app.admin.schema.dept import CreateDeptParam, UpdateDeptParam
89
from backend.app.admin.schema.user import GetUserInfoWithRelationDetail
910
from backend.common.security.permission import filter_data_permission
11+
from backend.utils.serializers import select_join_serialize
1012

1113

1214
class CRUDDept(CRUDPlus[Dept]):
@@ -63,8 +65,8 @@ async def get_all(
6365
if status is not None:
6466
filters['status'] = status
6567

66-
data_filtered = filter_data_permission(request_user)
67-
return await self.select_models_order(db, 'sort', 'desc', data_filtered, **filters)
68+
data_filter = filter_data_permission(request_user)
69+
return await self.select_models_order(db, 'sort', 'desc', data_filter, **filters)
6870

6971
async def create(self, db: AsyncSession, obj: CreateDeptParam) -> None:
7072
"""
@@ -97,15 +99,20 @@ async def delete(self, db: AsyncSession, dept_id: int) -> int:
9799
"""
98100
return await self.delete_model_by_column(db, id=dept_id, logical_deletion=True, deleted_flag_column='del_flag')
99101

100-
async def get_with_relation(self, db: AsyncSession, dept_id: int) -> Dept | None:
102+
async def get_join(self, db: AsyncSession, dept_id: int) -> Any | None:
101103
"""
102104
获取部门及关联数据
103105
104106
:param db: 数据库会话
105107
:param dept_id: 部门 ID
106108
:return:
107109
"""
108-
return await self.select_model(db, dept_id, load_strategies=['users'])
110+
result = await self.select_model(
111+
db,
112+
dept_id,
113+
join_conditions=[JoinConfig(model=User, join_on=User.dept_id == self.model.id, fill_result=True)],
114+
)
115+
return select_join_serialize(result, relationships=['Dept-o2m-User'])
109116

110117
async def get_children(self, db: AsyncSession, dept_id: int) -> Sequence[Dept | None]:
111118
"""

backend/app/admin/crud/crud_menu.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
from collections.abc import Sequence
22

3+
from sqlalchemy import delete
34
from sqlalchemy.ext.asyncio import AsyncSession
45
from sqlalchemy_crud_plus import CRUDPlus
56

67
from backend.app.admin.model import Menu
8+
from backend.app.admin.model.m2m import role_menu
79
from backend.app.admin.schema.menu import CreateMenuParam, UpdateMenuParam
810

911

@@ -92,6 +94,9 @@ async def delete(self, db: AsyncSession, menu_id: int) -> int:
9294
:param menu_id: 菜单 ID
9395
:return:
9496
"""
97+
role_menu_stmt = delete(role_menu).where(role_menu.c.menu_id == menu_id)
98+
await db.execute(role_menu_stmt)
99+
95100
return await self.delete_model(db, menu_id)
96101

97102
async def get_children(self, db: AsyncSession, menu_id: int) -> Sequence[Menu | None]:

backend/app/admin/crud/crud_role.py

Lines changed: 58 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
from collections.abc import Sequence
2+
from typing import Any
23

3-
from sqlalchemy import Select, select
4+
from sqlalchemy import Select, delete, insert, select
45
from sqlalchemy.ext.asyncio import AsyncSession
5-
from sqlalchemy_crud_plus import CRUDPlus
6+
from sqlalchemy_crud_plus import CRUDPlus, JoinConfig
67

78
from backend.app.admin.model import DataScope, Menu, Role
9+
from backend.app.admin.model.m2m import role_data_scope, role_menu
810
from backend.app.admin.schema.role import (
11+
CreateRoleMenuParam,
912
CreateRoleParam,
13+
CreateRoleScopeParam,
1014
UpdateRoleMenuParam,
1115
UpdateRoleParam,
1216
UpdateRoleScopeParam,
1317
)
18+
from backend.utils.serializers import select_join_serialize
1419

1520

1621
class CRUDRole(CRUDPlus[Role]):
@@ -26,15 +31,39 @@ async def get(self, db: AsyncSession, role_id: int) -> Role | None:
2631
"""
2732
return await self.select_model(db, role_id)
2833

29-
async def get_with_relation(self, db: AsyncSession, role_id: int) -> Role | None:
34+
@staticmethod
35+
async def get_menus(db: AsyncSession, role_id: int) -> Sequence[Menu] | None:
36+
"""
37+
获取角色菜单
38+
39+
:param db: 数据库会话
40+
:param role_id: 角色 ID
41+
:return:
42+
"""
43+
menu_stmt = select(Menu).join(role_menu, Menu.id == role_menu.c.menu_id).where(role_menu.c.role_id == role_id)
44+
result = await db.execute(menu_stmt)
45+
return result.scalars().all()
46+
47+
async def get_join(self, db: AsyncSession, role_id: int) -> Any:
3048
"""
3149
获取角色及关联数据
3250
3351
:param db: 数据库会话
3452
:param role_id: 角色 ID
3553
:return:
3654
"""
37-
return await self.select_model(db, role_id, load_strategies=['menus', 'scopes'])
55+
result = await self.select_models(
56+
db,
57+
id=role_id,
58+
join_conditions=[
59+
JoinConfig(model=role_menu, join_on=role_menu.c.role_id == self.model.id),
60+
JoinConfig(model=Menu, join_on=Menu.id == role_menu.c.menu_id, fill_result=True),
61+
JoinConfig(model=role_data_scope, join_on=role_data_scope.c.role_id == self.model.id),
62+
JoinConfig(model=DataScope, join_on=DataScope.id == role_data_scope.c.data_scope_id, fill_result=True),
63+
],
64+
)
65+
66+
return select_join_serialize(result, relationships=['Role-m2m-Menu', 'Role-m2m-DataScope:scopes'])
3867

3968
async def get_all(self, db: AsyncSession) -> Sequence[Role]:
4069
"""
@@ -61,15 +90,7 @@ async def get_select(self, name: str | None, status: int | None) -> Select:
6190
if status is not None:
6291
filters['status'] = status
6392

64-
return await self.select_order(
65-
'id',
66-
load_strategies={
67-
'users': 'noload',
68-
'menus': 'noload',
69-
'scopes': 'noload',
70-
},
71-
**filters,
72-
)
93+
return await self.select_order('id', **filters)
7394

7495
async def get_by_name(self, db: AsyncSession, name: str) -> Role | None:
7596
"""
@@ -102,7 +123,8 @@ async def update(self, db: AsyncSession, role_id: int, obj: UpdateRoleParam) ->
102123
"""
103124
return await self.update_model(db, role_id, obj)
104125

105-
async def update_menus(self, db: AsyncSession, role_id: int, menu_ids: UpdateRoleMenuParam) -> int:
126+
@staticmethod
127+
async def update_menus(db: AsyncSession, role_id: int, menu_ids: UpdateRoleMenuParam) -> int:
106128
"""
107129
更新角色菜单
108130
@@ -111,13 +133,19 @@ async def update_menus(self, db: AsyncSession, role_id: int, menu_ids: UpdateRol
111133
:param menu_ids: 菜单 ID 列表
112134
:return:
113135
"""
114-
current_role = await self.get_with_relation(db, role_id)
115-
stmt = select(Menu).where(Menu.id.in_(menu_ids.menus))
116-
menus = await db.execute(stmt)
117-
current_role.menus = menus.scalars().all()
118-
return len(current_role.menus)
136+
role_menu_stmt = delete(role_menu).where(role_menu.c.role_id == role_id)
137+
await db.execute(role_menu_stmt)
138+
139+
role_menu_data = [
140+
CreateRoleMenuParam(role_id=role_id, menu_id=menu_id).model_dump() for menu_id in menu_ids.menus
141+
]
142+
role_menu_stmt = insert(role_menu)
143+
await db.execute(role_menu_stmt, role_menu_data)
119144

120-
async def update_scopes(self, db: AsyncSession, role_id: int, scope_ids: UpdateRoleScopeParam) -> int:
145+
return len(menu_ids.menus)
146+
147+
@staticmethod
148+
async def update_scopes(db: AsyncSession, role_id: int, scope_ids: UpdateRoleScopeParam) -> int:
121149
"""
122150
更新角色数据范围
123151
@@ -126,11 +154,16 @@ async def update_scopes(self, db: AsyncSession, role_id: int, scope_ids: UpdateR
126154
:param scope_ids: 权限范围 ID 列表
127155
:return:
128156
"""
129-
current_role = await self.get_with_relation(db, role_id)
130-
stmt = select(DataScope).where(DataScope.id.in_(scope_ids.scopes))
131-
scopes = await db.execute(stmt)
132-
current_role.scopes = scopes.scalars().all()
133-
return len(current_role.scopes)
157+
role_scope_stmt = delete(role_data_scope).where(role_data_scope.c.role_id == role_id)
158+
await db.execute(role_scope_stmt)
159+
160+
role_scope_data = [
161+
CreateRoleScopeParam(role_id=role_id, data_scope_id=scope_id).model_dump() for scope_id in scope_ids.scopes
162+
]
163+
role_scope_stmt = insert(role_data_scope)
164+
await db.execute(role_scope_stmt, role_scope_data)
165+
166+
return len(scope_ids.scopes)
134167

135168
async def delete(self, db: AsyncSession, role_ids: list[int]) -> int:
136169
"""

0 commit comments

Comments
 (0)