aiteamutils 0.2.51__py3-none-any.whl → 0.2.53__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aiteamutils/base_repository.py +68 -364
- aiteamutils/base_service.py +46 -43
- aiteamutils/database.py +24 -5
- aiteamutils/dependencies.py +183 -105
- aiteamutils/security.py +93 -281
- aiteamutils/version.py +1 -1
- {aiteamutils-0.2.51.dist-info → aiteamutils-0.2.53.dist-info}/METADATA +1 -1
- aiteamutils-0.2.53.dist-info/RECORD +16 -0
- aiteamutils-0.2.51.dist-info/RECORD +0 -16
- {aiteamutils-0.2.51.dist-info → aiteamutils-0.2.53.dist-info}/WHEEL +0 -0
aiteamutils/base_repository.py
CHANGED
@@ -3,281 +3,54 @@ from typing import TypeVar, Generic, Dict, Any, List, Optional, Type, Union
|
|
3
3
|
from sqlalchemy.orm import DeclarativeBase, Load
|
4
4
|
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
|
5
5
|
from sqlalchemy import select, or_, and_
|
6
|
-
from .database import DatabaseService
|
7
6
|
from .exceptions import CustomException, ErrorCode
|
8
7
|
from sqlalchemy.orm import joinedload
|
9
8
|
from sqlalchemy.sql import Select
|
10
9
|
from fastapi import Request
|
10
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
11
11
|
|
12
12
|
ModelType = TypeVar("ModelType", bound=DeclarativeBase)
|
13
13
|
|
14
14
|
class BaseRepository(Generic[ModelType]):
|
15
|
-
|
15
|
+
##################
|
16
16
|
# 1. 초기화 영역 #
|
17
17
|
##################
|
18
|
-
def __init__(self,
|
18
|
+
def __init__(self, session: AsyncSession, model: Type[ModelType]):
|
19
19
|
"""
|
20
20
|
Args:
|
21
|
-
|
21
|
+
session (AsyncSession): 데이터베이스 세션
|
22
22
|
model (Type[ModelType]): 모델 클래스
|
23
23
|
"""
|
24
|
-
self.
|
24
|
+
self.session = session
|
25
25
|
self.model = model
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
"""
|
36
|
-
return select(self.model)
|
37
|
-
|
38
|
-
#######################
|
39
|
-
# 3. 전차리 영역 #
|
40
|
-
#######################
|
41
|
-
def _apply_exact_match(self, stmt: Select, field_name: str, value: Any) -> Select:
|
42
|
-
"""정확한 값 매칭 조건을 적용합니다.
|
43
|
-
|
44
|
-
Args:
|
45
|
-
stmt (Select): 쿼리문
|
46
|
-
field_name (str): 필드명
|
47
|
-
value (Any): 매칭할 값
|
48
|
-
|
49
|
-
Returns:
|
50
|
-
Select: 조건이 적용된 쿼리
|
51
|
-
"""
|
52
|
-
return stmt.where(getattr(self.model, field_name) == value)
|
53
|
-
|
54
|
-
def _apply_like_match(self, stmt: Select, field_name: str, value: str) -> Select:
|
55
|
-
"""LIKE 검색 조건을 적용합니다.
|
56
|
-
|
57
|
-
Args:
|
58
|
-
stmt (Select): 쿼리문
|
59
|
-
field_name (str): 필드명
|
60
|
-
value (str): 검색할 값
|
61
|
-
|
62
|
-
Returns:
|
63
|
-
Select: 조건이 적용된 쿼리
|
64
|
-
"""
|
65
|
-
return stmt.where(getattr(self.model, field_name).ilike(f"%{value}%"))
|
66
|
-
|
67
|
-
def _apply_relation_match(self, stmt: Select, relations: List[str], field_name: str, operator: str, value: Any) -> Select:
|
68
|
-
"""관계 테이블 검색 조건을 적용합니다."""
|
69
|
-
current = self.model
|
70
|
-
|
71
|
-
# 관계 체인 따라가기
|
72
|
-
for i in range(len(relations)-1):
|
73
|
-
current = getattr(current, relations[i]).property.mapper.class_
|
74
|
-
|
75
|
-
# 마지막 모델과 필드
|
76
|
-
final_model = getattr(current, relations[-1]).property.mapper.class_
|
77
|
-
|
78
|
-
# 중첩된 EXISTS 절 생성
|
79
|
-
current = self.model
|
80
|
-
subq = select(1)
|
81
|
-
|
82
|
-
# 첫 번째 관계
|
83
|
-
next_model = getattr(current, relations[0]).property.mapper.class_
|
84
|
-
subq = subq.where(getattr(next_model, 'ulid') == getattr(current, f"{relations[0]}_ulid"))
|
85
|
-
|
86
|
-
# 중간 관계들
|
87
|
-
for i in range(1, len(relations)):
|
88
|
-
prev_model = next_model
|
89
|
-
next_model = getattr(prev_model, relations[i]).property.mapper.class_
|
90
|
-
subq = subq.where(getattr(next_model, 'ulid') == getattr(prev_model, f"{relations[i]}_ulid"))
|
91
|
-
|
92
|
-
# 최종 검색 조건
|
93
|
-
subq = subq.where(getattr(final_model, field_name).__getattribute__(operator)(value))
|
94
|
-
|
95
|
-
return stmt.where(subq.exists())
|
96
|
-
|
97
|
-
def _apply_ordering(self, stmt: Select, order_by: List[str]) -> Select:
|
98
|
-
"""정렬 조건을 적용합니다.
|
99
|
-
|
100
|
-
Args:
|
101
|
-
stmt (Select): 쿼리문
|
102
|
-
order_by (List[str]): 정렬 기준 필드 목록 (예: ["name", "-created_at"])
|
103
|
-
|
104
|
-
Returns:
|
105
|
-
Select: 정렬이 적용된 쿼리
|
106
|
-
"""
|
107
|
-
for field in order_by:
|
108
|
-
if field.startswith("-"):
|
109
|
-
field_name = field[1:]
|
110
|
-
stmt = stmt.order_by(getattr(self.model, field_name).desc())
|
111
|
-
else:
|
112
|
-
stmt = stmt.order_by(getattr(self.model, field).asc())
|
113
|
-
return stmt
|
114
|
-
|
115
|
-
def _apply_pagination(self, stmt: Select, skip: int = 0, limit: int = 100) -> Select:
|
116
|
-
"""페이징을 적용합니다.
|
117
|
-
|
118
|
-
Args:
|
119
|
-
stmt (Select): 쿼리문
|
120
|
-
skip (int): 건너뛸 레코드 수
|
121
|
-
limit (int): 조회할 최대 레코드 수
|
122
|
-
|
123
|
-
Returns:
|
124
|
-
Select: 페이징이 적용된 쿼리
|
125
|
-
"""
|
126
|
-
return stmt.offset(skip).limit(limit)
|
127
|
-
|
128
|
-
def _apply_joins(self, stmt: Select, joins: List[str]) -> Select:
|
129
|
-
"""조인을 적용합니다.
|
130
|
-
|
131
|
-
Args:
|
132
|
-
stmt (Select): 쿼리문
|
133
|
-
joins (List[str]): 조인할 관계명 목록
|
134
|
-
|
135
|
-
Returns:
|
136
|
-
Select: 조인이 적용된 쿼리
|
137
|
-
"""
|
138
|
-
for join in joins:
|
139
|
-
stmt = stmt.options(joinedload(getattr(self.model, join)))
|
140
|
-
return stmt
|
141
|
-
|
142
|
-
def _build_jsonb_condition(self, model: Any, field_path: str, value: str) -> Any:
|
143
|
-
"""JSONB 필드에 대한 검색 조건을 생성합니다.
|
144
|
-
|
145
|
-
Args:
|
146
|
-
model: 대상 모델
|
147
|
-
field_path (str): JSONB 키 경로 (예: "address", "name.first")
|
148
|
-
value (str): 검색할 값
|
149
|
-
|
150
|
-
Returns:
|
151
|
-
Any: SQLAlchemy 검색 조건
|
152
|
-
"""
|
153
|
-
# JSONB 경로가 중첩된 경우 (예: "name.first")
|
154
|
-
if "." in field_path:
|
155
|
-
path_parts = field_path.split(".")
|
156
|
-
jsonb_path = "{" + ",".join(path_parts) + "}"
|
157
|
-
return model.extra_data[jsonb_path].astext.ilike(f"%{value}%")
|
158
|
-
# 단일 키인 경우
|
159
|
-
return model.extra_data[field_path].astext.ilike(f"%{value}%")
|
160
|
-
|
161
|
-
def _apply_jsonb_match(self, stmt: Select, relations: List[str], json_key: str, value: str) -> Select:
|
162
|
-
"""JSONB 필드 검색 조건을 적용합니다.
|
163
|
-
|
164
|
-
Args:
|
165
|
-
stmt (Select): 쿼리문
|
166
|
-
relations (List[str]): 관계 테이블 경로
|
167
|
-
json_key (str): JSONB 키 경로
|
168
|
-
value (str): 검색할 값
|
169
|
-
|
170
|
-
Returns:
|
171
|
-
Select: 조건이 적용된 쿼리
|
172
|
-
"""
|
173
|
-
current = self.model
|
174
|
-
|
175
|
-
# 단일 모델 검색
|
176
|
-
if not relations:
|
177
|
-
condition = self._build_jsonb_condition(current, json_key, value)
|
178
|
-
return stmt.where(condition)
|
179
|
-
|
180
|
-
# 관계 모델 검색
|
181
|
-
for i in range(len(relations)-1):
|
182
|
-
current = getattr(current, relations[i]).property.mapper.class_
|
183
|
-
|
184
|
-
final_model = getattr(current, relations[-1]).property.mapper.class_
|
185
|
-
|
186
|
-
# 관계 체인 구성
|
187
|
-
if len(relations) == 1:
|
188
|
-
condition = getattr(self.model, relations[0]).has(
|
189
|
-
self._build_jsonb_condition(final_model, json_key, value)
|
190
|
-
)
|
191
|
-
else:
|
192
|
-
condition = getattr(self.model, relations[0]).has(
|
193
|
-
getattr(final_model, relations[-1]).has(
|
194
|
-
self._build_jsonb_condition(final_model, json_key, value)
|
195
|
-
)
|
26
|
+
|
27
|
+
@property
|
28
|
+
def session(self):
|
29
|
+
"""현재 세션을 반환합니다."""
|
30
|
+
if self._session is None:
|
31
|
+
raise CustomException(
|
32
|
+
ErrorCode.DB_CONNECTION_ERROR,
|
33
|
+
detail="Database session is not set",
|
34
|
+
source_function=f"{self.__class__.__name__}.session"
|
196
35
|
)
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
def
|
201
|
-
"""
|
202
|
-
|
203
|
-
return stmt
|
204
|
-
|
205
|
-
for key, value in search_params.items():
|
206
|
-
if not value.get("value"):
|
207
|
-
continue
|
208
|
-
|
209
|
-
conditions = []
|
210
|
-
for field in value.get("fields", []):
|
211
|
-
parts = field.split('.')
|
212
|
-
|
213
|
-
if len(parts) == 1:
|
214
|
-
# 직접 필드 검색
|
215
|
-
condition = self._apply_like_match(stmt, parts[0], value["value"]).whereclause
|
216
|
-
elif 'extra_data' in parts:
|
217
|
-
# JSONB 필드 검색
|
218
|
-
extra_data_idx = parts.index('extra_data')
|
219
|
-
tables = parts[:extra_data_idx]
|
220
|
-
json_key = ".".join(parts[extra_data_idx + 1:])
|
221
|
-
condition = self._apply_jsonb_match(
|
222
|
-
stmt,
|
223
|
-
tables,
|
224
|
-
json_key,
|
225
|
-
value["value"]
|
226
|
-
).whereclause
|
227
|
-
else:
|
228
|
-
# 관계 테이블 검색
|
229
|
-
condition = self._apply_relation_match(
|
230
|
-
stmt,
|
231
|
-
parts[:-1],
|
232
|
-
parts[-1],
|
233
|
-
"ilike",
|
234
|
-
f"%{value['value']}%"
|
235
|
-
).whereclause
|
236
|
-
|
237
|
-
conditions.append(condition)
|
238
|
-
|
239
|
-
if conditions:
|
240
|
-
stmt = stmt.where(or_(*conditions))
|
241
|
-
|
242
|
-
return stmt
|
243
|
-
|
244
|
-
def _apply_filters(self, stmt, filters: Dict[str, Any]):
|
245
|
-
"""일반 필터를 적용합니다."""
|
246
|
-
for key, value in filters.items():
|
247
|
-
if value is None:
|
248
|
-
continue
|
249
|
-
|
250
|
-
if "." in key:
|
251
|
-
# 관계 테이블 필터
|
252
|
-
relation, field = key.split(".")
|
253
|
-
stmt = self._apply_relation_match(stmt, relation, field, "__eq__", value)
|
254
|
-
else:
|
255
|
-
# 일반 필드 필터
|
256
|
-
stmt = stmt.where(getattr(self.model, key) == value)
|
257
|
-
|
258
|
-
return stmt
|
36
|
+
return self._session
|
37
|
+
|
38
|
+
@session.setter
|
39
|
+
def session(self, value):
|
40
|
+
"""세션을 설정합니다."""
|
41
|
+
self._session = value
|
259
42
|
|
260
43
|
#######################
|
261
|
-
#
|
44
|
+
# 2. CRUD 작업 #
|
262
45
|
#######################
|
263
46
|
async def get(
|
264
47
|
self,
|
265
48
|
ulid: str
|
266
49
|
) -> Optional[Dict[str, Any]]:
|
267
|
-
"""ULID로 엔티티를 조회합니다.
|
268
|
-
|
269
|
-
Args:
|
270
|
-
ulid (str): 조회할 엔티티의 ULID
|
271
|
-
|
272
|
-
Returns:
|
273
|
-
Optional[Dict[str, Any]]: 조회된 엔티티, 없으면 None
|
274
|
-
|
275
|
-
Raises:
|
276
|
-
CustomException: 데이터베이스 작업 중 오류 발생 시
|
277
|
-
"""
|
50
|
+
"""ULID로 엔티티를 조회합니다."""
|
278
51
|
try:
|
279
52
|
stmt = select(self.model).filter_by(ulid=ulid, is_deleted=False)
|
280
|
-
result = await self.
|
53
|
+
result = await self.session.execute(stmt)
|
281
54
|
entity = result.scalars().unique().first()
|
282
55
|
|
283
56
|
if not entity:
|
@@ -298,14 +71,7 @@ class BaseRepository(Generic[ModelType]):
|
|
298
71
|
source_function=f"{self.__class__.__name__}.get",
|
299
72
|
original_error=e
|
300
73
|
)
|
301
|
-
|
302
|
-
raise CustomException(
|
303
|
-
ErrorCode.DB_QUERY_ERROR,
|
304
|
-
detail=f"Unexpected repository error in {self.model.__tablename__}: {str(e)}",
|
305
|
-
source_function=f"{self.__class__.__name__}.get",
|
306
|
-
original_error=e
|
307
|
-
)
|
308
|
-
|
74
|
+
|
309
75
|
async def list(
|
310
76
|
self,
|
311
77
|
skip: int = 0,
|
@@ -328,7 +94,7 @@ class BaseRepository(Generic[ModelType]):
|
|
328
94
|
# 페이지네이션 적용
|
329
95
|
stmt = stmt.limit(limit).offset(skip)
|
330
96
|
|
331
|
-
result = await self.
|
97
|
+
result = await self.session.execute(stmt)
|
332
98
|
return result.scalars().unique().all()
|
333
99
|
|
334
100
|
except SQLAlchemyError as e:
|
@@ -340,165 +106,103 @@ class BaseRepository(Generic[ModelType]):
|
|
340
106
|
)
|
341
107
|
|
342
108
|
async def create(self, data: Dict[str, Any]) -> ModelType:
|
343
|
-
"""새로운 엔티티를 생성합니다.
|
344
|
-
|
345
|
-
Args:
|
346
|
-
data (Dict[str, Any]): 생성할 엔티티 데이터
|
347
|
-
|
348
|
-
Returns:
|
349
|
-
ModelType: 생성된 엔티티
|
350
|
-
|
351
|
-
Raises:
|
352
|
-
CustomException: 데이터베이스 작업 중 오류 발생 시
|
353
|
-
"""
|
109
|
+
"""새로운 엔티티를 생성합니다."""
|
354
110
|
try:
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
111
|
+
entity = self.model(**data)
|
112
|
+
self.session.add(entity)
|
113
|
+
await self.session.flush()
|
114
|
+
await self.session.refresh(entity)
|
115
|
+
return entity
|
360
116
|
except IntegrityError as e:
|
117
|
+
await self.session.rollback()
|
361
118
|
self._handle_integrity_error(e, "create", data)
|
362
119
|
except SQLAlchemyError as e:
|
120
|
+
await self.session.rollback()
|
363
121
|
raise CustomException(
|
364
122
|
ErrorCode.DB_CREATE_ERROR,
|
365
123
|
detail=f"Database create error in {self.model.__tablename__}: {str(e)}",
|
366
124
|
source_function=f"{self.__class__.__name__}.create",
|
367
125
|
original_error=e
|
368
126
|
)
|
369
|
-
except Exception as e:
|
370
|
-
raise CustomException(
|
371
|
-
ErrorCode.DB_CREATE_ERROR,
|
372
|
-
detail=f"Unexpected repository create error in {self.model.__tablename__}: {str(e)}",
|
373
|
-
source_function=f"{self.__class__.__name__}.create",
|
374
|
-
original_error=e
|
375
|
-
)
|
376
127
|
|
377
128
|
async def update(self, ulid: str, data: Dict[str, Any]) -> Optional[ModelType]:
|
378
|
-
"""기존 엔티티를 수정합니다.
|
379
|
-
|
380
|
-
Args:
|
381
|
-
ulid (str): 수정할 엔티티의 ULID
|
382
|
-
data (Dict[str, Any]): 수정할 데이터
|
383
|
-
|
384
|
-
Returns:
|
385
|
-
Optional[ModelType]: 수정된 엔티티, 없으면 None
|
386
|
-
|
387
|
-
Raises:
|
388
|
-
CustomException: 데이터베이스 작업 중 오류 발생 시
|
389
|
-
"""
|
129
|
+
"""기존 엔티티를 수정합니다."""
|
390
130
|
try:
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
)
|
131
|
+
stmt = select(self.model).filter_by(ulid=ulid, is_deleted=False)
|
132
|
+
result = await self.session.execute(stmt)
|
133
|
+
entity = result.scalars().first()
|
134
|
+
|
396
135
|
if not entity:
|
397
136
|
raise CustomException(
|
398
137
|
ErrorCode.DB_NO_RESULT,
|
399
138
|
detail=f"{self.model.__tablename__}|ulid|{ulid}",
|
400
139
|
source_function=f"{self.__class__.__name__}.update"
|
401
140
|
)
|
141
|
+
|
142
|
+
for key, value in data.items():
|
143
|
+
setattr(entity, key, value)
|
144
|
+
|
145
|
+
await self.session.flush()
|
146
|
+
await self.session.refresh(entity)
|
402
147
|
return entity
|
403
|
-
|
404
|
-
e.detail = f"Repository update error for {self.model.__tablename__}: {e.detail}"
|
405
|
-
e.source_function = f"{self.__class__.__name__}.update -> {e.source_function}"
|
406
|
-
raise e
|
148
|
+
|
407
149
|
except IntegrityError as e:
|
150
|
+
await self.session.rollback()
|
408
151
|
self._handle_integrity_error(e, "update", data)
|
409
152
|
except SQLAlchemyError as e:
|
153
|
+
await self.session.rollback()
|
410
154
|
raise CustomException(
|
411
155
|
ErrorCode.DB_UPDATE_ERROR,
|
412
156
|
detail=f"Database update error in {self.model.__tablename__}: {str(e)}",
|
413
157
|
source_function=f"{self.__class__.__name__}.update",
|
414
158
|
original_error=e
|
415
159
|
)
|
416
|
-
except Exception as e:
|
417
|
-
raise CustomException(
|
418
|
-
ErrorCode.DB_UPDATE_ERROR,
|
419
|
-
detail=f"Unexpected repository update error in {self.model.__tablename__}: {str(e)}",
|
420
|
-
source_function=f"{self.__class__.__name__}.update",
|
421
|
-
original_error=e
|
422
|
-
)
|
423
160
|
|
424
161
|
async def delete(self, ulid: str) -> bool:
|
425
|
-
"""엔티티를 소프트
|
426
|
-
|
427
|
-
Args:
|
428
|
-
ulid (str): 삭제할 엔티티의 ULID
|
429
|
-
|
430
|
-
Returns:
|
431
|
-
bool: 삭제 성공 여부
|
432
|
-
|
433
|
-
Raises:
|
434
|
-
CustomException: 데이터베이스 작업 중 오류 발생 시
|
435
|
-
"""
|
162
|
+
"""엔티티를 소프트 삭제합니다."""
|
436
163
|
try:
|
437
|
-
|
164
|
+
stmt = select(self.model).filter_by(ulid=ulid, is_deleted=False)
|
165
|
+
result = await self.session.execute(stmt)
|
166
|
+
entity = result.scalars().first()
|
167
|
+
|
438
168
|
if not entity:
|
439
169
|
raise CustomException(
|
440
170
|
ErrorCode.DB_NO_RESULT,
|
441
171
|
detail=f"{self.model.__tablename__}|ulid|{ulid}",
|
442
172
|
source_function=f"{self.__class__.__name__}.delete"
|
443
173
|
)
|
174
|
+
|
175
|
+
entity.is_deleted = True
|
176
|
+
await self.session.flush()
|
444
177
|
return True
|
445
|
-
|
446
|
-
e.detail = f"Repository delete error for {self.model.__tablename__}: {e.detail}"
|
447
|
-
e.source_function = f"{self.__class__.__name__}.delete -> {e.source_function}"
|
448
|
-
raise e
|
449
|
-
except IntegrityError as e:
|
450
|
-
self._handle_integrity_error(e, "delete")
|
178
|
+
|
451
179
|
except SQLAlchemyError as e:
|
180
|
+
await self.session.rollback()
|
452
181
|
raise CustomException(
|
453
182
|
ErrorCode.DB_DELETE_ERROR,
|
454
183
|
detail=f"Database delete error in {self.model.__tablename__}: {str(e)}",
|
455
184
|
source_function=f"{self.__class__.__name__}.delete",
|
456
185
|
original_error=e
|
457
186
|
)
|
458
|
-
except Exception as e:
|
459
|
-
raise CustomException(
|
460
|
-
ErrorCode.DB_DELETE_ERROR,
|
461
|
-
detail=f"Unexpected repository delete error in {self.model.__tablename__}: {str(e)}",
|
462
|
-
source_function=f"{self.__class__.__name__}.delete",
|
463
|
-
original_error=e
|
464
|
-
)
|
465
|
-
|
466
|
-
async def real_row_delete(self, ulid: str) -> bool:
|
467
|
-
"""엔티티를 실제로 삭제합니다.
|
468
|
-
|
469
|
-
Args:
|
470
|
-
ulid (str): 삭제할 엔티티의 ULID
|
471
187
|
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
Raises:
|
476
|
-
CustomException: 데이터베이스 작업 중 오류 발생 시
|
477
|
-
"""
|
188
|
+
async def real_delete(self, ulid: str) -> bool:
|
189
|
+
"""엔티티를 실제로 삭제합니다."""
|
478
190
|
try:
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
191
|
+
stmt = select(self.model).filter_by(ulid=ulid)
|
192
|
+
result = await self.session.execute(stmt)
|
193
|
+
entity = result.scalars().first()
|
194
|
+
|
483
195
|
if entity:
|
484
|
-
await self.
|
196
|
+
await self.session.delete(entity)
|
197
|
+
await self.session.flush()
|
485
198
|
return True
|
486
199
|
return False
|
487
|
-
|
488
|
-
e.detail = f"Repository real delete error for {self.model.__tablename__}: {e.detail}"
|
489
|
-
e.source_function = f"{self.__class__.__name__}.real_row_delete -> {e.source_function}"
|
490
|
-
raise e
|
200
|
+
|
491
201
|
except SQLAlchemyError as e:
|
202
|
+
await self.session.rollback()
|
492
203
|
raise CustomException(
|
493
204
|
ErrorCode.DB_DELETE_ERROR,
|
494
205
|
detail=f"Database real delete error in {self.model.__tablename__}: {str(e)}",
|
495
|
-
source_function=f"{self.__class__.__name__}.
|
496
|
-
original_error=e
|
497
|
-
)
|
498
|
-
except Exception as e:
|
499
|
-
raise CustomException(
|
500
|
-
ErrorCode.DB_DELETE_ERROR,
|
501
|
-
detail=f"Unexpected repository real delete error in {self.model.__tablename__}: {str(e)}",
|
502
|
-
source_function=f"{self.__class__.__name__}.real_row_delete",
|
206
|
+
source_function=f"{self.__class__.__name__}.real_delete",
|
503
207
|
original_error=e
|
504
208
|
)
|
aiteamutils/base_service.py
CHANGED
@@ -9,6 +9,7 @@ from .base_repository import BaseRepository
|
|
9
9
|
from .security import hash_password
|
10
10
|
from fastapi import Request
|
11
11
|
from ulid import ULID
|
12
|
+
from sqlalchemy import select
|
12
13
|
|
13
14
|
ModelType = TypeVar("ModelType", bound=DeclarativeBase)
|
14
15
|
|
@@ -30,11 +31,29 @@ class BaseService(Generic[ModelType]):
|
|
30
31
|
self.repository = repository
|
31
32
|
self.model = repository.model
|
32
33
|
self.additional_models = additional_models or {}
|
33
|
-
self.
|
34
|
+
self._session = None
|
34
35
|
self.searchable_fields = {
|
35
36
|
"name": {"type": "text", "description": "이름"},
|
36
37
|
"organization_ulid": {"type": "exact", "description": "조직 ID"}
|
37
38
|
}
|
39
|
+
|
40
|
+
@property
|
41
|
+
def session(self):
|
42
|
+
"""현재 세션을 반환합니다."""
|
43
|
+
if self._session is None:
|
44
|
+
raise CustomException(
|
45
|
+
ErrorCode.DB_CONNECTION_ERROR,
|
46
|
+
detail="Database session is not set",
|
47
|
+
source_function=f"{self.__class__.__name__}.session"
|
48
|
+
)
|
49
|
+
return self._session
|
50
|
+
|
51
|
+
@session.setter
|
52
|
+
def session(self, value):
|
53
|
+
"""세션을 설정합니다."""
|
54
|
+
self._session = value
|
55
|
+
if hasattr(self.repository, 'session'):
|
56
|
+
self.repository.session = value
|
38
57
|
|
39
58
|
#########################
|
40
59
|
# 2. 이벤트 처리 메서드 #
|
@@ -448,18 +467,7 @@ class BaseService(Generic[ModelType]):
|
|
448
467
|
)
|
449
468
|
|
450
469
|
async def delete(self, ulid: str, model_name: str = None) -> bool:
|
451
|
-
"""엔티티를 소프트 삭제합니다 (is_deleted = True).
|
452
|
-
|
453
|
-
Args:
|
454
|
-
ulid (str): 삭제할 엔티티의 ULID
|
455
|
-
model_name (str, optional): 삭제할 모델 이름. Defaults to None.
|
456
|
-
|
457
|
-
Returns:
|
458
|
-
bool: 삭제 성공 여부
|
459
|
-
|
460
|
-
Raises:
|
461
|
-
CustomException: 데이터베이스 작업 중 오류 발생 시
|
462
|
-
"""
|
470
|
+
"""엔티티를 소프트 삭제합니다 (is_deleted = True)."""
|
463
471
|
try:
|
464
472
|
if model_name:
|
465
473
|
if model_name not in self.additional_models:
|
@@ -468,23 +476,24 @@ class BaseService(Generic[ModelType]):
|
|
468
476
|
detail=f"Model {model_name} not registered",
|
469
477
|
source_function=f"{self.__class__.__name__}.delete"
|
470
478
|
)
|
471
|
-
|
479
|
+
|
480
|
+
stmt = select(self.additional_models[model_name]).filter_by(ulid=ulid, is_deleted=False)
|
481
|
+
result = await self.session.execute(stmt)
|
482
|
+
entity = result.scalars().first()
|
483
|
+
|
472
484
|
if not entity:
|
473
485
|
raise CustomException(
|
474
486
|
ErrorCode.NOT_FOUND,
|
475
487
|
detail=f"{self.additional_models[model_name].__tablename__}|ulid|{ulid}",
|
476
488
|
source_function=f"{self.__class__.__name__}.delete"
|
477
489
|
)
|
490
|
+
|
491
|
+
entity.is_deleted = True
|
492
|
+
await self.session.flush()
|
478
493
|
return True
|
479
494
|
|
480
|
-
|
481
|
-
|
482
|
-
raise CustomException(
|
483
|
-
ErrorCode.NOT_FOUND,
|
484
|
-
detail=f"{self.model.__tablename__}|ulid|{ulid}",
|
485
|
-
source_function=f"{self.__class__.__name__}.delete"
|
486
|
-
)
|
487
|
-
return True
|
495
|
+
return await self.repository.delete(ulid)
|
496
|
+
|
488
497
|
except CustomException as e:
|
489
498
|
raise e
|
490
499
|
except Exception as e:
|
@@ -549,20 +558,7 @@ class BaseService(Generic[ModelType]):
|
|
549
558
|
request: Request | None = None,
|
550
559
|
response_model: Any = None
|
551
560
|
) -> List[Dict[str, Any]]:
|
552
|
-
"""엔티티 목록을 조회합니다.
|
553
|
-
|
554
|
-
Args:
|
555
|
-
skip (int, optional): 건너뛸 레코드 수. Defaults to 0.
|
556
|
-
limit (int, optional): 조회할 최대 레코드 수. Defaults to 100.
|
557
|
-
filters (Dict[str, Any] | None, optional): 필터링 조건. Defaults to None.
|
558
|
-
search_params (Dict[str, Any] | None, optional): 검색 파라미터. Defaults to None.
|
559
|
-
model_name (str | None, optional): 조회할 모델 이름. Defaults to None.
|
560
|
-
request (Request | None, optional): 요청 객체. Defaults to None.
|
561
|
-
response_model (Any, optional): 응답 스키마. Defaults to None.
|
562
|
-
|
563
|
-
Returns:
|
564
|
-
List[Dict[str, Any]]: 엔티티 목록
|
565
|
-
"""
|
561
|
+
"""엔티티 목록을 조회합니다."""
|
566
562
|
try:
|
567
563
|
if model_name:
|
568
564
|
if model_name not in self.additional_models:
|
@@ -571,21 +567,28 @@ class BaseService(Generic[ModelType]):
|
|
571
567
|
detail=f"Model {model_name} not registered",
|
572
568
|
source_function=f"{self.__class__.__name__}.list"
|
573
569
|
)
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
limit=limit,
|
578
|
-
filters=filters
|
570
|
+
|
571
|
+
stmt = select(self.additional_models[model_name]).where(
|
572
|
+
self.additional_models[model_name].is_deleted == False
|
579
573
|
)
|
574
|
+
|
575
|
+
if filters:
|
576
|
+
for key, value in filters.items():
|
577
|
+
if value is not None:
|
578
|
+
stmt = stmt.where(getattr(self.additional_models[model_name], key) == value)
|
579
|
+
|
580
|
+
stmt = stmt.offset(skip).limit(limit)
|
581
|
+
result = await self.session.execute(stmt)
|
582
|
+
entities = result.scalars().all()
|
583
|
+
|
580
584
|
return [self._process_response(entity, response_model) for entity in entities]
|
581
585
|
|
582
|
-
|
586
|
+
return await self.repository.list(
|
583
587
|
skip=skip,
|
584
588
|
limit=limit,
|
585
589
|
filters=filters,
|
586
590
|
search_params=search_params
|
587
591
|
)
|
588
|
-
return [self._process_response(entity, response_model) for entity in entities]
|
589
592
|
|
590
593
|
except CustomException as e:
|
591
594
|
e.detail = f"Service list error for {self.repository.model.__tablename__}: {e.detail}"
|