fix(nested-crud): re-inject parent FK after model serialization to prevent 1364 errors
Root cause: child model root_validators (Vision ID anti-leakage guard) strip
integer IDs before they can be serialized into the INSERT dict, causing MariaDB
to reject the INSERT with 'Field does not have a default value' (1364).
Fix: re-inject resolved_parent_id into data_to_insert after validated_obj.dict()
in post_child_obj(). This is safe — the integer was already verified against the
DB before model validation.
Affected (were all broken since ~2026-01-27):
- journal/{id}/journal_entry/
- event/{id}/event_session/
- event/{id}/event_person/
- event/{id}/event_registration/
- event/{id}/event_presenter/
- event/{id}/event_presentation/
- event/{id}/event_location/
- event/{id}/event_track/
- event/{id}/event_device/
- event/{id}/event_abstract/
- event/{id}/event_badge/ (different symptom: NULL FK)
Tests: add nested create lifecycle regression tests to test_e2e_v3_demo_parity.py
- POST + Vision check + DELETE for journal/journal_entry and event/event_session
- All 9 checks passing (7s)
Docs: update tests/README.md with accurate demo_parity description and
a 'When to Run Tests' matrix to prevent future gaps in coverage.
This commit is contained in:
@@ -46,7 +46,7 @@ async def get_child_obj_li(
|
||||
):
|
||||
"""
|
||||
List Child Objects (One-to-Many).
|
||||
|
||||
|
||||
Retrieves a list of child objects associated with a specific parent.
|
||||
1. Verifies parent existence and user access to the parent.
|
||||
2. Filters children where `{parent_obj_type}_id` matches the parent's ID.
|
||||
@@ -62,7 +62,7 @@ async def get_child_obj_li(
|
||||
and_like_dict_obj = None
|
||||
or_like_dict_obj = None
|
||||
and_in_dict_li_obj = None
|
||||
|
||||
|
||||
jp_obj = safe_json_loads(urllib.parse.unquote(jp)) if jp else None
|
||||
if jp_obj:
|
||||
if jp_obj.get('qry'): qry_dict_li = jp_obj['qry']
|
||||
@@ -74,7 +74,7 @@ async def get_child_obj_li(
|
||||
|
||||
order_by_li = safe_json_loads(order_by_li)
|
||||
obj_name = child_obj_type
|
||||
|
||||
|
||||
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).")
|
||||
|
||||
@@ -125,10 +125,10 @@ async def get_child_obj_li(
|
||||
if sql_result is False:
|
||||
# Standardized rich error bubbling
|
||||
db_err = format_db_error(get_last_sql_error())
|
||||
|
||||
|
||||
# If it's a schema error (like Unknown Column), it's a 400 Bad Request
|
||||
status_code = 400 if db_err.category == "database_schema" else 500
|
||||
|
||||
|
||||
return mk_resp(data=False, status_code=status_code, response=response, status_message="Listing failed due to database error.", details=db_err.dict())
|
||||
|
||||
if sql_result:
|
||||
@@ -155,7 +155,7 @@ async def search_child_obj_li(
|
||||
):
|
||||
"""
|
||||
Search Child Objects (POST).
|
||||
|
||||
|
||||
Advanced search endpoint for nested objects.
|
||||
"""
|
||||
from app.db_sql import redis_lookup_id_random, sql_select
|
||||
@@ -239,7 +239,7 @@ async def post_child_obj(
|
||||
):
|
||||
"""
|
||||
Create Child Object.
|
||||
|
||||
|
||||
1. Verifies Parent existence and access.
|
||||
2. Automatically links the new child to the parent (`{parent_obj_type}_id` = parent_id).
|
||||
3. Performs standard creation logic (validation, injection, sanitization).
|
||||
@@ -295,6 +295,10 @@ async def post_child_obj(
|
||||
|
||||
data_to_insert = validated_obj.dict(exclude_unset=True)
|
||||
|
||||
# Re-inject parent FK after model serialization. Some model root_validators strip
|
||||
# integer IDs (a Vision ID anti-leakage guard) which would drop the FK from the dict.
|
||||
data_to_insert[f'{parent_obj_type}_id'] = resolved_parent_id
|
||||
|
||||
if sql_insert_result := sql_insert(data=data_to_insert, table_name=table_name_insert):
|
||||
new_obj_id = sql_insert_result
|
||||
new_obj_id_random = get_id_random(record_id=new_obj_id, table_name=child_obj_type)
|
||||
@@ -324,7 +328,7 @@ async def get_child_obj(
|
||||
):
|
||||
"""
|
||||
Retrieve Child Object.
|
||||
|
||||
|
||||
Verifies that the child belongs to the specified parent.
|
||||
"""
|
||||
from app.db_sql import redis_lookup_id_random, sql_select
|
||||
@@ -373,7 +377,7 @@ async def patch_child_obj(
|
||||
):
|
||||
"""
|
||||
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
|
||||
@@ -435,7 +439,7 @@ async def get_child_obj(
|
||||
):
|
||||
"""
|
||||
Retrieve Child Object.
|
||||
|
||||
|
||||
Verifies that the child belongs to the specified parent.
|
||||
"""
|
||||
from app.db_sql import redis_lookup_id_random, sql_select
|
||||
@@ -484,7 +488,7 @@ async def patch_child_obj(
|
||||
):
|
||||
"""
|
||||
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
|
||||
@@ -545,7 +549,7 @@ async def delete_child_obj(
|
||||
):
|
||||
"""
|
||||
Delete Child Object.
|
||||
|
||||
|
||||
Verifies that the child belongs to the specified parent before deleting.
|
||||
"""
|
||||
from app.db_sql import redis_lookup_id_random, sql_select, sql_update, sql_delete
|
||||
|
||||
Reference in New Issue
Block a user