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
aiteamutils/__init__.py
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
from .base_model import Base
|
2
|
+
from .database import DatabaseManager
|
3
|
+
from .exceptions import (
|
4
|
+
CustomException,
|
5
|
+
ErrorCode,
|
6
|
+
custom_exception_handler,
|
7
|
+
request_validation_exception_handler,
|
8
|
+
sqlalchemy_exception_handler,
|
9
|
+
generic_exception_handler
|
10
|
+
)
|
11
|
+
from .security import (
|
12
|
+
verify_password,
|
13
|
+
hash_password,
|
14
|
+
create_jwt_token,
|
15
|
+
verify_jwt_token,
|
16
|
+
rate_limit,
|
17
|
+
RateLimitExceeded
|
18
|
+
)
|
19
|
+
from .base_service import BaseService
|
20
|
+
from .base_repository import BaseRepository
|
21
|
+
from .validators import validate_with
|
22
|
+
from .enums import ActivityType
|
23
|
+
from .cache import CacheManager
|
24
|
+
|
25
|
+
__version__ = "0.1.0"
|
26
|
+
|
27
|
+
__all__ = [
|
28
|
+
# Base Models
|
29
|
+
"Base",
|
30
|
+
"BaseService",
|
31
|
+
"BaseRepository",
|
32
|
+
|
33
|
+
# Database
|
34
|
+
"DatabaseManager",
|
35
|
+
|
36
|
+
# Exceptions
|
37
|
+
"CustomException",
|
38
|
+
"ErrorCode",
|
39
|
+
"custom_exception_handler",
|
40
|
+
"request_validation_exception_handler",
|
41
|
+
"sqlalchemy_exception_handler",
|
42
|
+
"generic_exception_handler",
|
43
|
+
|
44
|
+
# Security
|
45
|
+
"verify_password",
|
46
|
+
"hash_password",
|
47
|
+
"create_jwt_token",
|
48
|
+
"verify_jwt_token",
|
49
|
+
"rate_limit",
|
50
|
+
"RateLimitExceeded",
|
51
|
+
|
52
|
+
# Validators
|
53
|
+
"validate_with",
|
54
|
+
|
55
|
+
# Enums
|
56
|
+
"ActivityType",
|
57
|
+
|
58
|
+
# Cache
|
59
|
+
"CacheManager"
|
60
|
+
]
|
@@ -0,0 +1,81 @@
|
|
1
|
+
from datetime import datetime, timezone
|
2
|
+
from typing import Any, Dict, TypeVar, Generic, Optional
|
3
|
+
from ulid import ULID
|
4
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
5
|
+
from sqlalchemy import Column, String, PrimaryKeyConstraint, UniqueConstraint
|
6
|
+
from sqlalchemy.dialects.postgresql import TIMESTAMP
|
7
|
+
from pydantic import BaseModel, ConfigDict
|
8
|
+
from pydantic import Field
|
9
|
+
|
10
|
+
class Base(DeclarativeBase):
|
11
|
+
"""SQLAlchemy 기본 모델"""
|
12
|
+
pass
|
13
|
+
|
14
|
+
class BaseColumn(Base):
|
15
|
+
"""공통 설정 및 메서드를 제공하는 BaseColumn"""
|
16
|
+
__abstract__ = True
|
17
|
+
|
18
|
+
ulid: Mapped[str] = mapped_column(
|
19
|
+
String,
|
20
|
+
primary_key=True,
|
21
|
+
unique=True,
|
22
|
+
default=lambda: str(ULID()),
|
23
|
+
doc="ULID",
|
24
|
+
nullable=False
|
25
|
+
)
|
26
|
+
created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
|
27
|
+
updated_at: Mapped[datetime] = mapped_column(
|
28
|
+
default=datetime.utcnow,
|
29
|
+
onupdate=datetime.utcnow
|
30
|
+
)
|
31
|
+
is_deleted: Mapped[bool] = mapped_column(
|
32
|
+
default=False,
|
33
|
+
index=True
|
34
|
+
)
|
35
|
+
|
36
|
+
def to_dict(self) -> Dict[str, Any]:
|
37
|
+
"""모델을 딕셔너리로 변환합니다.
|
38
|
+
|
39
|
+
Returns:
|
40
|
+
Dict[str, Any]: 모델의 속성을 포함하는 딕셔너리
|
41
|
+
"""
|
42
|
+
result = {}
|
43
|
+
|
44
|
+
# 테이블 컬럼 처리
|
45
|
+
for column in self.__table__.columns:
|
46
|
+
value = getattr(self, column.name)
|
47
|
+
if isinstance(value, datetime):
|
48
|
+
value = value.isoformat()
|
49
|
+
result[column.name] = value
|
50
|
+
|
51
|
+
# Relationship 처리 (이미 로드된 관계만 처리)
|
52
|
+
for relationship in self.__mapper__.relationships:
|
53
|
+
if relationship.key == "organizations": # 순환 참조 방지
|
54
|
+
continue
|
55
|
+
try:
|
56
|
+
value = getattr(self, relationship.key)
|
57
|
+
if value is not None:
|
58
|
+
if isinstance(value, list):
|
59
|
+
result[relationship.key] = [item.to_dict() for item in value]
|
60
|
+
else:
|
61
|
+
result[relationship.key] = value.to_dict()
|
62
|
+
else:
|
63
|
+
result[relationship.key] = None
|
64
|
+
except Exception:
|
65
|
+
result[relationship.key] = None
|
66
|
+
|
67
|
+
return result
|
68
|
+
|
69
|
+
class BaseSchema(BaseModel):
|
70
|
+
"""공통 설정 및 메서드를 제공하는 BaseSchema"""
|
71
|
+
model_config = ConfigDict(
|
72
|
+
str_strip_whitespace=True,
|
73
|
+
extra="allow",
|
74
|
+
from_attributes=True,
|
75
|
+
populate_by_name=True,
|
76
|
+
use_enum_values=True
|
77
|
+
)
|
78
|
+
|
79
|
+
def to_dict(self) -> Dict[str, Any]:
|
80
|
+
"""모델을 딕셔너리로 변환"""
|
81
|
+
return self.model_dump()
|
@@ -0,0 +1,503 @@
|
|
1
|
+
"""기본 레포지토리 모듈."""
|
2
|
+
from typing import TypeVar, Generic, Dict, Any, List, Optional, Type
|
3
|
+
from sqlalchemy.orm import DeclarativeBase
|
4
|
+
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
|
5
|
+
from sqlalchemy import select, or_, and_
|
6
|
+
from .database import DatabaseService
|
7
|
+
from .exceptions import CustomException, ErrorCode
|
8
|
+
from sqlalchemy.orm import joinedload
|
9
|
+
from sqlalchemy.sql import Select
|
10
|
+
|
11
|
+
ModelType = TypeVar("ModelType", bound=DeclarativeBase)
|
12
|
+
|
13
|
+
class BaseRepository(Generic[ModelType]):
|
14
|
+
##################
|
15
|
+
# 1. 초기화 영역 #
|
16
|
+
##################
|
17
|
+
def __init__(self, db_service: DatabaseService, model: Type[ModelType]):
|
18
|
+
"""
|
19
|
+
Args:
|
20
|
+
db_service (DatabaseService): 데이터베이스 서비스 인스턴스
|
21
|
+
model (Type[ModelType]): 모델 클래스
|
22
|
+
"""
|
23
|
+
self.db_service = db_service
|
24
|
+
self.model = model
|
25
|
+
|
26
|
+
#######################
|
27
|
+
# 2. 쿼리 빌딩 #
|
28
|
+
#######################
|
29
|
+
def _build_base_query(self) -> Select:
|
30
|
+
"""기본 쿼리를 생성합니다.
|
31
|
+
|
32
|
+
Returns:
|
33
|
+
Select: 기본 쿼리
|
34
|
+
"""
|
35
|
+
return select(self.model)
|
36
|
+
|
37
|
+
#######################
|
38
|
+
# 3. 전차리 영역 #
|
39
|
+
#######################
|
40
|
+
def _apply_exact_match(self, stmt: Select, field_name: str, value: Any) -> Select:
|
41
|
+
"""정확한 값 매칭 조건을 적용합니다.
|
42
|
+
|
43
|
+
Args:
|
44
|
+
stmt (Select): 쿼리문
|
45
|
+
field_name (str): 필드명
|
46
|
+
value (Any): 매칭할 값
|
47
|
+
|
48
|
+
Returns:
|
49
|
+
Select: 조건이 적용된 쿼리
|
50
|
+
"""
|
51
|
+
return stmt.where(getattr(self.model, field_name) == value)
|
52
|
+
|
53
|
+
def _apply_like_match(self, stmt: Select, field_name: str, value: str) -> Select:
|
54
|
+
"""LIKE 검색 조건을 적용합니다.
|
55
|
+
|
56
|
+
Args:
|
57
|
+
stmt (Select): 쿼리문
|
58
|
+
field_name (str): 필드명
|
59
|
+
value (str): 검색할 값
|
60
|
+
|
61
|
+
Returns:
|
62
|
+
Select: 조건이 적용된 쿼리
|
63
|
+
"""
|
64
|
+
return stmt.where(getattr(self.model, field_name).ilike(f"%{value}%"))
|
65
|
+
|
66
|
+
def _apply_relation_match(self, stmt: Select, relations: List[str], field_name: str, operator: str, value: Any) -> Select:
|
67
|
+
"""관계 테이블 검색 조건을 적용합니다."""
|
68
|
+
current = self.model
|
69
|
+
|
70
|
+
# 관계 체인 따라가기
|
71
|
+
for i in range(len(relations)-1):
|
72
|
+
current = getattr(current, relations[i]).property.mapper.class_
|
73
|
+
|
74
|
+
# 마지막 모델과 필드
|
75
|
+
final_model = getattr(current, relations[-1]).property.mapper.class_
|
76
|
+
|
77
|
+
# 중첩된 EXISTS 절 생성
|
78
|
+
current = self.model
|
79
|
+
subq = select(1)
|
80
|
+
|
81
|
+
# 첫 번째 관계
|
82
|
+
next_model = getattr(current, relations[0]).property.mapper.class_
|
83
|
+
subq = subq.where(getattr(next_model, 'ulid') == getattr(current, f"{relations[0]}_ulid"))
|
84
|
+
|
85
|
+
# 중간 관계들
|
86
|
+
for i in range(1, len(relations)):
|
87
|
+
prev_model = next_model
|
88
|
+
next_model = getattr(prev_model, relations[i]).property.mapper.class_
|
89
|
+
subq = subq.where(getattr(next_model, 'ulid') == getattr(prev_model, f"{relations[i]}_ulid"))
|
90
|
+
|
91
|
+
# 최종 검색 조건
|
92
|
+
subq = subq.where(getattr(final_model, field_name).__getattribute__(operator)(value))
|
93
|
+
|
94
|
+
return stmt.where(subq.exists())
|
95
|
+
|
96
|
+
def _apply_ordering(self, stmt: Select, order_by: List[str]) -> Select:
|
97
|
+
"""정렬 조건을 적용합니다.
|
98
|
+
|
99
|
+
Args:
|
100
|
+
stmt (Select): 쿼리문
|
101
|
+
order_by (List[str]): 정렬 기준 필드 목록 (예: ["name", "-created_at"])
|
102
|
+
|
103
|
+
Returns:
|
104
|
+
Select: 정렬이 적용된 쿼리
|
105
|
+
"""
|
106
|
+
for field in order_by:
|
107
|
+
if field.startswith("-"):
|
108
|
+
field_name = field[1:]
|
109
|
+
stmt = stmt.order_by(getattr(self.model, field_name).desc())
|
110
|
+
else:
|
111
|
+
stmt = stmt.order_by(getattr(self.model, field).asc())
|
112
|
+
return stmt
|
113
|
+
|
114
|
+
def _apply_pagination(self, stmt: Select, skip: int = 0, limit: int = 100) -> Select:
|
115
|
+
"""페이징을 적용합니다.
|
116
|
+
|
117
|
+
Args:
|
118
|
+
stmt (Select): 쿼리문
|
119
|
+
skip (int): 건너뛸 레코드 수
|
120
|
+
limit (int): 조회할 최대 레코드 수
|
121
|
+
|
122
|
+
Returns:
|
123
|
+
Select: 페이징이 적용된 쿼리
|
124
|
+
"""
|
125
|
+
return stmt.offset(skip).limit(limit)
|
126
|
+
|
127
|
+
def _apply_joins(self, stmt: Select, joins: List[str]) -> Select:
|
128
|
+
"""조인을 적용합니다.
|
129
|
+
|
130
|
+
Args:
|
131
|
+
stmt (Select): 쿼리문
|
132
|
+
joins (List[str]): 조인할 관계명 목록
|
133
|
+
|
134
|
+
Returns:
|
135
|
+
Select: 조인이 적용된 쿼리
|
136
|
+
"""
|
137
|
+
for join in joins:
|
138
|
+
stmt = stmt.options(joinedload(getattr(self.model, join)))
|
139
|
+
return stmt
|
140
|
+
|
141
|
+
def _build_jsonb_condition(self, model: Any, field_path: str, value: str) -> Any:
|
142
|
+
"""JSONB 필드에 대한 검색 조건을 생성합니다.
|
143
|
+
|
144
|
+
Args:
|
145
|
+
model: 대상 모델
|
146
|
+
field_path (str): JSONB 키 경로 (예: "address", "name.first")
|
147
|
+
value (str): 검색할 값
|
148
|
+
|
149
|
+
Returns:
|
150
|
+
Any: SQLAlchemy 검색 조건
|
151
|
+
"""
|
152
|
+
# JSONB 경로가 중첩된 경우 (예: "name.first")
|
153
|
+
if "." in field_path:
|
154
|
+
path_parts = field_path.split(".")
|
155
|
+
jsonb_path = "{" + ",".join(path_parts) + "}"
|
156
|
+
return model.extra_data[jsonb_path].astext.ilike(f"%{value}%")
|
157
|
+
# 단일 키인 경우
|
158
|
+
return model.extra_data[field_path].astext.ilike(f"%{value}%")
|
159
|
+
|
160
|
+
def _apply_jsonb_match(self, stmt: Select, relations: List[str], json_key: str, value: str) -> Select:
|
161
|
+
"""JSONB 필드 검색 조건을 적용합니다.
|
162
|
+
|
163
|
+
Args:
|
164
|
+
stmt (Select): 쿼리문
|
165
|
+
relations (List[str]): 관계 테이블 경로
|
166
|
+
json_key (str): JSONB 키 경로
|
167
|
+
value (str): 검색할 값
|
168
|
+
|
169
|
+
Returns:
|
170
|
+
Select: 조건이 적용된 쿼리
|
171
|
+
"""
|
172
|
+
current = self.model
|
173
|
+
|
174
|
+
# 단일 모델 검색
|
175
|
+
if not relations:
|
176
|
+
condition = self._build_jsonb_condition(current, json_key, value)
|
177
|
+
return stmt.where(condition)
|
178
|
+
|
179
|
+
# 관계 모델 검색
|
180
|
+
for i in range(len(relations)-1):
|
181
|
+
current = getattr(current, relations[i]).property.mapper.class_
|
182
|
+
|
183
|
+
final_model = getattr(current, relations[-1]).property.mapper.class_
|
184
|
+
|
185
|
+
# 관계 체인 구성
|
186
|
+
if len(relations) == 1:
|
187
|
+
condition = getattr(self.model, relations[0]).has(
|
188
|
+
self._build_jsonb_condition(final_model, json_key, value)
|
189
|
+
)
|
190
|
+
else:
|
191
|
+
condition = getattr(self.model, relations[0]).has(
|
192
|
+
getattr(final_model, relations[-1]).has(
|
193
|
+
self._build_jsonb_condition(final_model, json_key, value)
|
194
|
+
)
|
195
|
+
)
|
196
|
+
|
197
|
+
return stmt.where(condition)
|
198
|
+
|
199
|
+
def _apply_search_params(self, stmt, search_params: Dict[str, Any]):
|
200
|
+
"""검색 파라미터를 적용합니다."""
|
201
|
+
if not search_params:
|
202
|
+
return stmt
|
203
|
+
|
204
|
+
for key, value in search_params.items():
|
205
|
+
if not value.get("value"):
|
206
|
+
continue
|
207
|
+
|
208
|
+
conditions = []
|
209
|
+
for field in value.get("fields", []):
|
210
|
+
parts = field.split('.')
|
211
|
+
|
212
|
+
if len(parts) == 1:
|
213
|
+
# 직접 필드 검색
|
214
|
+
condition = self._apply_like_match(stmt, parts[0], value["value"]).whereclause
|
215
|
+
elif 'extra_data' in parts:
|
216
|
+
# JSONB 필드 검색
|
217
|
+
extra_data_idx = parts.index('extra_data')
|
218
|
+
tables = parts[:extra_data_idx]
|
219
|
+
json_key = ".".join(parts[extra_data_idx + 1:])
|
220
|
+
condition = self._apply_jsonb_match(
|
221
|
+
stmt,
|
222
|
+
tables,
|
223
|
+
json_key,
|
224
|
+
value["value"]
|
225
|
+
).whereclause
|
226
|
+
else:
|
227
|
+
# 관계 테이블 검색
|
228
|
+
condition = self._apply_relation_match(
|
229
|
+
stmt,
|
230
|
+
parts[:-1],
|
231
|
+
parts[-1],
|
232
|
+
"ilike",
|
233
|
+
f"%{value['value']}%"
|
234
|
+
).whereclause
|
235
|
+
|
236
|
+
conditions.append(condition)
|
237
|
+
|
238
|
+
if conditions:
|
239
|
+
stmt = stmt.where(or_(*conditions))
|
240
|
+
|
241
|
+
return stmt
|
242
|
+
|
243
|
+
def _apply_filters(self, stmt, filters: Dict[str, Any]):
|
244
|
+
"""일반 필터를 적용합니다."""
|
245
|
+
for key, value in filters.items():
|
246
|
+
if value is None:
|
247
|
+
continue
|
248
|
+
|
249
|
+
if "." in key:
|
250
|
+
# 관계 테이블 필터
|
251
|
+
relation, field = key.split(".")
|
252
|
+
stmt = self._apply_relation_match(stmt, relation, field, "__eq__", value)
|
253
|
+
else:
|
254
|
+
# 일반 필드 필터
|
255
|
+
stmt = stmt.where(getattr(self.model, key) == value)
|
256
|
+
|
257
|
+
return stmt
|
258
|
+
|
259
|
+
#######################
|
260
|
+
# 4. CRUD 작업 #
|
261
|
+
#######################
|
262
|
+
async def get(
|
263
|
+
self,
|
264
|
+
ulid: str
|
265
|
+
) -> Optional[Dict[str, Any]]:
|
266
|
+
"""ULID로 엔티티를 조회합니다.
|
267
|
+
|
268
|
+
Args:
|
269
|
+
ulid (str): 조회할 엔티티의 ULID
|
270
|
+
|
271
|
+
Returns:
|
272
|
+
Optional[Dict[str, Any]]: 조회된 엔티티, 없으면 None
|
273
|
+
|
274
|
+
Raises:
|
275
|
+
CustomException: 데이터베이스 작업 중 오류 발생 시
|
276
|
+
"""
|
277
|
+
try:
|
278
|
+
stmt = select(self.model).filter_by(ulid=ulid, is_deleted=False)
|
279
|
+
result = await self.db_service.execute(stmt)
|
280
|
+
entity = result.scalars().unique().first()
|
281
|
+
|
282
|
+
if not entity:
|
283
|
+
raise CustomException(
|
284
|
+
ErrorCode.DB_NO_RESULT,
|
285
|
+
detail=f"{self.model.__tablename__}|ulid|{ulid}",
|
286
|
+
source_function=f"{self.__class__.__name__}.get"
|
287
|
+
)
|
288
|
+
return entity
|
289
|
+
except CustomException as e:
|
290
|
+
e.detail = f"Repository error for {self.model.__tablename__}: {e.detail}"
|
291
|
+
e.source_function = f"{self.__class__.__name__}.get -> {e.source_function}"
|
292
|
+
raise e
|
293
|
+
except SQLAlchemyError as e:
|
294
|
+
raise CustomException(
|
295
|
+
ErrorCode.DB_QUERY_ERROR,
|
296
|
+
detail=f"Database error in {self.model.__tablename__}: {str(e)}",
|
297
|
+
source_function=f"{self.__class__.__name__}.get",
|
298
|
+
original_error=e
|
299
|
+
)
|
300
|
+
except Exception as e:
|
301
|
+
raise CustomException(
|
302
|
+
ErrorCode.DB_QUERY_ERROR,
|
303
|
+
detail=f"Unexpected repository error in {self.model.__tablename__}: {str(e)}",
|
304
|
+
source_function=f"{self.__class__.__name__}.get",
|
305
|
+
original_error=e
|
306
|
+
)
|
307
|
+
|
308
|
+
async def list(
|
309
|
+
self,
|
310
|
+
skip: int = 0,
|
311
|
+
limit: int = 100,
|
312
|
+
filters: Dict[str, Any] | None = None,
|
313
|
+
search_params: Dict[str, Any] | None = None
|
314
|
+
) -> List[Any]:
|
315
|
+
"""엔티티 목록을 조회합니다."""
|
316
|
+
try:
|
317
|
+
stmt = select(self.model).where(self.model.is_deleted == False)
|
318
|
+
|
319
|
+
# 필터 적용
|
320
|
+
if filters:
|
321
|
+
stmt = self._apply_filters(stmt, filters)
|
322
|
+
|
323
|
+
# 검색 적용
|
324
|
+
if search_params:
|
325
|
+
stmt = self._apply_search_params(stmt, search_params)
|
326
|
+
|
327
|
+
# 페이지네이션 적용
|
328
|
+
stmt = stmt.limit(limit).offset(skip)
|
329
|
+
|
330
|
+
result = await self.db_service.db.execute(stmt)
|
331
|
+
return result.scalars().unique().all()
|
332
|
+
|
333
|
+
except SQLAlchemyError as e:
|
334
|
+
raise CustomException(
|
335
|
+
ErrorCode.DB_QUERY_ERROR,
|
336
|
+
detail=f"Unexpected repository list error in {self.model.__tablename__}: {str(e)}",
|
337
|
+
source_function=f"{self.__class__.__name__}.list",
|
338
|
+
original_error=e,
|
339
|
+
)
|
340
|
+
|
341
|
+
async def create(self, data: Dict[str, Any]) -> ModelType:
|
342
|
+
"""새로운 엔티티를 생성합니다.
|
343
|
+
|
344
|
+
Args:
|
345
|
+
data (Dict[str, Any]): 생성할 엔티티 데이터
|
346
|
+
|
347
|
+
Returns:
|
348
|
+
ModelType: 생성된 엔티티
|
349
|
+
|
350
|
+
Raises:
|
351
|
+
CustomException: 데이터베이스 작업 중 오류 발생 시
|
352
|
+
"""
|
353
|
+
try:
|
354
|
+
return await self.db_service.create_entity(self.model, data)
|
355
|
+
except CustomException as e:
|
356
|
+
e.detail = f"Repository create error for {self.model.__tablename__}: {e.detail}"
|
357
|
+
e.source_function = f"{self.__class__.__name__}.create -> {e.source_function}"
|
358
|
+
raise e
|
359
|
+
except IntegrityError as e:
|
360
|
+
self._handle_integrity_error(e, "create", data)
|
361
|
+
except SQLAlchemyError as e:
|
362
|
+
raise CustomException(
|
363
|
+
ErrorCode.DB_CREATE_ERROR,
|
364
|
+
detail=f"Database create error in {self.model.__tablename__}: {str(e)}",
|
365
|
+
source_function=f"{self.__class__.__name__}.create",
|
366
|
+
original_error=e
|
367
|
+
)
|
368
|
+
except Exception as e:
|
369
|
+
raise CustomException(
|
370
|
+
ErrorCode.DB_CREATE_ERROR,
|
371
|
+
detail=f"Unexpected repository create error in {self.model.__tablename__}: {str(e)}",
|
372
|
+
source_function=f"{self.__class__.__name__}.create",
|
373
|
+
original_error=e
|
374
|
+
)
|
375
|
+
|
376
|
+
async def update(self, ulid: str, data: Dict[str, Any]) -> Optional[ModelType]:
|
377
|
+
"""기존 엔티티를 수정합니다.
|
378
|
+
|
379
|
+
Args:
|
380
|
+
ulid (str): 수정할 엔티티의 ULID
|
381
|
+
data (Dict[str, Any]): 수정할 데이터
|
382
|
+
|
383
|
+
Returns:
|
384
|
+
Optional[ModelType]: 수정된 엔티티, 없으면 None
|
385
|
+
|
386
|
+
Raises:
|
387
|
+
CustomException: 데이터베이스 작업 중 오류 발생 시
|
388
|
+
"""
|
389
|
+
try:
|
390
|
+
entity = await self.db_service.update_entity(
|
391
|
+
self.model,
|
392
|
+
{"ulid": ulid, "is_deleted": False},
|
393
|
+
data
|
394
|
+
)
|
395
|
+
if not entity:
|
396
|
+
raise CustomException(
|
397
|
+
ErrorCode.DB_NO_RESULT,
|
398
|
+
detail=f"{self.model.__tablename__}|ulid|{ulid}",
|
399
|
+
source_function=f"{self.__class__.__name__}.update"
|
400
|
+
)
|
401
|
+
return entity
|
402
|
+
except CustomException as e:
|
403
|
+
e.detail = f"Repository update error for {self.model.__tablename__}: {e.detail}"
|
404
|
+
e.source_function = f"{self.__class__.__name__}.update -> {e.source_function}"
|
405
|
+
raise e
|
406
|
+
except IntegrityError as e:
|
407
|
+
self._handle_integrity_error(e, "update", data)
|
408
|
+
except SQLAlchemyError as e:
|
409
|
+
raise CustomException(
|
410
|
+
ErrorCode.DB_UPDATE_ERROR,
|
411
|
+
detail=f"Database update error in {self.model.__tablename__}: {str(e)}",
|
412
|
+
source_function=f"{self.__class__.__name__}.update",
|
413
|
+
original_error=e
|
414
|
+
)
|
415
|
+
except Exception as e:
|
416
|
+
raise CustomException(
|
417
|
+
ErrorCode.DB_UPDATE_ERROR,
|
418
|
+
detail=f"Unexpected repository update error in {self.model.__tablename__}: {str(e)}",
|
419
|
+
source_function=f"{self.__class__.__name__}.update",
|
420
|
+
original_error=e
|
421
|
+
)
|
422
|
+
|
423
|
+
async def delete(self, ulid: str) -> bool:
|
424
|
+
"""엔티티를 소프트 삭제합니다 (is_deleted = True).
|
425
|
+
|
426
|
+
Args:
|
427
|
+
ulid (str): 삭제할 엔티티의 ULID
|
428
|
+
|
429
|
+
Returns:
|
430
|
+
bool: 삭제 성공 여부
|
431
|
+
|
432
|
+
Raises:
|
433
|
+
CustomException: 데이터베이스 작업 중 오류 발생 시
|
434
|
+
"""
|
435
|
+
try:
|
436
|
+
entity = await self.db_service.soft_delete_entity(self.model, ulid)
|
437
|
+
if not entity:
|
438
|
+
raise CustomException(
|
439
|
+
ErrorCode.DB_NO_RESULT,
|
440
|
+
detail=f"{self.model.__tablename__}|ulid|{ulid}",
|
441
|
+
source_function=f"{self.__class__.__name__}.delete"
|
442
|
+
)
|
443
|
+
return True
|
444
|
+
except CustomException as e:
|
445
|
+
e.detail = f"Repository delete error for {self.model.__tablename__}: {e.detail}"
|
446
|
+
e.source_function = f"{self.__class__.__name__}.delete -> {e.source_function}"
|
447
|
+
raise e
|
448
|
+
except IntegrityError as e:
|
449
|
+
self._handle_integrity_error(e, "delete")
|
450
|
+
except SQLAlchemyError as e:
|
451
|
+
raise CustomException(
|
452
|
+
ErrorCode.DB_DELETE_ERROR,
|
453
|
+
detail=f"Database delete error in {self.model.__tablename__}: {str(e)}",
|
454
|
+
source_function=f"{self.__class__.__name__}.delete",
|
455
|
+
original_error=e
|
456
|
+
)
|
457
|
+
except Exception as e:
|
458
|
+
raise CustomException(
|
459
|
+
ErrorCode.DB_DELETE_ERROR,
|
460
|
+
detail=f"Unexpected repository delete error in {self.model.__tablename__}: {str(e)}",
|
461
|
+
source_function=f"{self.__class__.__name__}.delete",
|
462
|
+
original_error=e
|
463
|
+
)
|
464
|
+
|
465
|
+
async def real_row_delete(self, ulid: str) -> bool:
|
466
|
+
"""엔티티를 실제로 삭제합니다.
|
467
|
+
|
468
|
+
Args:
|
469
|
+
ulid (str): 삭제할 엔티티의 ULID
|
470
|
+
|
471
|
+
Returns:
|
472
|
+
bool: 삭제 성공 여부
|
473
|
+
|
474
|
+
Raises:
|
475
|
+
CustomException: 데이터베이스 작업 중 오류 발생 시
|
476
|
+
"""
|
477
|
+
try:
|
478
|
+
entity = await self.db_service.retrieve_entity(
|
479
|
+
self.model,
|
480
|
+
{"ulid": ulid}
|
481
|
+
)
|
482
|
+
if entity:
|
483
|
+
await self.db_service.delete_entity(entity)
|
484
|
+
return True
|
485
|
+
return False
|
486
|
+
except CustomException as e:
|
487
|
+
e.detail = f"Repository real delete error for {self.model.__tablename__}: {e.detail}"
|
488
|
+
e.source_function = f"{self.__class__.__name__}.real_row_delete -> {e.source_function}"
|
489
|
+
raise e
|
490
|
+
except SQLAlchemyError as e:
|
491
|
+
raise CustomException(
|
492
|
+
ErrorCode.DB_DELETE_ERROR,
|
493
|
+
detail=f"Database real delete error in {self.model.__tablename__}: {str(e)}",
|
494
|
+
source_function=f"{self.__class__.__name__}.real_row_delete",
|
495
|
+
original_error=e
|
496
|
+
)
|
497
|
+
except Exception as e:
|
498
|
+
raise CustomException(
|
499
|
+
ErrorCode.DB_DELETE_ERROR,
|
500
|
+
detail=f"Unexpected repository real delete error in {self.model.__tablename__}: {str(e)}",
|
501
|
+
source_function=f"{self.__class__.__name__}.real_row_delete",
|
502
|
+
original_error=e
|
503
|
+
)
|