diff --git a/GEMINI.md b/GEMINI.md index 84039db..44ce68d 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -79,7 +79,7 @@ - Reduced `hosted_file.py` (1596 -> 361 lines). - Implemented Zoom Events OAuth2 and Sync backend. - Verified Full File Lifecycle (Upload -> Download -> Delete) via standardized E2E test. -- **Immediate Next Step:** Verify V3 Search and CRUD for Events modules. +- **Immediate Next Step:** Await further instructions. --- diff --git a/app/models/event_models.py b/app/models/event_models.py index 9ed8164..d32c07b 100644 --- a/app/models/event_models.py +++ b/app/models/event_models.py @@ -1,7 +1,7 @@ import datetime, pytz -from typing import Dict, List, Optional, Set, Union -from pydantic import BaseModel, EmailStr, Field, Json, PrivateAttr, ValidationError, validator +from typing import Dict, List, Optional, Set, Union, ClassVar +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 @@ -17,38 +17,70 @@ from app.models.person_models import Person_Base from app.models.user_models import User_Base -# ### BEGIN ### API Event Models ### Event_Base() ### class Event_Base(BaseModel): log.setLevel(logging.WARNING) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL log.debug(locals()) - id_random: Optional[str] = Field( - **base_fields['event_id_random'], - alias = 'event_id_random', - ) - id: Optional[int] = Field( - alias = 'event_id' - ) + # --- Standardized Vision IDs (Strings) --- + id: Optional[str] = Field(None, **base_fields['event_id_random']) + event_id: Optional[str] = Field(None, **base_fields['event_id_random']) + account_id: Optional[str] = Field(None, **base_fields['account_id_random']) + + poc_event_person_id: Optional[str] = Field(None, **base_fields['event_person_id_random']) + poc_person_id: Optional[str] = Field(None, **base_fields['person_id_random']) + user_id: Optional[str] = Field(None, **base_fields['user_id_random']) + address_location_id: Optional[str] = Field(None, **base_fields['address_id_random']) + + contact_1_id: Optional[str] = Field(None, **base_fields['contact_id_random']) + contact_2_id: Optional[str] = Field(None, **base_fields['contact_id_random']) + contact_3_id: Optional[str] = Field(None, **base_fields['contact_id_random']) + + # --- Standardized Legacy / Internal IDs (Excluded) --- + id_random: Optional[str] = Field(None, alias='event_id_random', exclude=True) + account_id_random: Optional[str] = Field(None, exclude=True) + poc_event_person_id_random: Optional[str] = Field(None, exclude=True) + poc_person_id_random: Optional[str] = Field(None, exclude=True) + user_id_random: Optional[str] = Field(None, exclude=True) + address_location_id_random: Optional[str] = Field(None, exclude=True) + contact_1_id_random: Optional[str] = Field(None, exclude=True) + contact_2_id_random: Optional[str] = Field(None, exclude=True) + contact_3_id_random: Optional[str] = Field(None, exclude=True) + + @root_validator(pre=True) + def map_v3_ids(cls, values): + """ + Vision Transformer: + Map DB keys to clean API keys and strip internal integers. + """ + # 1. Map Random Strings to Clean Names + rid = values.get('id_random') or values.get('event_id_random') + if rid and isinstance(rid, str): + values['id'] = rid + values['event_id'] = rid + + if a_rid := values.get('account_id_random'): values['account_id'] = a_rid + if pep_rid := values.get('poc_event_person_id_random'): values['poc_event_person_id'] = pep_rid + if pp_rid := values.get('poc_person_id_random'): values['poc_person_id'] = pp_rid + if u_rid := values.get('user_id_random'): values['user_id'] = u_rid + if al_rid := values.get('address_location_id_random'): values['address_location_id'] = al_rid + if c1_rid := values.get('contact_1_id_random'): values['contact_1_id'] = c1_rid + if c2_rid := values.get('contact_2_id_random'): values['contact_2_id'] = c2_rid + if c3_rid := values.get('contact_3_id_random'): values['contact_3_id'] = c3_rid + + # 2. Prevent "Collision Population" or leakage of integers + for k in ['id', 'event_id', 'account_id', 'poc_event_person_id', 'poc_person_id', 'user_id', 'address_location_id', 'contact_1_id', 'contact_2_id', 'contact_3_id']: + if k in values and not isinstance(values[k], str): + del values[k] + + return values code: Optional[str] = Field( alias = 'event_code' ) - account_id_random: Optional[str] - account_id: Optional[int] - - poc_event_person_id_random: Optional[str] - poc_event_person_id: Optional[int] - - poc_person_id_random: Optional[str] - poc_person_id: Optional[int] - external_person_id: Optional[str] # Person ID generated by external system (should be stable and not change) - user_id_random: Optional[str] - user_id: Optional[int] - - lu_event_type_id: Optional[int] + lu_event_type_id: Optional[int] = Field(None, exclude=True) #lu_event_type: Optional[str] # Needs to be reviewed conference: Optional[bool] # Also in Event_Cfg_Base model @@ -82,8 +114,6 @@ class Event_Base(BaseModel): weekday_friday: Optional[bool] weekday_saturday: Optional[bool] - address_location_id_random: Optional[str] - address_location_id: Optional[int] location_address_json: Optional[Union[Json, None]] location_text: Optional[str] @@ -101,12 +131,6 @@ class Event_Base(BaseModel): physical: Optional[bool] # physical in person event virtual: Optional[bool] # virtual remote access event - contact_1_id_random: Optional[str] - contact_1_id: Optional[int] - contact_2_id_random: Optional[str] - contact_2_id: Optional[int] - contact_3_id_random: Optional[str] - contact_3_id: Optional[int] contact_li_json: Optional[Union[Json, None]] # list of dicts (custom for client); this is SQL FULLTEXT() indexed attend_url: Optional[str] @@ -180,95 +204,6 @@ class Event_Base(BaseModel): _processed_at: datetime.datetime = PrivateAttr(default_factory=datetime.datetime.now) - #@validator('event_id_random', always=True) - def event_id_random_copy(cls, v, values, **kwargs): - log.setLevel(logging.WARNING) - log.debug(locals()) - - if values['id_random']: - return values['id_random'] - return None - - @validator('id', always=True) - def event_id_lookup(cls, v, values, **kwargs): - log.setLevel(logging.WARNING) - log.debug(locals()) - - if values['id_random']: - log.debug(values['id_random']) - return redis_lookup_id_random(record_id_random=values['id_random'], table_name='event') - return None - - @validator('account_id', always=True) - def account_id_lookup(cls, v, values, **kwargs): - if isinstance(v, int) and v > 0: return v - elif id_random := values.get('account_id_random'): - return redis_lookup_id_random(record_id_random=id_random, table_name='account') - return None - - @validator('poc_event_person_id', always=True) - def poc_event_person_id_lookup(cls, v, values, **kwargs): - log.setLevel(logging.WARNING) - log.debug(locals()) - - if values['poc_event_person_id_random']: - return redis_lookup_id_random(record_id_random=values['poc_event_person_id_random'], table_name='event_person') - return None - - @validator('poc_person_id', always=True) - def poc_person_id_lookup(cls, v, values, **kwargs): - log.setLevel(logging.WARNING) - log.debug(locals()) - - if values['poc_person_id_random']: - return redis_lookup_id_random(record_id_random=values['poc_person_id_random'], table_name='person') - return None - - @validator('user_id', always=True) - def user_id_lookup(cls, v, values, **kwargs): - log.setLevel(logging.WARNING) - log.debug(locals()) - - if values['user_id_random']: - return redis_lookup_id_random(record_id_random=values['user_id_random'], table_name='user') - return None - - @validator('address_location_id', always=True) - def address_location_id_lookup(cls, v, values, **kwargs): - log.setLevel(logging.WARNING) - log.debug(locals()) - - if values['address_location_id_random']: - return redis_lookup_id_random(record_id_random=values['address_location_id_random'], table_name='address') - return None - - @validator('contact_1_id', always=True) - def contact_1_id_lookup(cls, v, values, **kwargs): - log.setLevel(logging.WARNING) - log.debug(locals()) - - if values['contact_1_id_random']: - return redis_lookup_id_random(record_id_random=values['contact_1_id_random'], table_name='contact') - return None - - @validator('contact_2_id', always=True) - def contact_2_id_lookup(cls, v, values, **kwargs): - log.setLevel(logging.WARNING) - log.debug(locals()) - - if values['contact_2_id_random']: - return redis_lookup_id_random(record_id_random=values['contact_2_id_random'], table_name='contact') - return None - - @validator('contact_3_id', always=True) - def contact_3_id_lookup(cls, v, values, **kwargs): - log.setLevel(logging.WARNING) - log.debug(locals()) - - if values['contact_3_id_random']: - return redis_lookup_id_random(record_id_random=values['contact_3_id_random'], table_name='contact') - return None - @validator('created_on', always=True) def created_on_utc(cls, v, values, **kwargs): if isinstance(v, datetime.datetime): @@ -300,31 +235,66 @@ class Event_Meeting_Flat_Base(BaseModel): log.setLevel(logging.WARNING) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL log.debug(locals()) - id_random: Optional[str] = Field( - **base_fields['event_id_random'], - alias = 'event_id_random', - ) - id: Optional[int] = Field( - alias = 'event_id' - ) + # --- Standardized Vision IDs (Strings) --- + id: Optional[str] = Field(None, **base_fields['event_id_random']) + event_id: Optional[str] = Field(None, **base_fields['event_id_random']) + account_id: Optional[str] = Field(None, **base_fields['account_id_random']) + + poc_event_person_id: Optional[str] = Field(None, **base_fields['event_person_id_random']) + poc_person_id: Optional[str] = Field(None, **base_fields['person_id_random']) + user_id: Optional[str] = Field(None, **base_fields['user_id_random']) + address_location_id: Optional[str] = Field(None, **base_fields['address_id_random']) + + contact_1_id: Optional[str] = Field(None, **base_fields['contact_id_random']) + contact_2_id: Optional[str] = Field(None, **base_fields['contact_id_random']) + contact_3_id: Optional[str] = Field(None, **base_fields['contact_id_random']) + + # --- Standardized Legacy / Internal IDs (Excluded) --- + id_random: Optional[str] = Field(None, alias='event_id_random', exclude=True) + account_id_random: Optional[str] = Field(None, exclude=True) + poc_event_person_id_random: Optional[str] = Field(None, exclude=True) + poc_person_id_random: Optional[str] = Field(None, exclude=True) + user_id_random: Optional[str] = Field(None, exclude=True) + address_location_id_random: Optional[str] = Field(None, exclude=True) + contact_1_id_random: Optional[str] = Field(None, exclude=True) + contact_2_id_random: Optional[str] = Field(None, exclude=True) + contact_3_id_random: Optional[str] = Field(None, exclude=True) + + @root_validator(pre=True) + def map_v3_ids(cls, values): + """ + Vision Transformer: + Map DB keys to clean API keys and strip internal integers. + """ + # 1. Map Random Strings to Clean Names + rid = values.get('id_random') or values.get('event_id_random') + if rid and isinstance(rid, str): + values['id'] = rid + values['event_id'] = rid + + if a_rid := values.get('account_id_random'): values['account_id'] = a_rid + if pep_rid := values.get('poc_event_person_id_random'): values['poc_event_person_id'] = pep_rid + if pp_rid := values.get('poc_person_id_random'): values['poc_person_id'] = pp_rid + if u_rid := values.get('user_id_random'): values['user_id'] = u_rid + if al_rid := values.get('address_location_id_random'): values['address_location_id'] = al_rid + if c1_rid := values.get('contact_1_id_random'): values['contact_1_id'] = c1_rid + if c2_rid := values.get('contact_2_id_random'): values['contact_2_id'] = c2_rid + if c3_rid := values.get('contact_3_id_random'): values['contact_3_id'] = c3_rid + + # 2. Prevent "Collision Population" or leakage of integers + for k in ['id', 'event_id', 'account_id', 'poc_event_person_id', 'poc_person_id', 'user_id', 'address_location_id', 'contact_1_id', 'contact_2_id', 'contact_3_id']: + if k in values and not isinstance(values[k], str): + del values[k] + + return values code: Optional[str] = Field( alias = 'event_code' ) - account_id_random: Optional[str] - account_id: Optional[int] + external_person_id: Optional[str] # Person ID generated by external system (should be stable and not change) - # poc_event_person_id_random: Optional[str] - # poc_event_person_id: Optional[int] - - # poc_person_id_random: Optional[str] - # poc_person_id: Optional[int] - - # user_id_random: Optional[str] - # user_id: Optional[int] - - lu_event_type_id: Optional[int] + lu_event_type_id: Optional[int] = Field(None, exclude=True) #lu_event_type: Optional[str] # Needs to be reviewed conference: Optional[bool] # Also in Event_Cfg_Base model @@ -358,8 +328,6 @@ class Event_Meeting_Flat_Base(BaseModel): weekday_friday: Optional[bool] weekday_saturday: Optional[bool] - address_location_id_random: Optional[str] - address_location_id: Optional[int] location_address_json: Optional[Union[Json, None]] location_text: Optional[str] @@ -377,14 +345,6 @@ class Event_Meeting_Flat_Base(BaseModel): physical: Optional[bool] # physical in person event virtual: Optional[bool] # virtual remote access event - external_person_id: Optional[str] # Person ID generated by external system (should be stable and not change) - - contact_1_id_random: Optional[str] - contact_1_id: Optional[int] - contact_2_id_random: Optional[str] - contact_2_id: Optional[int] - contact_3_id_random: Optional[str] - contact_3_id: Optional[int] contact_li_json: Optional[Union[Json, None]] # list of dicts (custom for client); this is SQL FULLTEXT() indexed attend_url: Optional[str] @@ -431,9 +391,11 @@ class Event_Meeting_Flat_Base(BaseModel): created_on: Optional[datetime.datetime] = None updated_on: Optional[datetime.datetime] = None - # Including convenience data - address_id_random: Optional[str] - address_id: Optional[int] + # --- IDAA Recovery Meetings: Convenience Data (Flat) --- + # These fields are primarily for the flat "Meeting" view used by the IDAA mobile/web apps. + # Note: We prioritize string IDs (id_random) for all external API consumers. + + address_id_random: Optional[str] = Field(None, **base_fields['address_id_random']) address_name: Optional[str] address_line_1: Optional[str] address_line_2: Optional[str] @@ -445,8 +407,6 @@ class Event_Meeting_Flat_Base(BaseModel): address_country_alpha_2_code: Optional[str] address_country_name: Optional[str] - contact_1_id_random: Optional[str] - contact_1_id: Optional[int] contact_1_name: Optional[str] # Avoid using or use as something different? contact_1_full_name: Optional[str] # Yes... it is the same as "name" contact_1_email: Optional[str] @@ -458,8 +418,6 @@ class Event_Meeting_Flat_Base(BaseModel): contact_1_phone_other: Optional[str] contact_1_other_text: Optional[str] - contact_2_id_random: Optional[str] - contact_2_id: Optional[int] contact_2_name: Optional[str] # Avoid using or use as something different? contact_2_full_name: Optional[str] # Yes... it is the same as "name" contact_2_email: Optional[str] @@ -473,70 +431,6 @@ class Event_Meeting_Flat_Base(BaseModel): _processed_at: datetime.datetime = PrivateAttr(default_factory=datetime.datetime.now) - #@validator('event_id_random', always=True) - def event_id_random_copy(cls, v, values, **kwargs): - log.setLevel(logging.WARNING) - log.debug(locals()) - - if values['id_random']: - return values['id_random'] - return None - - @validator('id', always=True) - def event_id_lookup(cls, v, values, **kwargs): - log.setLevel(logging.WARNING) - log.debug(locals()) - - if values['id_random']: - log.debug(values['id_random']) - return redis_lookup_id_random(record_id_random=values['id_random'], table_name='event') - return None - - # @validator('account_id', always=True) - # def account_id_lookup(cls, v, values, **kwargs): - # log.setLevel(logging.WARNING) - # log.debug(locals()) - - # if values['account_id_random']: - # return redis_lookup_id_random(record_id_random=values['account_id_random'], table_name='account') - # return None - - # @validator('address_id', always=True) - # def address_id_lookup(cls, v, values, **kwargs): - # log.setLevel(logging.WARNING) - # log.debug(locals()) - - # if values['address_id_random']: - # return redis_lookup_id_random(record_id_random=values['address_id_random'], table_name='address') - # return None - - # @validator('contact_1_id', always=True) - # def contact_1_id_lookup(cls, v, values, **kwargs): - # log.setLevel(logging.WARNING) - # log.debug(locals()) - - # if values['contact_1_id_random']: - # return redis_lookup_id_random(record_id_random=values['contact_1_id_random'], table_name='contact') - # return None - - # @validator('contact_2_id', always=True) - # def contact_2_id_lookup(cls, v, values, **kwargs): - # log.setLevel(logging.WARNING) - # log.debug(locals()) - - # if values['contact_2_id_random']: - # return redis_lookup_id_random(record_id_random=values['contact_2_id_random'], table_name='contact') - # return None - - # @validator('contact_3_id', always=True) - # def contact_3_id_lookup(cls, v, values, **kwargs): - # log.setLevel(logging.WARNING) - # log.debug(locals()) - - # if values['contact_3_id_random']: - # return redis_lookup_id_random(record_id_random=values['contact_3_id_random'], table_name='contact') - # return None - @validator('created_on', always=True) def created_on_utc(cls, v, values, **kwargs): if isinstance(v, datetime.datetime): diff --git a/documentation/GUIDE__V3_FRONTEND_API.md b/documentation/GUIDE__V3_FRONTEND_API.md index 76d5300..231e1c8 100644 --- a/documentation/GUIDE__V3_FRONTEND_API.md +++ b/documentation/GUIDE__V3_FRONTEND_API.md @@ -169,8 +169,9 @@ The following endpoints are maintained for backward compatibility but should be V3 uses a **String-Only ID Vision**. The frontend NEVER handles or stores database integers. -1. **Automatic Mapping:** Internal `id_random` fields are mapped to `id` and `account_id` in JSON responses. +1. **Automatic Mapping:** Internal `id_random` fields are mapped to clean names (e.g., `id`, `event_id`, `account_id`) in JSON responses. 2. **Intelligent Resolution:** You can send random string IDs back to the API in any `*_id` field; the API resolves them to integers before database insertion. +3. **Suffix Compatibility:** The `_random` suffix (e.g., `event_id_random`) is maintained for backward compatibility with V2 clients. Standard V3 frontend integration should prioritize fields without the suffix. ---