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
|
import datetime, pytz
|
||||||
|
|
||||||
from typing import Dict, List, Optional, Set, Union, ClassVar
|
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.db_sql import redis_lookup_id_random
|
||||||
from app.lib_general import log, logging
|
from app.lib_general import log, logging
|
||||||
@@ -18,13 +18,12 @@ class Archive_Content_Base(BaseModel):
|
|||||||
# log.debug(test_var)
|
# log.debug(test_var)
|
||||||
# return test_var
|
# return test_var
|
||||||
|
|
||||||
id_random: Optional[str] = Field(
|
# --- Vision IDs (primary public identifiers — always random strings) ---
|
||||||
# **base_fields['archive_content_id_random'],
|
id: Optional[str] = Field(None, **base_fields['archive_content_id_random'])
|
||||||
alias = '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: Optional[int] = Field(
|
id_random: Optional[str] = Field(None, alias='archive_content_id_random')
|
||||||
alias = 'archive_content_id'
|
|
||||||
)
|
|
||||||
account_id_random: Optional[str]
|
account_id_random: Optional[str]
|
||||||
account_id: Optional[int]
|
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 that are part of the model (for reading) but should not be saved to the DB table
|
||||||
fields_to_exclude_from_db: ClassVar[list] = [
|
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',
|
'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_path', 'api_hosted_file_path_download', 'api_hosted_file_path_stream',
|
||||||
'hosted_file_hash_sha256', 'hosted_file_content_type', 'hosted_file_size'
|
'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)
|
_processed_at: datetime.datetime = PrivateAttr(default_factory=datetime.datetime.now)
|
||||||
|
|
||||||
@validator('id', always=True)
|
@root_validator(pre=True)
|
||||||
def archive_content_id_lookup(cls, v, values, **kwargs):
|
def map_v3_ids(cls, values):
|
||||||
if isinstance(v, int) and v > 0: return v
|
"""
|
||||||
elif id_random := values.get('id_random'):
|
Vision Transformer: Map DB random-string keys to clean Vision ID fields.
|
||||||
return redis_lookup_id_random(record_id_random=id_random, table_name='archive_content')
|
Collision prevention strips any integer that snuck into the string ID fields.
|
||||||
return None
|
"""
|
||||||
|
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)
|
@validator('archive_id', always=True)
|
||||||
def archive_id_lookup(cls, v, values, **kwargs):
|
def archive_id_lookup(cls, v, values, **kwargs):
|
||||||
|
|||||||
@@ -464,8 +464,13 @@ async def post_obj(
|
|||||||
if return_obj:
|
if return_obj:
|
||||||
if sql_select_result := sql_select(table_name=table_name_select, record_id=new_obj_id):
|
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)
|
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=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:
|
else:
|
||||||
# Standardized rich error bubbling
|
# Standardized rich error bubbling
|
||||||
db_err = format_db_error(get_last_sql_error())
|
db_err = format_db_error(get_last_sql_error())
|
||||||
|
|||||||
@@ -306,8 +306,13 @@ async def post_child_obj(
|
|||||||
if return_obj:
|
if return_obj:
|
||||||
if sql_select_result := sql_select(table_name=table_name_select, record_id=new_obj_id):
|
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)
|
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=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:
|
else:
|
||||||
# Standardized rich error bubbling
|
# Standardized rich error bubbling
|
||||||
db_err = format_db_error(get_last_sql_error())
|
db_err = format_db_error(get_last_sql_error())
|
||||||
|
|||||||
@@ -68,6 +68,30 @@ Modify data in the system.
|
|||||||
* **Header:** `x-ae-ignore-extra-fields: true`
|
* **Header:** `x-ae-ignore-extra-fields: true`
|
||||||
* **Behavior:** When set to `true`, the backend will automatically strip any fields from the payload that are not defined in the object's model before attempting to save to the database.
|
* **Behavior:** When set to `true`, the backend will automatically strip any fields from the payload that are not defined in the object's model before attempting to save to the database.
|
||||||
|
|
||||||
|
### D. ID Fields in Responses (Vision ID Convention)
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> **V3 responses always use random string IDs — never database integers.**
|
||||||
|
|
||||||
|
After a successful `POST` create or any `GET`, the response contains:
|
||||||
|
|
||||||
|
| Field | Type | Use |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `{obj_type}_id` | `string` | **Primary public ID.** Use this for subsequent `PATCH` calls and UI routing. |
|
||||||
|
| `{obj_type}_id_random` | `string` | Legacy alias. Same value as `{obj_type}_id`. Present for backward compat only. |
|
||||||
|
|
||||||
|
**Example — create then immediately PATCH:**
|
||||||
|
```ts
|
||||||
|
const created = await postArchiveContent(archiveId, payload);
|
||||||
|
const newId = created.data.archive_content_id; // random string e.g. "xK9mP3qRtL2"
|
||||||
|
|
||||||
|
// Use it directly in the PATCH URL — no lookup needed
|
||||||
|
await patchArchiveContent(newId, { name: 'Updated Name' });
|
||||||
|
// PATCH /v3/crud/archive/{archive_id}/archive_content/{newId}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note on `_id_random` suffix:** The `{obj_type}_id_random` field is a legacy artifact from the pre-Vision model. Once you confirm `{obj_type}_id` is a random string (length 11–22), you do not need `_id_random` as a fallback. New code should only read `{obj_type}_id`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. V3 Uniform Lookup System
|
## 4. V3 Uniform Lookup System
|
||||||
|
|||||||
Reference in New Issue
Block a user