fix(models): migrate Archive_Content_Base to Vision ID pattern
- Replace integer `id` (alias archive_content_id) with Vision string fields:
`id: Optional[str]` and `archive_content_id: Optional[str]` — both always
hold the random string ID, never the DB integer.
- Add `root_validator(pre=True)` (map_v3_ids) that maps id_random /
archive_content_id_random → id and archive_content_id, with collision
prevention to reject any integer that arrives in these fields.
- Remove old `archive_content_id_lookup` integer validator (superseded by
sanitize_payload + root_validator).
- Keep `id_random` (alias archive_content_id_random) in responses for
backward compatibility; add id, archive_content_id, id_random to
fields_to_exclude_from_db so they never appear in INSERT/UPDATE payloads.
Generic CRUD layer safety net (post_obj + post_child_obj):
- After building resp_data on create, swap any integer {obj_type}_id with
the corresponding {obj_type}_id_random value — catches models not yet
migrated to Vision IDs.
- Fix return_obj=False fallback to return obj_id as the random string.
Docs: add Section 3D to GUIDE__AE_API_V3_for_Frontend.md documenting the
Vision ID convention — {obj_type}_id is always the random string; the
_id_random suffix is a legacy artifact that frontend code should phase out.
Fixes: POST /v3/crud/archive/{id}/archive_content/ returning integer ID,
breaking the subsequent PATCH flow (422 min_length validation failure).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import datetime, pytz
|
||||
|
||||
from typing import Dict, List, Optional, Set, Union, ClassVar
|
||||
from pydantic import BaseModel, EmailStr, Field, Json, PrivateAttr, ValidationError, validator
|
||||
from pydantic import BaseModel, EmailStr, Field, Json, PrivateAttr, ValidationError, validator, root_validator
|
||||
|
||||
from app.db_sql import redis_lookup_id_random
|
||||
from app.lib_general import log, logging
|
||||
@@ -18,13 +18,12 @@ class Archive_Content_Base(BaseModel):
|
||||
# log.debug(test_var)
|
||||
# return test_var
|
||||
|
||||
id_random: Optional[str] = Field(
|
||||
# **base_fields['archive_content_id_random'],
|
||||
alias = 'archive_content_id_random',
|
||||
)
|
||||
id: Optional[int] = Field(
|
||||
alias = 'archive_content_id'
|
||||
)
|
||||
# --- Vision IDs (primary public identifiers — always random strings) ---
|
||||
id: Optional[str] = Field(None, **base_fields['archive_content_id_random'])
|
||||
archive_content_id: Optional[str] = Field(None, **base_fields['archive_content_id_random'])
|
||||
# Legacy alias kept for backward compatibility; populated by root_validator
|
||||
id_random: Optional[str] = Field(None, alias='archive_content_id_random')
|
||||
|
||||
account_id_random: Optional[str]
|
||||
account_id: Optional[int]
|
||||
|
||||
@@ -94,6 +93,7 @@ class Archive_Content_Base(BaseModel):
|
||||
|
||||
# Fields that are part of the model (for reading) but should not be saved to the DB table
|
||||
fields_to_exclude_from_db: ClassVar[list] = [
|
||||
'id', 'archive_content_id', 'id_random',
|
||||
'account_id', 'account_id_random', 'archive_id_random', 'hosted_file_id_random',
|
||||
'hosted_file_path', 'api_hosted_file_path_download', 'api_hosted_file_path_stream',
|
||||
'hosted_file_hash_sha256', 'hosted_file_content_type', 'hosted_file_size'
|
||||
@@ -101,12 +101,21 @@ class Archive_Content_Base(BaseModel):
|
||||
|
||||
_processed_at: datetime.datetime = PrivateAttr(default_factory=datetime.datetime.now)
|
||||
|
||||
@validator('id', always=True)
|
||||
def archive_content_id_lookup(cls, v, values, **kwargs):
|
||||
if isinstance(v, int) and v > 0: return v
|
||||
elif id_random := values.get('id_random'):
|
||||
return redis_lookup_id_random(record_id_random=id_random, table_name='archive_content')
|
||||
return None
|
||||
@root_validator(pre=True)
|
||||
def map_v3_ids(cls, values):
|
||||
"""
|
||||
Vision Transformer: Map DB random-string keys to clean Vision ID fields.
|
||||
Collision prevention strips any integer that snuck into the string ID fields.
|
||||
"""
|
||||
rid = values.get('id_random') or values.get('archive_content_id_random')
|
||||
if rid and isinstance(rid, str):
|
||||
values['id'] = rid
|
||||
values['archive_content_id'] = rid
|
||||
# Collision prevention: reject integer values in Vision string fields
|
||||
for k in ['id', 'archive_content_id']:
|
||||
if k in values and not isinstance(values[k], str):
|
||||
del values[k]
|
||||
return values
|
||||
|
||||
@validator('archive_id', always=True)
|
||||
def archive_id_lookup(cls, v, values, **kwargs):
|
||||
|
||||
@@ -464,8 +464,13 @@ async def post_obj(
|
||||
if return_obj:
|
||||
if sql_select_result := sql_select(table_name=table_name_select, record_id=new_obj_id):
|
||||
resp_data = output_model(**sql_select_result).dict(by_alias=serialization.by_alias, exclude_unset=serialization.exclude_unset)
|
||||
# V3 contract: {obj_type}_id in responses must be the random string, never the integer.
|
||||
_id_key = f'{obj_name}_id' if serialization.by_alias else 'id'
|
||||
_rand_key = f'{obj_name}_id_random' if serialization.by_alias else 'id_random'
|
||||
if isinstance(resp_data.get(_id_key), int) and resp_data.get(_rand_key):
|
||||
resp_data[_id_key] = resp_data[_rand_key]
|
||||
return mk_resp(data=resp_data, response=response)
|
||||
return mk_resp(data={"obj_id": new_obj_id, "obj_id_random": new_obj_id_random}, response=response)
|
||||
return mk_resp(data={"obj_id": new_obj_id_random, "obj_id_random": new_obj_id_random}, response=response)
|
||||
else:
|
||||
# Standardized rich error bubbling
|
||||
db_err = format_db_error(get_last_sql_error())
|
||||
|
||||
@@ -306,8 +306,13 @@ async def post_child_obj(
|
||||
if return_obj:
|
||||
if sql_select_result := sql_select(table_name=table_name_select, record_id=new_obj_id):
|
||||
resp_data = output_model(**sql_select_result).dict(by_alias=serialization.by_alias, exclude_unset=serialization.exclude_unset)
|
||||
# V3 contract: {obj_type}_id in responses must be the random string, never the integer.
|
||||
_id_key = f'{child_obj_type}_id' if serialization.by_alias else 'id'
|
||||
_rand_key = f'{child_obj_type}_id_random' if serialization.by_alias else 'id_random'
|
||||
if isinstance(resp_data.get(_id_key), int) and resp_data.get(_rand_key):
|
||||
resp_data[_id_key] = resp_data[_rand_key]
|
||||
return mk_resp(data=resp_data, response=response)
|
||||
return mk_resp(data={"obj_id": new_obj_id, "obj_id_random": new_obj_id_random}, response=response)
|
||||
return mk_resp(data={"obj_id": new_obj_id_random, "obj_id_random": new_obj_id_random}, response=response)
|
||||
else:
|
||||
# Standardized rich error bubbling
|
||||
db_err = format_db_error(get_last_sql_error())
|
||||
|
||||
Reference in New Issue
Block a user