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:
Scott Idem
2026-03-16 12:39:45 -04:00
parent ee28a4f26e
commit eaa18a1d45
3 changed files with 171 additions and 24 deletions

View File

@@ -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