fix: resolve secondary FKs in nested POST (event_badge_template_id)

In the nested POST handler (api_crud_v3_nested.py), sanitize_payload was
running before model instantiation. For secondary FK fields like
event_badge_template_id, sanitize_payload resolved the random string →
integer, then the model's root_validator stripped the integer back to None
(Vision ID anti-leakage guard). Only the parent FK survived because it was
explicitly re-injected after serialization.

Fix: moved sanitize_payload to run on data_to_insert after serialization,
matching the flat V3 POST pattern (api_crud_v3.py). Also moved account_id
injection to after sanitize_payload, fixing a latent bug where account_id
was silently written as NULL on non-bypass auth.

Adds regression test to test_e2e_v3_demo_parity.py that creates an
event_badge via nested POST with event_badge_template_id and verifies the
field is non-None in the response.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-04-11 19:00:51 -04:00
parent 516865b7d8
commit 0ecc5a97d5
2 changed files with 103 additions and 11 deletions

View File

@@ -313,15 +313,6 @@ async def post_child_obj(
if not table_name_insert or not input_model or not table_name_select or not output_model:
return mk_resp(data=False, status_code=500, response=response, status_message=f"Configuration error.")
if not account.super and account.auth_method != 'bypass' and account.account_id:
if 'account_id' in input_model.__fields__:
obj_data['account_id'] = account.account_id
obj_data[f'{parent_obj_type}_id'] = resolved_parent_id
# Sanitize payload (ID resolution, virtual fields, and optionally extra fields)
sanitize_payload(obj_data, input_model, ignore_extra=x_ae_ignore_extra_fields)
try:
validated_obj = input_model(**obj_data)
except ValidationError as e:
@@ -332,8 +323,21 @@ 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.
# Sanitize AFTER serialization so that:
# 1. The model receives raw Vision ID strings (passes field-length constraints).
# 2. ID resolution (string → integer) happens on the dict going to the DB,
# avoiding the root_validator's integer-stripping anti-leakage guard.
# (Matches the flat V3 POST pattern in api_crud_v3.py.)
sanitize_payload(data_to_insert, input_model, ignore_extra=x_ae_ignore_extra_fields)
# Enforce account ownership AFTER sanitize_payload so the integer account_id goes
# straight to the DB without conflicting with Vision ID string constraints in the model.
if not account.super and account.auth_method != 'bypass' and account.account_id:
if 'account_id' in input_model.__fields__:
data_to_insert['account_id'] = account.account_id
# Re-inject parent FK last — overrides anything sanitize_payload or the model may have
# set — ensuring the child is always linked to the correct parent.
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):