feat(api-v3): implement permissive updates, automatic ID resolution, and structured error reporting

- Added 'x-ae-ignore-extra-fields' header to support stripping unknown fields in POST/PATCH.
- Added automatic resolution of '*_id_random' strings to integer IDs in 'sanitize_payload'.
- Refactored 'post_obj' to return structured (field -> message) validation errors in 'meta.details'.
- Updated 'mk_resp' to support non-string 'details' in response metadata.
- Added 'tests/verify_feedback_fixes.py' to validate logic changes.

Ref: V3 API Refinement Feedback from mcp_agent.
This commit is contained in:
Scott Idem
2026-01-14 19:11:56 -05:00
parent 722409de0b
commit 34a752d455
4 changed files with 153 additions and 14 deletions

View File

@@ -122,20 +122,36 @@ def safe_json_loads(json_str: Optional[str]) -> Any:
try: return json.loads(json_str)
except: return None
def sanitize_payload(data: dict, model: Any) -> None:
def sanitize_payload(data: dict, model: Any, ignore_extra: bool = False) -> None:
"""
Sanitizes an input payload before database insertion or update.
1. Removes virtual lookup fields (ending in `_id_random`) that are used for API
1. Resolves virtual lookup fields (`*_id_random`) into their integer database IDs.
2. Removes virtual lookup fields (ending in `_id_random`) that are used for API
convenience but do not exist in the database.
2. Removes fields explicitly marked for exclusion in the model's
3. Removes fields explicitly marked for exclusion in the model's
`fields_to_exclude_from_db` ClassVar (e.g., view-only fields).
4. If `ignore_extra` is True, removes all fields NOT present in the model definition.
Modifies the `data` dictionary in-place.
"""
if not isinstance(data, dict):
return
from app.db_sql import redis_lookup_id_random
# Resolve virtual _id_random fields to integer IDs (e.g., account_id_random -> account_id)
# This must happen BEFORE we delete them.
for k, v in list(data.items()):
if k.endswith('_id_random') and k != 'id_random' and v:
target_id_field = k.replace('_id_random', '_id')
# Only resolve if the integer version is missing or null
if not data.get(target_id_field):
obj_type_lookup = k.replace('_id_random', '')
resolved_id = redis_lookup_id_random(record_id_random=v, table_name=obj_type_lookup)
if resolved_id:
data[target_id_field] = resolved_id
# Filter out virtual _id_random fields (e.g., account_id_random)
keys_to_remove = [k for k in data.keys() if k.endswith('_id_random') and k != 'id_random']
for k in keys_to_remove:
@@ -146,3 +162,15 @@ def sanitize_payload(data: dict, model: Any) -> None:
for k in model.fields_to_exclude_from_db:
if k in data:
del data[k]
# If permissive mode is on, remove any field not in the Pydantic model
if ignore_extra and model and hasattr(model, '__fields__'):
model_fields = set(model.__fields__.keys())
# Also check for aliases
for f in model.__fields__.values():
if f.alias:
model_fields.add(f.alias)
extra_keys = [k for k in data.keys() if k not in model_fields]
for k in extra_keys:
del data[k]