aiteamutils 0.2.0__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/__init__.py +60 -0
- aiteamutils/base_model.py +81 -0
- aiteamutils/base_repository.py +503 -0
- aiteamutils/base_service.py +668 -0
- aiteamutils/cache.py +48 -0
- aiteamutils/config.py +26 -0
- aiteamutils/database.py +823 -0
- aiteamutils/dependencies.py +158 -0
- aiteamutils/enums.py +23 -0
- aiteamutils/exceptions.py +333 -0
- aiteamutils/security.py +396 -0
- aiteamutils/validators.py +188 -0
- aiteamutils-0.2.0.dist-info/METADATA +72 -0
- aiteamutils-0.2.0.dist-info/RECORD +15 -0
- aiteamutils-0.2.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,668 @@
|
|
1
|
+
"""기본 서비스 모듈."""
|
2
|
+
from datetime import datetime
|
3
|
+
from typing import TypeVar, Generic, Dict, Any, List, Optional, Type, Union
|
4
|
+
from sqlalchemy.orm import DeclarativeBase
|
5
|
+
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
|
6
|
+
from .database import DatabaseService
|
7
|
+
from .exceptions import CustomException, ErrorCode
|
8
|
+
from .base_repository import BaseRepository
|
9
|
+
from .security import hash_password
|
10
|
+
from fastapi import Request
|
11
|
+
from ulid import ULID
|
12
|
+
|
13
|
+
ModelType = TypeVar("ModelType", bound=DeclarativeBase)
|
14
|
+
|
15
|
+
class BaseService(Generic[ModelType]):
|
16
|
+
|
17
|
+
##################
|
18
|
+
# 1. 초기화 영역 #
|
19
|
+
##################
|
20
|
+
def __init__(
|
21
|
+
self,
|
22
|
+
repository: BaseRepository[ModelType],
|
23
|
+
additional_models: Dict[str, Type[DeclarativeBase]] = None
|
24
|
+
):
|
25
|
+
"""
|
26
|
+
Args:
|
27
|
+
repository (BaseRepository[ModelType]): 레포지토리 인스턴스
|
28
|
+
additional_models (Dict[str, Type[DeclarativeBase]], optional): 추가 모델 매핑. Defaults to None.
|
29
|
+
"""
|
30
|
+
self.repository = repository
|
31
|
+
self.model = repository.model
|
32
|
+
self.additional_models = additional_models or {}
|
33
|
+
self.db_service = repository.db_service
|
34
|
+
self.searchable_fields = {
|
35
|
+
"name": {"type": "text", "description": "이름"},
|
36
|
+
"organization_ulid": {"type": "exact", "description": "조직 ID"}
|
37
|
+
}
|
38
|
+
|
39
|
+
#########################
|
40
|
+
# 2. 이벤트 처리 메서드 #
|
41
|
+
#########################
|
42
|
+
async def pre_save(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
43
|
+
"""저장 전 처리를 수행합니다.
|
44
|
+
|
45
|
+
Args:
|
46
|
+
data (Dict[str, Any]): 저장할 데이터
|
47
|
+
|
48
|
+
Returns:
|
49
|
+
Dict[str, Any]: 처리된 데이터
|
50
|
+
"""
|
51
|
+
return data
|
52
|
+
|
53
|
+
async def post_save(self, entity: ModelType) -> None:
|
54
|
+
"""저장 후 처리를 수행합니다.
|
55
|
+
|
56
|
+
Args:
|
57
|
+
entity (ModelType): 저장된 엔티티
|
58
|
+
"""
|
59
|
+
pass
|
60
|
+
|
61
|
+
async def pre_delete(self, ulid: str) -> None:
|
62
|
+
"""삭제 전 처리를 수행합니다.
|
63
|
+
|
64
|
+
Args:
|
65
|
+
ulid (str): 삭제할 엔티티의 ULID
|
66
|
+
"""
|
67
|
+
pass
|
68
|
+
|
69
|
+
async def post_delete(self, ulid: str) -> None:
|
70
|
+
"""삭제 후 처리를 수행합니다.
|
71
|
+
|
72
|
+
Args:
|
73
|
+
ulid (str): 삭제된 엔티티의 ULID
|
74
|
+
"""
|
75
|
+
pass
|
76
|
+
|
77
|
+
######################
|
78
|
+
# 3. 캐시 관리 메서드 #
|
79
|
+
######################
|
80
|
+
async def get_from_cache(self, key: str) -> Optional[Any]:
|
81
|
+
"""캐시에서 데이터를 조회합니다.
|
82
|
+
|
83
|
+
Args:
|
84
|
+
key (str): 캐시 키
|
85
|
+
|
86
|
+
Returns:
|
87
|
+
Optional[Any]: 캐시된 데이터 또는 None
|
88
|
+
"""
|
89
|
+
return None
|
90
|
+
|
91
|
+
async def set_to_cache(self, key: str, value: Any, ttl: int = 3600) -> None:
|
92
|
+
"""데이터를 캐시에 저장합니다.
|
93
|
+
|
94
|
+
Args:
|
95
|
+
key (str): 캐시 키
|
96
|
+
value (Any): 저장할 값
|
97
|
+
ttl (int, optional): 캐시 유효 시간(초). Defaults to 3600.
|
98
|
+
"""
|
99
|
+
pass
|
100
|
+
|
101
|
+
async def invalidate_cache(self, key: str) -> None:
|
102
|
+
"""캐시를 무효화합니다.
|
103
|
+
|
104
|
+
Args:
|
105
|
+
key (str): 캐시 키
|
106
|
+
"""
|
107
|
+
pass
|
108
|
+
|
109
|
+
##########################
|
110
|
+
# 4. 비즈니스 검증 메서드 #
|
111
|
+
##########################
|
112
|
+
def _validate_business_rules(self, data: Dict[str, Any]) -> None:
|
113
|
+
"""비즈니스 규칙을 검증합니다.
|
114
|
+
|
115
|
+
Args:
|
116
|
+
data (Dict[str, Any]): 검증할 데이터
|
117
|
+
|
118
|
+
Raises:
|
119
|
+
CustomException: 비즈니스 규칙 위반 시
|
120
|
+
"""
|
121
|
+
pass
|
122
|
+
|
123
|
+
def _validate_permissions(self, request: Request, action: str) -> None:
|
124
|
+
"""권한을 검증합니다.
|
125
|
+
|
126
|
+
Args:
|
127
|
+
request (Request): FastAPI 요청 객체
|
128
|
+
action (str): 수행할 작업
|
129
|
+
|
130
|
+
Raises:
|
131
|
+
CustomException: 권한이 없는 경우
|
132
|
+
"""
|
133
|
+
pass
|
134
|
+
|
135
|
+
########################
|
136
|
+
# 5. 응답 처리 메서드 #
|
137
|
+
########################
|
138
|
+
def _handle_response_model(self, entity: ModelType, response_model: Any) -> Dict[str, Any]:
|
139
|
+
"""응답 모델에 맞게 데이터를 처리합니다.
|
140
|
+
|
141
|
+
Args:
|
142
|
+
entity (ModelType): 처리할 엔티티
|
143
|
+
response_model (Any): 응답 모델
|
144
|
+
|
145
|
+
Returns:
|
146
|
+
Dict[str, Any]: 처리된 데이터
|
147
|
+
"""
|
148
|
+
if not response_model:
|
149
|
+
return self._process_response(entity)
|
150
|
+
|
151
|
+
result = self._process_response(entity)
|
152
|
+
|
153
|
+
# response_model에 없는 필드 제거
|
154
|
+
keys_to_remove = [key for key in result if key not in response_model.model_fields]
|
155
|
+
for key in keys_to_remove:
|
156
|
+
result.pop(key)
|
157
|
+
|
158
|
+
# 모델 검증
|
159
|
+
return response_model(**result).model_dump()
|
160
|
+
|
161
|
+
def _handle_exclude_fields(self, data: Dict[str, Any], exclude_fields: List[str]) -> Dict[str, Any]:
|
162
|
+
"""제외할 필드를 처리합니다.
|
163
|
+
|
164
|
+
Args:
|
165
|
+
data (Dict[str, Any]): 처리할 데이터
|
166
|
+
exclude_fields (List[str]): 제외할 필드 목록
|
167
|
+
|
168
|
+
Returns:
|
169
|
+
Dict[str, Any]: 처리된 데이터
|
170
|
+
"""
|
171
|
+
if not exclude_fields:
|
172
|
+
return data
|
173
|
+
|
174
|
+
return {k: v for k, v in data.items() if k not in exclude_fields}
|
175
|
+
|
176
|
+
def _validate_ulid(self, ulid: str) -> bool:
|
177
|
+
"""ULID 형식을 검증합니다.
|
178
|
+
|
179
|
+
Args:
|
180
|
+
ulid (str): 검증할 ULID
|
181
|
+
|
182
|
+
Returns:
|
183
|
+
bool: 유효한 ULID 여부
|
184
|
+
"""
|
185
|
+
try:
|
186
|
+
ULID.from_str(ulid)
|
187
|
+
return True
|
188
|
+
except (ValueError, AttributeError):
|
189
|
+
return False
|
190
|
+
|
191
|
+
def _process_columns(self, entity: ModelType, exclude_extra_data: bool = True) -> Dict[str, Any]:
|
192
|
+
"""엔티티의 컬럼들을 처리합니다.
|
193
|
+
|
194
|
+
Args:
|
195
|
+
entity (ModelType): 처리할 엔티티
|
196
|
+
exclude_extra_data (bool, optional): extra_data 컬럼 제외 여부. Defaults to True.
|
197
|
+
|
198
|
+
Returns:
|
199
|
+
Dict[str, Any]: 처리된 컬럼 데이터
|
200
|
+
"""
|
201
|
+
result = {}
|
202
|
+
for column in entity.__table__.columns:
|
203
|
+
if exclude_extra_data and column.name == 'extra_data':
|
204
|
+
continue
|
205
|
+
|
206
|
+
# 필드 값 처리
|
207
|
+
if hasattr(entity, column.name):
|
208
|
+
value = getattr(entity, column.name)
|
209
|
+
if isinstance(value, datetime):
|
210
|
+
value = value.isoformat()
|
211
|
+
result[column.name] = value
|
212
|
+
elif hasattr(entity, 'extra_data') and isinstance(entity.extra_data, dict):
|
213
|
+
result[column.name] = entity.extra_data.get(column.name)
|
214
|
+
else:
|
215
|
+
result[column.name] = None
|
216
|
+
|
217
|
+
# extra_data의 내용을 최상위 레벨로 업데이트
|
218
|
+
if hasattr(entity, 'extra_data') and isinstance(entity.extra_data, dict):
|
219
|
+
result.update(entity.extra_data or {})
|
220
|
+
|
221
|
+
return result
|
222
|
+
|
223
|
+
def _process_response(self, entity: ModelType, response_model: Any = None) -> Dict[str, Any]:
|
224
|
+
"""응답 데이터를 처리합니다.
|
225
|
+
extra_data의 내용을 최상위 레벨로 변환하고, 라우터에서 선언한 응답 스키마에 맞게 데이터를 변환합니다.
|
226
|
+
|
227
|
+
Args:
|
228
|
+
entity (ModelType): 처리할 엔티티
|
229
|
+
response_model (Any, optional): 응답 스키마. Defaults to None.
|
230
|
+
|
231
|
+
Returns:
|
232
|
+
Dict[str, Any]: 처리된 엔티티 데이터
|
233
|
+
"""
|
234
|
+
if not entity:
|
235
|
+
return None
|
236
|
+
|
237
|
+
# 모든 필드 처리
|
238
|
+
result = self._process_columns(entity)
|
239
|
+
|
240
|
+
# Relationship 처리 (이미 로드된 관계만 처리)
|
241
|
+
for relationship in entity.__mapper__.relationships:
|
242
|
+
if not relationship.key in entity.__dict__:
|
243
|
+
continue
|
244
|
+
|
245
|
+
try:
|
246
|
+
value = getattr(entity, relationship.key)
|
247
|
+
# response_model이 있는 경우 해당 필드의 annotation type을 가져옴
|
248
|
+
nested_response_model = None
|
249
|
+
if response_model and relationship.key in response_model.model_fields:
|
250
|
+
field_info = response_model.model_fields[relationship.key]
|
251
|
+
nested_response_model = field_info.annotation
|
252
|
+
|
253
|
+
if value is not None:
|
254
|
+
if isinstance(value, list):
|
255
|
+
result[relationship.key] = [
|
256
|
+
self._process_response(item, nested_response_model)
|
257
|
+
for item in value
|
258
|
+
]
|
259
|
+
else:
|
260
|
+
result[relationship.key] = self._process_response(value, nested_response_model)
|
261
|
+
else:
|
262
|
+
result[relationship.key] = None
|
263
|
+
except Exception:
|
264
|
+
result[relationship.key] = None
|
265
|
+
|
266
|
+
# response_model이 있는 경우 필터링
|
267
|
+
if response_model:
|
268
|
+
# 현재 키 목록을 저장
|
269
|
+
current_keys = list(result.keys())
|
270
|
+
# response_model에 없는 키 제거
|
271
|
+
for key in current_keys:
|
272
|
+
if key not in response_model.model_fields:
|
273
|
+
result.pop(key)
|
274
|
+
# 모델 검증 및 업데이트
|
275
|
+
result.update(response_model(**result).model_dump())
|
276
|
+
|
277
|
+
return result
|
278
|
+
|
279
|
+
def _process_basic_fields(self, entity: ModelType) -> Dict[str, Any]:
|
280
|
+
"""엔티티의 기본 필드만 처리합니다.
|
281
|
+
|
282
|
+
Args:
|
283
|
+
entity (ModelType): 처리할 엔티티
|
284
|
+
|
285
|
+
Returns:
|
286
|
+
Dict[str, Any]: 기본 필드만 포함된 딕셔너리
|
287
|
+
"""
|
288
|
+
if not entity:
|
289
|
+
return None
|
290
|
+
|
291
|
+
return self._process_columns(entity)
|
292
|
+
|
293
|
+
async def _create_for_model(self, model_name: str, data: Dict[str, Any], exclude_fields: List[str] = None) -> DeclarativeBase:
|
294
|
+
"""지정된 모델에 대해 새로운 엔티티를 생성합니다.
|
295
|
+
|
296
|
+
Args:
|
297
|
+
model_name (str): 생성할 모델 이름
|
298
|
+
data (Dict[str, Any]): 생성할 엔티티 데이터
|
299
|
+
exclude_fields (List[str], optional): 제외할 필드 목록. Defaults to None.
|
300
|
+
|
301
|
+
Returns:
|
302
|
+
DeclarativeBase: 생성된 엔티티
|
303
|
+
|
304
|
+
Raises:
|
305
|
+
CustomException: 데이터베이스 작업 중 오류 발생 시
|
306
|
+
"""
|
307
|
+
if model_name not in self.additional_models:
|
308
|
+
raise CustomException(
|
309
|
+
ErrorCode.INVALID_REQUEST,
|
310
|
+
detail=f"Model {model_name} not registered",
|
311
|
+
source_function=f"{self.__class__.__name__}._create_for_model"
|
312
|
+
)
|
313
|
+
|
314
|
+
try:
|
315
|
+
# 제외할 필드 처리
|
316
|
+
if exclude_fields:
|
317
|
+
data = {k: v for k, v in data.items() if k not in exclude_fields}
|
318
|
+
|
319
|
+
return await self.db_service.create_entity(self.additional_models[model_name], data)
|
320
|
+
except CustomException as e:
|
321
|
+
raise e
|
322
|
+
except Exception as e:
|
323
|
+
raise CustomException(
|
324
|
+
ErrorCode.DB_CREATE_ERROR,
|
325
|
+
detail=str(e),
|
326
|
+
source_function=f"{self.__class__.__name__}._create_for_model",
|
327
|
+
original_error=e
|
328
|
+
)
|
329
|
+
|
330
|
+
def _process_password(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
331
|
+
"""비밀번호 필드가 있는 경우 해시화합니다.
|
332
|
+
|
333
|
+
Args:
|
334
|
+
data (Dict[str, Any]): 처리할 데이터
|
335
|
+
|
336
|
+
Returns:
|
337
|
+
Dict[str, Any]: 처리된 데이터
|
338
|
+
"""
|
339
|
+
if "password" in data:
|
340
|
+
data["password"] = hash_password(data["password"])
|
341
|
+
return data
|
342
|
+
|
343
|
+
#######################
|
344
|
+
# 6. CRUD 작업 메서드 #
|
345
|
+
#######################
|
346
|
+
async def create(self, data: Dict[str, Any], exclude_fields: List[str] = None, model_name: str = None) -> Union[ModelType, DeclarativeBase]:
|
347
|
+
"""새로운 엔티티를 생성합니다.
|
348
|
+
|
349
|
+
Args:
|
350
|
+
data (Dict[str, Any]): 생성할 엔티티 데이터
|
351
|
+
exclude_fields (List[str], optional): 제외할 필드 목록. Defaults to None.
|
352
|
+
model_name (str, optional): 생성할 모델 이름. Defaults to None.
|
353
|
+
|
354
|
+
Returns:
|
355
|
+
Union[ModelType, DeclarativeBase]: 생성된 엔티티
|
356
|
+
|
357
|
+
Raises:
|
358
|
+
CustomException: 데이터베이스 작업 중 오류 발생 시
|
359
|
+
"""
|
360
|
+
try:
|
361
|
+
# 비밀번호 해시화
|
362
|
+
data = self._process_password(data)
|
363
|
+
|
364
|
+
# 제외할 필드 처리
|
365
|
+
if exclude_fields:
|
366
|
+
data = {k: v for k, v in data.items() if k not in exclude_fields}
|
367
|
+
|
368
|
+
if model_name:
|
369
|
+
return await self._create_for_model(model_name, data)
|
370
|
+
|
371
|
+
return await self.repository.create(data)
|
372
|
+
except CustomException as e:
|
373
|
+
raise e
|
374
|
+
except Exception as e:
|
375
|
+
raise CustomException(
|
376
|
+
ErrorCode.DB_CREATE_ERROR,
|
377
|
+
detail=str(e),
|
378
|
+
source_function=f"{self.__class__.__name__}.create",
|
379
|
+
original_error=e
|
380
|
+
)
|
381
|
+
|
382
|
+
async def update(
|
383
|
+
self,
|
384
|
+
ulid: str,
|
385
|
+
data: Dict[str, Any],
|
386
|
+
exclude_fields: List[str] = None,
|
387
|
+
model_name: str = None
|
388
|
+
) -> Optional[ModelType]:
|
389
|
+
"""기존 엔티티를 수정합니다.
|
390
|
+
|
391
|
+
Args:
|
392
|
+
ulid (str): 수정할 엔티티의 ULID
|
393
|
+
data (Dict[str, Any]): 수정할 데이터
|
394
|
+
exclude_fields (List[str], optional): 제외할 필드 목록. Defaults to None.
|
395
|
+
model_name (str, optional): 수정할 모델 이름. Defaults to None.
|
396
|
+
|
397
|
+
Returns:
|
398
|
+
Optional[ModelType]: 수정된 엔티티, 없으면 None
|
399
|
+
|
400
|
+
Raises:
|
401
|
+
CustomException: 데이터베이스 작업 중 오류 발생 시
|
402
|
+
"""
|
403
|
+
try:
|
404
|
+
# 비밀번호 해시화
|
405
|
+
data = self._process_password(data)
|
406
|
+
|
407
|
+
# 제외할 필드 처리
|
408
|
+
if exclude_fields:
|
409
|
+
data = {k: v for k, v in data.items() if k not in exclude_fields}
|
410
|
+
|
411
|
+
async with self.db_service.transaction():
|
412
|
+
if model_name:
|
413
|
+
if model_name not in self.additional_models:
|
414
|
+
raise CustomException(
|
415
|
+
ErrorCode.INVALID_REQUEST,
|
416
|
+
detail=f"Model {model_name} not registered",
|
417
|
+
source_function=f"{self.__class__.__name__}.update"
|
418
|
+
)
|
419
|
+
entity = await self.db_service.update_entity(
|
420
|
+
self.additional_models[model_name],
|
421
|
+
{"ulid": ulid},
|
422
|
+
data
|
423
|
+
)
|
424
|
+
if not entity:
|
425
|
+
raise CustomException(
|
426
|
+
ErrorCode.NOT_FOUND,
|
427
|
+
detail=f"{self.additional_models[model_name].__tablename__}|ulid|{ulid}",
|
428
|
+
source_function=f"{self.__class__.__name__}.update"
|
429
|
+
)
|
430
|
+
return entity
|
431
|
+
|
432
|
+
entity = await self.repository.update(ulid, data)
|
433
|
+
if not entity:
|
434
|
+
raise CustomException(
|
435
|
+
ErrorCode.NOT_FOUND,
|
436
|
+
detail=f"{self.model.__tablename__}|ulid|{ulid}",
|
437
|
+
source_function=f"{self.__class__.__name__}.update"
|
438
|
+
)
|
439
|
+
return entity
|
440
|
+
except CustomException as e:
|
441
|
+
raise e
|
442
|
+
except Exception as e:
|
443
|
+
raise CustomException(
|
444
|
+
ErrorCode.DB_UPDATE_ERROR,
|
445
|
+
detail=str(e),
|
446
|
+
source_function=f"{self.__class__.__name__}.update",
|
447
|
+
original_error=e
|
448
|
+
)
|
449
|
+
|
450
|
+
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
|
+
"""
|
463
|
+
try:
|
464
|
+
if model_name:
|
465
|
+
if model_name not in self.additional_models:
|
466
|
+
raise CustomException(
|
467
|
+
ErrorCode.INVALID_REQUEST,
|
468
|
+
detail=f"Model {model_name} not registered",
|
469
|
+
source_function=f"{self.__class__.__name__}.delete"
|
470
|
+
)
|
471
|
+
entity = await self.db_service.soft_delete_entity(self.additional_models[model_name], ulid)
|
472
|
+
if not entity:
|
473
|
+
raise CustomException(
|
474
|
+
ErrorCode.NOT_FOUND,
|
475
|
+
detail=f"{self.additional_models[model_name].__tablename__}|ulid|{ulid}",
|
476
|
+
source_function=f"{self.__class__.__name__}.delete"
|
477
|
+
)
|
478
|
+
return True
|
479
|
+
|
480
|
+
entity = await self.repository.delete(ulid)
|
481
|
+
if not entity:
|
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
|
488
|
+
except CustomException as e:
|
489
|
+
raise e
|
490
|
+
except Exception as e:
|
491
|
+
raise CustomException(
|
492
|
+
ErrorCode.DB_DELETE_ERROR,
|
493
|
+
detail=str(e),
|
494
|
+
source_function=f"{self.__class__.__name__}.delete",
|
495
|
+
original_error=e
|
496
|
+
)
|
497
|
+
|
498
|
+
async def real_row_delete(self, ulid: str, model_name: str = None) -> bool:
|
499
|
+
"""엔티티를 실제로 삭제합니다.
|
500
|
+
|
501
|
+
Args:
|
502
|
+
ulid (str): 삭제할 엔티티의 ULID
|
503
|
+
model_name (str, optional): 삭제할 모델 이름. Defaults to None.
|
504
|
+
|
505
|
+
Returns:
|
506
|
+
bool: 삭제 성공 여부
|
507
|
+
|
508
|
+
Raises:
|
509
|
+
CustomException: 데이터베이스 작업 중 오류 발생 시
|
510
|
+
"""
|
511
|
+
try:
|
512
|
+
if model_name:
|
513
|
+
if model_name not in self.additional_models:
|
514
|
+
raise CustomException(
|
515
|
+
ErrorCode.INVALID_REQUEST,
|
516
|
+
detail=f"Model {model_name} not registered",
|
517
|
+
source_function=f"{self.__class__.__name__}.real_row_delete"
|
518
|
+
)
|
519
|
+
entity = await self.db_service.retrieve_entity(
|
520
|
+
self.additional_models[model_name],
|
521
|
+
{"ulid": ulid}
|
522
|
+
)
|
523
|
+
if entity:
|
524
|
+
await self.db_service.delete_entity(entity)
|
525
|
+
return True
|
526
|
+
return False
|
527
|
+
|
528
|
+
return await self.repository.real_row_delete(ulid)
|
529
|
+
except CustomException as e:
|
530
|
+
raise e
|
531
|
+
except Exception as e:
|
532
|
+
raise CustomException(
|
533
|
+
ErrorCode.DB_DELETE_ERROR,
|
534
|
+
detail=str(e),
|
535
|
+
source_function=f"{self.__class__.__name__}.real_row_delete",
|
536
|
+
original_error=e
|
537
|
+
)
|
538
|
+
|
539
|
+
#########################
|
540
|
+
# 7. 조회 및 검색 메서드 #
|
541
|
+
#########################
|
542
|
+
async def list(
|
543
|
+
self,
|
544
|
+
skip: int = 0,
|
545
|
+
limit: int = 100,
|
546
|
+
filters: Dict[str, Any] | None = None,
|
547
|
+
search_params: Dict[str, Any] | None = None,
|
548
|
+
model_name: str | None = None,
|
549
|
+
request: Request | None = None,
|
550
|
+
response_model: Any = None
|
551
|
+
) -> 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
|
+
"""
|
566
|
+
try:
|
567
|
+
if model_name:
|
568
|
+
if model_name not in self.additional_models:
|
569
|
+
raise CustomException(
|
570
|
+
ErrorCode.INVALID_REQUEST,
|
571
|
+
detail=f"Model {model_name} not registered",
|
572
|
+
source_function=f"{self.__class__.__name__}.list"
|
573
|
+
)
|
574
|
+
entities = await self.db_service.list_entities(
|
575
|
+
self.additional_models[model_name],
|
576
|
+
skip=skip,
|
577
|
+
limit=limit,
|
578
|
+
filters=filters
|
579
|
+
)
|
580
|
+
return [self._process_response(entity, response_model) for entity in entities]
|
581
|
+
|
582
|
+
entities = await self.repository.list(
|
583
|
+
skip=skip,
|
584
|
+
limit=limit,
|
585
|
+
filters=filters,
|
586
|
+
search_params=search_params
|
587
|
+
)
|
588
|
+
return [self._process_response(entity, response_model) for entity in entities]
|
589
|
+
|
590
|
+
except CustomException as e:
|
591
|
+
e.detail = f"Service list error for {self.repository.model.__tablename__}: {e.detail}"
|
592
|
+
e.source_function = f"{self.__class__.__name__}.list -> {e.source_function}"
|
593
|
+
raise e
|
594
|
+
except Exception as e:
|
595
|
+
raise CustomException(
|
596
|
+
ErrorCode.INTERNAL_ERROR,
|
597
|
+
detail=str(e),
|
598
|
+
source_function=f"{self.__class__.__name__}.list",
|
599
|
+
original_error=e
|
600
|
+
)
|
601
|
+
|
602
|
+
async def get(
|
603
|
+
self,
|
604
|
+
ulid: str,
|
605
|
+
model_name: str | None = None,
|
606
|
+
request: Request | None = None,
|
607
|
+
response_model: Any = None
|
608
|
+
) -> Optional[Dict[str, Any]]:
|
609
|
+
"""특정 엔티티를 조회합니다.
|
610
|
+
|
611
|
+
Args:
|
612
|
+
ulid (str): 조회할 엔티티의 ULID
|
613
|
+
model_name (str | None, optional): 조회할 모델 이름. Defaults to None.
|
614
|
+
request (Request | None, optional): 요청 객체. Defaults to None.
|
615
|
+
response_model (Any, optional): 응답 스키마. Defaults to None.
|
616
|
+
|
617
|
+
Returns:
|
618
|
+
Optional[Dict[str, Any]]: 조회된 엔티티, 없으면 None
|
619
|
+
|
620
|
+
Raises:
|
621
|
+
CustomException: 데이터베이스 작업 중 오류 발생 시
|
622
|
+
"""
|
623
|
+
try:
|
624
|
+
# ULID 검증
|
625
|
+
if not self._validate_ulid(ulid):
|
626
|
+
raise CustomException(
|
627
|
+
ErrorCode.VALIDATION_ERROR,
|
628
|
+
detail=f"Invalid ULID format: {ulid}",
|
629
|
+
source_function=f"{self.__class__.__name__}.get"
|
630
|
+
)
|
631
|
+
|
632
|
+
if model_name:
|
633
|
+
if model_name not in self.additional_models:
|
634
|
+
raise CustomException(
|
635
|
+
ErrorCode.INVALID_REQUEST,
|
636
|
+
detail=f"Model {model_name} not registered",
|
637
|
+
source_function=f"{self.__class__.__name__}.get"
|
638
|
+
)
|
639
|
+
entity = await self.db_service.retrieve_entity(
|
640
|
+
self.additional_models[model_name],
|
641
|
+
{"ulid": ulid, "is_deleted": False}
|
642
|
+
)
|
643
|
+
if not entity:
|
644
|
+
raise CustomException(
|
645
|
+
ErrorCode.NOT_FOUND,
|
646
|
+
detail=f"{self.additional_models[model_name].__tablename__}|ulid|{ulid}",
|
647
|
+
source_function=f"{self.__class__.__name__}.get"
|
648
|
+
)
|
649
|
+
return self._process_response(entity, response_model)
|
650
|
+
|
651
|
+
entity = await self.repository.get(ulid)
|
652
|
+
if not entity:
|
653
|
+
raise CustomException(
|
654
|
+
ErrorCode.NOT_FOUND,
|
655
|
+
detail=f"{self.model.__tablename__}|ulid|{ulid}",
|
656
|
+
source_function=f"{self.__class__.__name__}.get"
|
657
|
+
)
|
658
|
+
|
659
|
+
return self._process_response(entity, response_model)
|
660
|
+
except CustomException as e:
|
661
|
+
raise e
|
662
|
+
except Exception as e:
|
663
|
+
raise CustomException(
|
664
|
+
ErrorCode.INTERNAL_ERROR,
|
665
|
+
detail=str(e),
|
666
|
+
source_function=f"{self.__class__.__name__}.get",
|
667
|
+
original_error=e
|
668
|
+
)
|