fix(v3-vision): platform-wide hardening of ID Vision models

Hardened root_validators in Journal, Post, Page, and Hosted File models to use Union[int, str] for Vision ID fields. This prevents resolved integer IDs from being deleted during CREATE/UPDATE operations, resolving a critical regression found during Post Comment bug fixing.
This commit is contained in:
Scott Idem
2026-02-05 20:38:19 -05:00
parent 78f04bca50
commit a7c82615ab
4 changed files with 64 additions and 65 deletions

View File

@@ -67,21 +67,24 @@ class Hosted_File_Base(BaseModel):
def map_v3_ids(cls, values):
"""
Vision Transformer:
Map DB keys to clean API keys and strip internal integers.
Map DB keys to clean API keys and strip internal integers during READ operations.
During CREATE (POST) operations, we ensure resolved integers are preserved.
"""
# 1. Capture the random ID string
# 1. Map Random Strings to Clean Names
rid = values.get('id_random') or values.get('hosted_file_id_random')
# 2. Map Random Strings to Clean Names for the Frontend
# We always want the string version in 'id' and 'hosted_file_id' for the API response
if rid:
if rid and isinstance(rid, str):
values['id'] = rid
values['hosted_file_id'] = rid
if a_rid := values.get('account_id_random'):
# If we have a random account ID string, use it for the Vision API
values['account_id'] = a_rid
if a_rid := values.get('account_id_random'): values['account_id'] = a_rid
# 2. Prevent "Collision Population" or leakage of integers during API responses
for k in ['id', 'hosted_file_id', 'account_id']:
val = values.get(k)
if val is not None and not isinstance(val, str):
if values.get(f'{k}_random') or (k=='id' and values.get('id_random')):
del values[k]
return values
class Config:

View File

@@ -16,12 +16,12 @@ class Journal_Base(BaseModel):
log.setLevel(logging.INFO) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
log.debug(locals())
# --- Standardized Vision IDs (Strings) ---
id: Optional[str] = Field(None, **base_fields['journal_id_random'])
journal_id: Optional[str] = Field(None, **base_fields['journal_id_random'])
account_id: Optional[str] = Field(None, **base_fields['account_id_random'])
person_id: Optional[str] = Field(None, **base_fields['person_id_random'])
user_id: Optional[str] = Field(None, **base_fields['user_id_random'])
# --- Standardized Vision IDs (Strings for API, Integers for DB) ---
id: Optional[Union[int, str]] = Field(None, **base_fields['journal_id_random'])
journal_id: Optional[Union[int, str]] = Field(None, **base_fields['journal_id_random'])
account_id: Optional[Union[int, str]] = Field(None, **base_fields['account_id_random'])
person_id: Optional[Union[int, str]] = Field(None, **base_fields['person_id_random'])
user_id: Optional[Union[int, str]] = Field(None, **base_fields['user_id_random'])
external_id: Optional[str] # ID generated by or for external systems (should be stable and not change)
import_id: Optional[str] # Used for import purposes to track the source of the data
@@ -132,24 +132,25 @@ class Journal_Base(BaseModel):
def map_v3_ids(cls, values):
"""
Vision Transformer:
Map DB keys to clean API keys and strip internal integers.
Map DB keys to clean API keys and strip internal integers during READ operations.
During CREATE (POST) operations, we ensure resolved integers are preserved.
"""
# 1. Map Random Strings to Clean Names
if rid := values.get('id_random') or values.get('journal_id_random'):
rid = values.get('id_random') or values.get('journal_id_random')
if rid and isinstance(rid, str):
values['id'] = rid
values['journal_id'] = rid
if a_rid := values.get('account_id_random'):
values['account_id'] = a_rid
if p_rid := values.get('person_id_random'):
values['person_id'] = p_rid
if u_rid := values.get('user_id_random'):
values['user_id'] = u_rid
if a_rid := values.get('account_id_random'): values['account_id'] = a_rid
if p_rid := values.get('person_id_random'): values['person_id'] = p_rid
if u_rid := values.get('user_id_random'): values['user_id'] = u_rid
# 2. Prevent "Collision Population"
# 2. Prevent "Collision Population" or leakage of integers during API responses
for k in ['id', 'journal_id', 'account_id', 'person_id', 'user_id']:
if k in values and not isinstance(values[k], str):
del values[k]
val = values.get(k)
if val is not None and not isinstance(val, str):
if values.get(f'{k}_random') or (k=='id' and values.get('id_random')):
del values[k]
return values

View File

@@ -14,19 +14,11 @@ class Page_Base(BaseModel):
log.setLevel(logging.WARNING) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
log.debug(locals())
# --- Standardized Vision IDs (Strings) ---
id: Optional[str] = Field(None, **base_fields['page_id_random'])
page_id: Optional[str] = Field(None, **base_fields['page_id_random'])
account_id: Optional[str] = Field(None, **base_fields['account_id_random'])
site_id: Optional[str] = Field(None, **base_fields['site_id_random'])
# page_id_random: Optional[str] = Field(
# **base_fields['page_id_random'],
# alias = 'page_id_random',
# )
# id: Optional[int] = Field(
# alias = 'page_id'
# )
# --- Standardized Vision IDs (Strings for API, Integers for DB) ---
id: Optional[Union[int, str]] = Field(None, **base_fields['page_id_random'])
page_id: Optional[Union[int, str]] = Field(None, **base_fields['page_id_random'])
account_id: Optional[Union[int, str]] = Field(None, **base_fields['account_id_random'])
site_id: Optional[Union[int, str]] = Field(None, **base_fields['site_id_random'])
code: Optional[str]
name: Optional[str]
@@ -69,22 +61,24 @@ class Page_Base(BaseModel):
def map_v3_ids(cls, values):
"""
Vision Transformer:
Map DB keys to clean API keys and strip internal integers.
Map DB keys to clean API keys and strip internal integers during READ operations.
During CREATE (POST) operations, we ensure resolved integers are preserved.
"""
# 1. Map Random Strings to Clean Names
if rid := values.get('id_random') or values.get('page_id_random'):
rid = values.get('id_random') or values.get('page_id_random')
if rid and isinstance(rid, str):
values['id'] = rid
values['page_id'] = rid
if a_rid := values.get('account_id_random'):
values['account_id'] = a_rid
if s_rid := values.get('site_id_random'):
values['site_id'] = s_rid
if a_rid := values.get('account_id_random'): values['account_id'] = a_rid
if s_rid := values.get('site_id_random'): values['site_id'] = s_rid
# 2. Prevent "Collision Population"
# 2. Prevent "Collision Population" or leakage of integers during API responses
for k in ['id', 'account_id', 'site_id']:
if k in values and not isinstance(values[k], str):
del values[k]
val = values.get(k)
if val is not None and not isinstance(val, str):
if values.get(f'{k}_random') or (k=='id' and values.get('id_random')):
del values[k]
return values

View File

@@ -16,12 +16,12 @@ class Post_Base(BaseModel):
log.setLevel(logging.WARNING) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
log.debug(locals())
# --- Standardized Vision IDs (Strings) ---
id: Optional[str] = Field(None, **base_fields['post_id_random'])
post_id: Optional[str] = Field(None, **base_fields['post_id_random'])
account_id: Optional[str] = Field(None, **base_fields['account_id_random'])
person_id: Optional[str] = Field(None, **base_fields['person_id_random'])
user_id: Optional[str] = Field(None, **base_fields['user_id_random'])
# --- Standardized Vision IDs (Strings for API, Integers for DB) ---
id: Optional[Union[int, str]] = Field(None, **base_fields['post_id_random'])
post_id: Optional[Union[int, str]] = Field(None, **base_fields['post_id_random'])
account_id: Optional[Union[int, str]] = Field(None, **base_fields['account_id_random'])
person_id: Optional[Union[int, str]] = Field(None, **base_fields['person_id_random'])
user_id: Optional[Union[int, str]] = Field(None, **base_fields['user_id_random'])
external_person_id: Optional[str] # Person ID generated by external system (should be stable and not change)
@@ -93,24 +93,25 @@ class Post_Base(BaseModel):
def map_v3_ids(cls, values):
"""
Vision Transformer:
Map DB keys to clean API keys and strip internal integers.
Map DB keys to clean API keys and strip internal integers during READ operations.
During CREATE (POST) operations, we ensure resolved integers are preserved.
"""
# 1. Map Random Strings to Clean Names
if rid := values.get('id_random') or values.get('post_id_random'):
rid = values.get('id_random') or values.get('post_id_random')
if rid and isinstance(rid, str):
values['id'] = rid
values['post_id'] = rid
if a_rid := values.get('account_id_random'):
values['account_id'] = a_rid
if p_rid := values.get('person_id_random'):
values['person_id'] = p_rid
if u_rid := values.get('user_id_random'):
values['user_id'] = u_rid
if a_rid := values.get('account_id_random'): values['account_id'] = a_rid
if p_rid := values.get('person_id_random'): values['person_id'] = p_rid
if u_rid := values.get('user_id_random'): values['user_id'] = u_rid
# 2. Prevent "Collision Population"
# 2. Prevent "Collision Population" or leakage of integers during API responses
for k in ['id', 'post_id', 'account_id', 'person_id', 'user_id']:
if k in values and not isinstance(values[k], str):
del values[k]
val = values.get(k)
if val is not None and not isinstance(val, str):
if values.get(f'{k}_random') or (k=='id' and values.get('id_random')):
del values[k]
return values