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:
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user