fix(api): resolve SQL unpacking crash and Event serialization errors
- Refactor SQL helpers in lib_sql_search to return empty tuples instead of False - Add Pydantic pre-validators to Event_Base to coerce time objects to strings - Improves API stability for Event searches and filtered lists
This commit is contained in:
@@ -8,15 +8,15 @@ from sqlalchemy import text
|
|||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
def sql_limit_offset_part(limit: int, offset: int = 0) -> str|bool:
|
def sql_limit_offset_part(limit: int, offset: int = 0) -> str:
|
||||||
"""Creates a partial SQL string for LIMIT and OFFSET."""
|
"""Creates a partial SQL string for LIMIT and OFFSET."""
|
||||||
if limit >= 0 and offset >= 0:
|
if limit >= 0 and offset >= 0:
|
||||||
log.info(f'Creating partial SQL string for LIMIT and OFFSET. Limit: {limit}; Offset: {offset}')
|
log.info(f'Creating partial SQL string for LIMIT and OFFSET. Limit: {limit}; Offset: {offset}')
|
||||||
return f'LIMIT {limit} OFFSET {offset}'
|
return f'LIMIT {limit} OFFSET {offset}'
|
||||||
else:
|
else:
|
||||||
return False
|
return ''
|
||||||
|
|
||||||
def sql_and_like_part(and_like_dict_obj: dict) -> tuple[str, dict]|bool:
|
def sql_and_like_part(and_like_dict_obj: dict) -> tuple[str, dict]:
|
||||||
"""Creates a partial SQL string for AND LIKE queries."""
|
"""Creates a partial SQL string for AND LIKE queries."""
|
||||||
data = {}
|
data = {}
|
||||||
if and_like_dict_obj and isinstance(and_like_dict_obj, dict):
|
if and_like_dict_obj and isinstance(and_like_dict_obj, dict):
|
||||||
@@ -26,9 +26,9 @@ def sql_and_like_part(and_like_dict_obj: dict) -> tuple[str, dict]|bool:
|
|||||||
clauses.append(f"{key} LIKE :and_like_{key}")
|
clauses.append(f"{key} LIKE :and_like_{key}")
|
||||||
data[f'and_like_{key}'] = value
|
data[f'and_like_{key}'] = value
|
||||||
return f"AND ({' AND '.join(clauses)})", data
|
return f"AND ({' AND '.join(clauses)})", data
|
||||||
return False
|
return '', {}
|
||||||
|
|
||||||
def sql_or_like_part(or_like_dict_obj: dict) -> tuple[str, dict]|bool:
|
def sql_or_like_part(or_like_dict_obj: dict) -> tuple[str, dict]:
|
||||||
"""Creates a partial SQL string for OR LIKE queries."""
|
"""Creates a partial SQL string for OR LIKE queries."""
|
||||||
data = {}
|
data = {}
|
||||||
if or_like_dict_obj and isinstance(or_like_dict_obj, dict):
|
if or_like_dict_obj and isinstance(or_like_dict_obj, dict):
|
||||||
@@ -38,9 +38,9 @@ def sql_or_like_part(or_like_dict_obj: dict) -> tuple[str, dict]|bool:
|
|||||||
clauses.append(f"{key} LIKE :or_like_{key}")
|
clauses.append(f"{key} LIKE :or_like_{key}")
|
||||||
data[f'or_like_{key}'] = value
|
data[f'or_like_{key}'] = value
|
||||||
return f"AND ({' OR '.join(clauses)})", data
|
return f"AND ({' OR '.join(clauses)})", data
|
||||||
return False
|
return '', {}
|
||||||
|
|
||||||
def sql_and_in_dict_li_part(and_in_dict_li_dict_obj: dict) -> tuple[str, dict]|bool:
|
def sql_and_in_dict_li_part(and_in_dict_li_dict_obj: dict) -> tuple[str, dict]:
|
||||||
"""Creates a partial SQL string for AND IN queries."""
|
"""Creates a partial SQL string for AND IN queries."""
|
||||||
data = {}
|
data = {}
|
||||||
if and_in_dict_li_dict_obj and isinstance(and_in_dict_li_dict_obj, dict):
|
if and_in_dict_li_dict_obj and isinstance(and_in_dict_li_dict_obj, dict):
|
||||||
@@ -50,9 +50,9 @@ def sql_and_in_dict_li_part(and_in_dict_li_dict_obj: dict) -> tuple[str, dict]|b
|
|||||||
clauses.append(f"{key} IN :and_in_{key}")
|
clauses.append(f"{key} IN :and_in_{key}")
|
||||||
data[f'and_in_{key}'] = value
|
data[f'and_in_{key}'] = value
|
||||||
return f"AND ({' AND '.join(clauses)})", data
|
return f"AND ({' AND '.join(clauses)})", data
|
||||||
return False
|
return '', {}
|
||||||
|
|
||||||
def sql_and_qry_part(and_qry_dict_obj: dict) -> tuple[str, dict]|bool:
|
def sql_and_qry_part(and_qry_dict_obj: dict) -> tuple[str, dict]:
|
||||||
"""Creates a partial SQL string for additional AND queries (equals)."""
|
"""Creates a partial SQL string for additional AND queries (equals)."""
|
||||||
data = {}
|
data = {}
|
||||||
if and_qry_dict_obj and isinstance(and_qry_dict_obj, dict):
|
if and_qry_dict_obj and isinstance(and_qry_dict_obj, dict):
|
||||||
@@ -62,9 +62,9 @@ def sql_and_qry_part(and_qry_dict_obj: dict) -> tuple[str, dict]|bool:
|
|||||||
clauses.append(f"{key} = :and_{key}")
|
clauses.append(f"{key} = :and_{key}")
|
||||||
data[f'and_{key}'] = value
|
data[f'and_{key}'] = value
|
||||||
return f"AND ({' AND '.join(clauses)})", data
|
return f"AND ({' AND '.join(clauses)})", data
|
||||||
return False
|
return '', {}
|
||||||
|
|
||||||
def sql_fulltext_qry_part(fulltext_qry_dict: dict) -> tuple[str, dict]|bool:
|
def sql_fulltext_qry_part(fulltext_qry_dict: dict) -> tuple[str, dict]:
|
||||||
"""Creates a partial SQL string for fulltext search."""
|
"""Creates a partial SQL string for fulltext search."""
|
||||||
data = {}
|
data = {}
|
||||||
if fulltext_qry_dict and isinstance(fulltext_qry_dict, dict):
|
if fulltext_qry_dict and isinstance(fulltext_qry_dict, dict):
|
||||||
@@ -74,12 +74,12 @@ def sql_fulltext_qry_part(fulltext_qry_dict: dict) -> tuple[str, dict]|bool:
|
|||||||
clauses.append(f"MATCH( {key} ) AGAINST( :ft_{key} IN BOOLEAN MODE )")
|
clauses.append(f"MATCH( {key} ) AGAINST( :ft_{key} IN BOOLEAN MODE )")
|
||||||
data[f'ft_{key}'] = value
|
data[f'ft_{key}'] = value
|
||||||
return f"AND ({' OR '.join(clauses)})", data
|
return f"AND ({' OR '.join(clauses)})", data
|
||||||
return False
|
return '', {}
|
||||||
|
|
||||||
def sql_enable_part(table_name: str, enabled: str) -> tuple[str, bool|None]|bool:
|
def sql_enable_part(table_name: str, enabled: str) -> tuple[str, bool|None]:
|
||||||
"""Handles enabled/disabled status filtering with schema check."""
|
"""Handles enabled/disabled status filtering with schema check."""
|
||||||
from app import lib_sql_core
|
from app import lib_sql_core
|
||||||
if not table_name: return False
|
if not table_name: return '', None
|
||||||
if enabled in ['enabled', 'disabled', 'all']:
|
if enabled in ['enabled', 'disabled', 'all']:
|
||||||
if enabled == 'all': return '', None
|
if enabled == 'all': return '', None
|
||||||
try:
|
try:
|
||||||
@@ -89,12 +89,12 @@ def sql_enable_part(table_name: str, enabled: str) -> tuple[str, bool|None]|bool
|
|||||||
return '', None
|
return '', None
|
||||||
val = (enabled == 'enabled')
|
val = (enabled == 'enabled')
|
||||||
return f"AND `{table_name}`.enable = {str(val).lower()}", val
|
return f"AND `{table_name}`.enable = {str(val).lower()}", val
|
||||||
return False
|
return '', None
|
||||||
|
|
||||||
def sql_hidden_part(table_name: str, hidden: str) -> tuple[str, bool|None]|bool:
|
def sql_hidden_part(table_name: str, hidden: str) -> tuple[str, bool|None]:
|
||||||
"""Handles hidden status filtering with schema check."""
|
"""Handles hidden status filtering with schema check."""
|
||||||
from app import lib_sql_core
|
from app import lib_sql_core
|
||||||
if not table_name: return False
|
if not table_name: return '', None
|
||||||
if hidden in ['hidden', 'not_hidden', 'all']:
|
if hidden in ['hidden', 'not_hidden', 'all']:
|
||||||
if hidden == 'all': return '', None
|
if hidden == 'all': return '', None
|
||||||
try:
|
try:
|
||||||
@@ -105,9 +105,9 @@ def sql_hidden_part(table_name: str, hidden: str) -> tuple[str, bool|None]|bool:
|
|||||||
if hidden == 'hidden':
|
if hidden == 'hidden':
|
||||||
return f"AND `{table_name}`.hide = true", True
|
return f"AND `{table_name}`.hide = true", True
|
||||||
return f"AND (`{table_name}`.hide = false OR `{table_name}`.hide IS NULL)", False
|
return f"AND (`{table_name}`.hide = false OR `{table_name}`.hide IS NULL)", False
|
||||||
return False
|
return '', None
|
||||||
|
|
||||||
def sql_where_qry_part(qry_dict_li: list) -> tuple[str, dict]|bool:
|
def sql_where_qry_part(qry_dict_li: list) -> tuple[str, dict]:
|
||||||
"""Standard v2 style WHERE clause builder."""
|
"""Standard v2 style WHERE clause builder."""
|
||||||
data = {}
|
data = {}
|
||||||
if qry_dict_li and isinstance(qry_dict_li, list):
|
if qry_dict_li and isinstance(qry_dict_li, list):
|
||||||
@@ -124,7 +124,7 @@ def sql_where_qry_part(qry_dict_li: list) -> tuple[str, dict]|bool:
|
|||||||
clauses.append(f'{type_} {field} {op} :{field}')
|
clauses.append(f'{type_} {field} {op} :{field}')
|
||||||
data[field] = val
|
data[field] = val
|
||||||
return ' '.join(clauses), data
|
return ' '.join(clauses), data
|
||||||
return False
|
return '', {}
|
||||||
|
|
||||||
def sql_search_qry_part(
|
def sql_search_qry_part(
|
||||||
search_query: Any,
|
search_query: Any,
|
||||||
|
|||||||
@@ -70,8 +70,8 @@ class Event_Base(BaseModel):
|
|||||||
|
|
||||||
recurring: Optional[bool]
|
recurring: Optional[bool]
|
||||||
recurring_pattern: Optional[str]
|
recurring_pattern: Optional[str]
|
||||||
recurring_start_time: Optional[datetime.time]
|
recurring_start_time: Optional[str]
|
||||||
recurring_end_time: Optional[datetime.time]
|
recurring_end_time: Optional[str]
|
||||||
recurring_text: Optional[str]
|
recurring_text: Optional[str]
|
||||||
|
|
||||||
weekday_sunday: Optional[bool]
|
weekday_sunday: Optional[bool]
|
||||||
@@ -281,6 +281,12 @@ class Event_Base(BaseModel):
|
|||||||
return v.astimezone(pytz.UTC).isoformat()
|
return v.astimezone(pytz.UTC).isoformat()
|
||||||
else: return v
|
else: return v
|
||||||
|
|
||||||
|
@validator('recurring_start_time', 'recurring_end_time', pre=True, always=True)
|
||||||
|
def time_to_str(cls, v):
|
||||||
|
if isinstance(v, (datetime.time, datetime.timedelta)):
|
||||||
|
return str(v)
|
||||||
|
return v
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
underscore_attrs_are_private = True
|
underscore_attrs_are_private = True
|
||||||
allow_population_by_field_name = True
|
allow_population_by_field_name = True
|
||||||
@@ -340,8 +346,8 @@ class Event_Meeting_Flat_Base(BaseModel):
|
|||||||
|
|
||||||
recurring: Optional[bool]
|
recurring: Optional[bool]
|
||||||
recurring_pattern: Optional[str]
|
recurring_pattern: Optional[str]
|
||||||
recurring_start_time: Optional[datetime.time]
|
recurring_start_time: Optional[str]
|
||||||
recurring_end_time: Optional[datetime.time]
|
recurring_end_time: Optional[str]
|
||||||
recurring_text: Optional[str]
|
recurring_text: Optional[str]
|
||||||
|
|
||||||
weekday_sunday: Optional[bool]
|
weekday_sunday: Optional[bool]
|
||||||
|
|||||||
Reference in New Issue
Block a user