diff --git a/app/lib_redis_helpers.py b/app/lib_redis_helpers.py index ca9ce12..0ca2c9b 100644 --- a/app/lib_redis_helpers.py +++ b/app/lib_redis_helpers.py @@ -158,7 +158,7 @@ def lookup_id_random_pop( id_prefixes = [ 'account', 'activity_log', 'address', 'address_location', 'archive', 'contact', 'contact_1', 'contact_2', 'cont_edu_cert', 'cont_edu_cert_person', - 'event', 'event_id_random_only', 'event_abstract', 'event_badge', + 'entry', 'event', 'event_id_random_only', 'event_abstract', 'event_badge', 'event_badge_template', 'event_exhibit', 'event_file', 'event_location', 'event_person', 'event_person_profile', 'event_presentation', 'event_presenter', 'event_registration', 'event_session', 'event_track', @@ -178,6 +178,7 @@ def lookup_id_random_pop( table = prefix if prefix == 'address_location': table = 'address' elif prefix in ['contact_1', 'contact_2']: table = 'contact' + elif prefix == 'entry': table = 'journal_entry' elif prefix == 'event_id_random_only': table = 'event' elif prefix == 'poc_event_person': table = 'event_person' elif prefix == 'poc_person': table = 'person' diff --git a/app/object_definitions/journals.py b/app/object_definitions/journals.py index 4fb63af..ee4f674 100644 --- a/app/object_definitions/journals.py +++ b/app/object_definitions/journals.py @@ -54,3 +54,6 @@ journal_obj_li = { ], }, } + +# Aliases for shorter/cleaner URLs +journal_obj_li['entry'] = journal_obj_li['journal_entry'] diff --git a/app/routers/api_crud_v3_nested.py b/app/routers/api_crud_v3_nested.py index 58b4321..a12b14d 100644 --- a/app/routers/api_crud_v3_nested.py +++ b/app/routers/api_crud_v3_nested.py @@ -78,6 +78,8 @@ async def get_child_obj_li( if obj_name not in obj_type_kv_li or parent_obj_type not in obj_type_kv_li: return mk_resp(data=False, status_code=400, response=response, status_message=f"Invalid object type(s).") + # ID Vision: Resolve physical table names from registry to support aliases + parent_table = obj_type_kv_li[parent_obj_type].get('tbl') obj_cfg = obj_type_kv_li[obj_name] table_name = obj_cfg.get(f'tbl_{view}', obj_cfg.get('tbl_default', obj_cfg.get('tbl'))) base_name = obj_cfg.get(f'mdl_{view}', obj_cfg.get('mdl_default', obj_cfg.get('mdl'))) @@ -88,13 +90,13 @@ async def get_child_obj_li( order_by_li = filter_order_by(order_by_li, base_name, table_name) status_filter = get_supported_filters(base_name, status_filter) - resolved_parent_id = redis_lookup_id_random(record_id_random=parent_obj_id, table_name=parent_obj_type) + resolved_parent_id = redis_lookup_id_random(record_id_random=parent_obj_id, table_name=parent_table) if not resolved_parent_id: return mk_resp(data=False, status_code=404, response=response, status_message=f"Parent not found.") parent_cfg = obj_type_kv_li[parent_obj_type] - parent_table = parent_cfg.get('tbl_default', parent_cfg.get('tbl')) - if parent_sql_res := sql_select(table_name=parent_table, record_id=resolved_parent_id): + parent_table_select = parent_cfg.get('tbl_default', parent_cfg.get('tbl')) + if parent_sql_res := sql_select(table_name=parent_table_select, record_id=resolved_parent_id): if not check_account_access(parent_sql_res, account, parent_obj_type): return mk_resp(data=False, status_code=403, response=response, status_message="Access denied to parent.") else: @@ -165,6 +167,8 @@ async def search_child_obj_li( if obj_name not in obj_type_kv_li or parent_obj_type not in obj_type_kv_li: return mk_resp(data=False, status_code=400, response=response, status_message="Invalid object type(s).") + # ID Vision: Resolve physical table names from registry to support aliases + parent_table = obj_type_kv_li[parent_obj_type].get('tbl') obj_cfg = obj_type_kv_li[obj_name] table_name = obj_cfg.get(f'tbl_{view}', obj_cfg.get('tbl_default', obj_cfg.get('tbl'))) base_name = obj_cfg.get(f'mdl_{view}', obj_cfg.get('mdl_default', obj_cfg.get('mdl'))) @@ -176,13 +180,13 @@ async def search_child_obj_li( status_filter = get_supported_filters(base_name, status_filter) searchable_fields = obj_cfg.get('searchable_fields') - resolved_parent_id = redis_lookup_id_random(record_id_random=parent_obj_id, table_name=parent_obj_type) + resolved_parent_id = redis_lookup_id_random(record_id_random=parent_obj_id, table_name=parent_table) if not resolved_parent_id: return mk_resp(data=False, status_code=404, response=response, status_message="Parent not found.") parent_cfg = obj_type_kv_li[parent_obj_type] - parent_table = parent_cfg.get('tbl_default', parent_cfg.get('tbl')) - if parent_sql_res := sql_select(table_name=parent_table, record_id=resolved_parent_id): + parent_table_select = parent_cfg.get('tbl_default', parent_cfg.get('tbl')) + if parent_sql_res := sql_select(table_name=parent_table_select, record_id=resolved_parent_id): if not check_account_access(parent_sql_res, account, parent_obj_type): return mk_resp(data=False, status_code=403, response=response, status_message="Access denied to parent.") else: @@ -249,13 +253,15 @@ async def post_child_obj( if parent_obj_type not in obj_type_kv_li or child_obj_type not in obj_type_kv_li: return mk_resp(data=False, status_code=400, response=response, status_message="Invalid object type.") - resolved_parent_id = redis_lookup_id_random(record_id_random=parent_obj_id, table_name=parent_obj_type) + # ID Vision: Resolve physical table names from registry to support aliases + parent_table = obj_type_kv_li[parent_obj_type].get('tbl') + resolved_parent_id = redis_lookup_id_random(record_id_random=parent_obj_id, table_name=parent_table) if not resolved_parent_id: return mk_resp(data=False, status_code=404, response=response, status_message="Parent not found.") parent_cfg = obj_type_kv_li[parent_obj_type] - parent_table = parent_cfg.get('tbl_default', parent_cfg.get('tbl')) - if parent_sql_res := sql_select(table_name=parent_table, record_id=resolved_parent_id): + parent_table_select = parent_cfg.get('tbl_default', parent_cfg.get('tbl')) + if parent_sql_res := sql_select(table_name=parent_table_select, record_id=resolved_parent_id): if not check_account_access(parent_sql_res, account, parent_obj_type): return mk_resp(data=False, status_code=403, response=response, status_message="Access denied to parent.") else: @@ -325,8 +331,15 @@ async def get_child_obj( if delay.sleep_time_s > 0: await asyncio.sleep(delay.sleep_time_s) - resolved_parent_id = redis_lookup_id_random(record_id_random=parent_obj_id, table_name=parent_obj_type) - resolved_child_id = redis_lookup_id_random(record_id_random=child_obj_id, table_name=child_obj_type) + # ID Vision: Resolve physical table names from registry to support aliases + if parent_obj_type not in obj_type_kv_li or child_obj_type not in obj_type_kv_li: + return mk_resp(data=False, status_code=400, response=response, status_message="Invalid object type(s).") + + parent_table = obj_type_kv_li[parent_obj_type].get('tbl') + child_table = obj_type_kv_li[child_obj_type].get('tbl') + + resolved_parent_id = redis_lookup_id_random(record_id_random=parent_obj_id, table_name=parent_table) + resolved_child_id = redis_lookup_id_random(record_id_random=child_obj_id, table_name=child_table) if not resolved_parent_id or not resolved_child_id: return mk_resp(data=False, status_code=404, response=response, status_message="Object(s) not found.") @@ -368,8 +381,127 @@ async def patch_child_obj( if delay.sleep_time_s > 0: await asyncio.sleep(delay.sleep_time_s) obj_data = await request.json() - resolved_parent_id = redis_lookup_id_random(record_id_random=parent_obj_id, table_name=parent_obj_type) - resolved_child_id = redis_lookup_id_random(record_id_random=child_obj_id, table_name=child_obj_type) + + # ID Vision: Resolve physical table names from registry to support aliases + if parent_obj_type not in obj_type_kv_li or child_obj_type not in obj_type_kv_li: + return mk_resp(data=False, status_code=400, response=response, status_message="Invalid object type(s).") + + parent_table = obj_type_kv_li[parent_obj_type].get('tbl') + child_table = obj_type_kv_li[child_obj_type].get('tbl') + + resolved_parent_id = redis_lookup_id_random(record_id_random=parent_obj_id, table_name=parent_table) + resolved_child_id = redis_lookup_id_random(record_id_random=child_obj_id, table_name=child_table) + + if not resolved_parent_id or not resolved_child_id: + return mk_resp(data=False, status_code=404, response=response, status_message="Object(s) not found.") + + obj_cfg = obj_type_kv_li[child_obj_type] + table_name_update = obj_cfg.get('tbl_update', obj_cfg.get('tbl')) + table_name_select = obj_cfg.get('tbl_default', obj_cfg.get('tbl')) + input_model = obj_cfg.get('mdl_in', obj_cfg.get('mdl')) + output_model = obj_cfg.get('mdl_out', obj_cfg.get('mdl_default', obj_cfg.get('mdl'))) + + if existing_child := sql_select(table_name=table_name_select, record_id=resolved_child_id): + if existing_child.get(f'{parent_obj_type}_id') != resolved_parent_id: + return mk_resp(data=False, status_code=404, response=response, status_message="Child not found under parent.") + else: + return mk_resp(data=False, status_code=404, response=response, status_message="Child not found.") + + # Sanitize payload (ID resolution, virtual fields, and optionally extra fields) + sanitize_payload(obj_data, input_model, ignore_extra=x_ae_ignore_extra_fields) + + if sql_update(data=obj_data, table_name=table_name_update, record_id=resolved_child_id): + if return_obj: + if updated_child := sql_select(table_name=table_name_select, record_id=resolved_child_id): + resp_data = output_model(**updated_child).dict(by_alias=serialization.by_alias, exclude_unset=serialization.exclude_unset) + return mk_resp(data=resp_data, response=response) + return mk_resp(data=True, response=response, status_message="Updated successfully.") + else: + db_err = format_db_error(get_last_sql_error()) + return mk_resp(data=False, status_code=400, response=response, status_message="Update failed.", details=db_err.dict()) + + +@router.get('/{parent_obj_type}/{parent_obj_id}/{child_obj_type}/{child_obj_id}', response_model=Resp_Body_Base) +async def get_child_obj( + response: Response, + parent_obj_type: str = Path(min_length=2, max_length=50), + parent_obj_id: str = Path(min_length=11, max_length=22), + child_obj_type: str = Path(min_length=2, max_length=50), + child_obj_id: str = Path(min_length=11, max_length=22), + view: str = Query('default'), + account: AccountContext = Depends(get_account_context), + serialization: SerializationParams = Depends(), + delay: DelayParams = Depends(), + ): + """ + Retrieve Child Object. + + Verifies that the child belongs to the specified parent. + """ + from app.db_sql import redis_lookup_id_random, sql_select + + if delay.sleep_time_s > 0: await asyncio.sleep(delay.sleep_time_s) + + # ID Vision: Resolve physical table names from registry to support aliases + if parent_obj_type not in obj_type_kv_li or child_obj_type not in obj_type_kv_li: + return mk_resp(data=False, status_code=400, response=response, status_message="Invalid object type(s).") + + parent_table = obj_type_kv_li[parent_obj_type].get('tbl') + child_table = obj_type_kv_li[child_obj_type].get('tbl') + + resolved_parent_id = redis_lookup_id_random(record_id_random=parent_obj_id, table_name=parent_table) + resolved_child_id = redis_lookup_id_random(record_id_random=child_obj_id, table_name=child_table) + + if not resolved_parent_id or not resolved_child_id: + return mk_resp(data=False, status_code=404, response=response, status_message="Object(s) not found.") + + obj_cfg = obj_type_kv_li[child_obj_type] + table_name = obj_cfg.get(f'tbl_{view}', obj_cfg.get('tbl_default', obj_cfg.get('tbl'))) + base_name = obj_cfg.get(f'mdl_{view}', obj_cfg.get('mdl_default', obj_cfg.get('mdl'))) + + if sql_result := sql_select(table_name=table_name, record_id=resolved_child_id): + if sql_result.get(f'{parent_obj_type}_id') != resolved_parent_id: + return mk_resp(data=False, status_code=404, response=response, status_message="Child not found under parent.") + + resp_data = base_name(**sql_result).dict(by_alias=serialization.by_alias, exclude_unset=serialization.exclude_unset) + return mk_resp(data=resp_data, response=response) + return mk_resp(data=False, status_code=404, response=response, status_message="Child not found.") + + +@router.patch('/{parent_obj_type}/{parent_obj_id}/{child_obj_type}/{child_obj_id}', response_model=Resp_Body_Base) +async def patch_child_obj( + request: Request, + response: Response, + parent_obj_type: str = Path(min_length=2, max_length=50), + parent_obj_id: str = Path(min_length=11, max_length=22), + child_obj_type: str = Path(min_length=2, max_length=50), + child_obj_id: str = Path(min_length=11, max_length=22), + return_obj: Optional[bool] = True, + x_ae_ignore_extra_fields: Optional[bool] = Header(False), + account: AccountContext = Depends(get_account_context), + serialization: SerializationParams = Depends(), + delay: DelayParams = Depends(), + ): + """ + Update Child Object. + + Verifies that the child belongs to the specified parent before updating. + """ + from app.db_sql import redis_lookup_id_random, sql_select, sql_update + + if delay.sleep_time_s > 0: await asyncio.sleep(delay.sleep_time_s) + + obj_data = await request.json() + + # ID Vision: Resolve physical table names from registry to support aliases + if parent_obj_type not in obj_type_kv_li or child_obj_type not in obj_type_kv_li: + return mk_resp(data=False, status_code=400, response=response, status_message="Invalid object type(s).") + + parent_table = obj_type_kv_li[parent_obj_type].get('tbl') + child_table = obj_type_kv_li[child_obj_type].get('tbl') + + resolved_parent_id = redis_lookup_id_random(record_id_random=parent_obj_id, table_name=parent_table) + resolved_child_id = redis_lookup_id_random(record_id_random=child_obj_id, table_name=child_table) if not resolved_parent_id or not resolved_child_id: return mk_resp(data=False, status_code=404, response=response, status_message="Object(s) not found.") @@ -420,8 +552,15 @@ async def delete_child_obj( if delay.sleep_time_s > 0: await asyncio.sleep(delay.sleep_time_s) - resolved_parent_id = redis_lookup_id_random(record_id_random=parent_obj_id, table_name=parent_obj_type) - resolved_child_id = redis_lookup_id_random(record_id_random=child_obj_id, table_name=child_obj_type) + # ID Vision: Resolve physical table names from registry to support aliases + if parent_obj_type not in obj_type_kv_li or child_obj_type not in obj_type_kv_li: + return mk_resp(data=False, status_code=400, response=response, status_message="Invalid object type(s).") + + parent_table = obj_type_kv_li[parent_obj_type].get('tbl') + child_table = obj_type_kv_li[child_obj_type].get('tbl') + + resolved_parent_id = redis_lookup_id_random(record_id_random=parent_obj_id, table_name=parent_table) + resolved_child_id = redis_lookup_id_random(record_id_random=child_obj_id, table_name=child_table) if not resolved_parent_id or not resolved_child_id: return mk_resp(data=False, status_code=404, response=response, status_message="Object(s) not found.") @@ -445,4 +584,4 @@ async def delete_child_obj( if success: return mk_resp(data=True, response=response, status_message=f"Deleted successfully via {method}.") - return mk_resp(data=False, status_code=400, response=response, status_message="Deletion failed.") + return mk_resp(data=False, status_code=400, response=response, status_message="Deletion failed.") \ No newline at end of file