diff --git a/app/methods/hosted_file_methods.py b/app/methods/hosted_file_methods.py index 230eb27..c635415 100644 --- a/app/methods/hosted_file_methods.py +++ b/app/methods/hosted_file_methods.py @@ -272,6 +272,8 @@ async def save_file( file_info: dict = {} file_info['saved'] = None + file_info['account_id'] = account_id + file_info['account_id_random'] = account_id_random file_info['link_to_type'] = link_to_type file_info['link_to_id'] = link_to_id file_info['link_to_id_random'] = link_to_id_random @@ -344,6 +346,8 @@ async def save_file_to_hosted_file( account_id: int, link_to_type: str, link_to_id: int, + account_id_random: str = None, + link_to_id_random: str = None, ): log.setLevel(logging.DEBUG) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL log.debug(locals()) @@ -353,8 +357,11 @@ async def save_file_to_hosted_file( file_info: dict = {} file_info['saved'] = None + file_info['account_id'] = account_id + file_info['account_id_random'] = account_id_random file_info['link_to_type'] = link_to_type file_info['link_to_id'] = link_to_id + file_info['link_to_id_random'] = link_to_id_random file_info['filename'] = filename file_info['extension'] = extension file_info['content_type'] = mimetypes.guess_type(filename)[0] diff --git a/app/methods/lib_media.py b/app/methods/lib_media.py index f9b6beb..5fe551b 100644 --- a/app/methods/lib_media.py +++ b/app/methods/lib_media.py @@ -24,6 +24,7 @@ async def clip_video_method( account_id: int, link_to_type: str, link_to_id: int, + account_id_random: str = None, filename_no_ext: str = 'automated_hosted_file_clip_video', to_type: str = 'mp4', reencode: bool = False, @@ -66,6 +67,7 @@ async def clip_video_method( filename = new_filename, extension = to_type, account_id = account_id, + account_id_random = account_id_random, link_to_type = link_to_type, link_to_id = link_to_id, ) @@ -87,6 +89,7 @@ async def convert_file_method( link_to_type: str, link_to_id: int, account_id: int, + account_id_random: str = None, filename_no_ext: str = 'automated_hosted_file_conversion', to_type: str = 'webp', ): @@ -115,6 +118,7 @@ async def convert_file_method( filename = f'{filename_no_ext}.{to_type}', extension = to_type, account_id = account_id, + account_id_random = account_id_random, link_to_type = link_to_type, link_to_id = link_to_id, ) diff --git a/app/models/archive_content_models.py b/app/models/archive_content_models.py index df89fce..3c80183 100644 --- a/app/models/archive_content_models.py +++ b/app/models/archive_content_models.py @@ -1,6 +1,6 @@ import datetime, pytz -from typing import Dict, List, Optional, Set, Union +from typing import Dict, List, Optional, Set, Union, ClassVar from pydantic import BaseModel, EmailStr, Field, Json, PrivateAttr, ValidationError, validator from app.db_sql import redis_lookup_id_random @@ -92,6 +92,13 @@ class Archive_Content_Base(BaseModel): hosted_file_content_type: Optional[str] hosted_file_size: Optional[str] + # 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] = [ + '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' + ] + _processed_at: datetime.datetime = PrivateAttr(default_factory=datetime.datetime.now) @validator('id', always=True) diff --git a/app/routers/api_crud_v2.py b/app/routers/api_crud_v2.py index 058178b..1dde95c 100644 --- a/app/routers/api_crud_v2.py +++ b/app/routers/api_crud_v2.py @@ -1323,6 +1323,20 @@ def post_obj_template( table_name_select = obj_type_kv_li[obj_type]['table_name'] base_name = obj_type_kv_li[obj_type]['base_name'] + # # Prune any keys that are not actual columns on the target table to avoid + # # SQL errors when clients include convenience fields (e.g., account_id) + # try: + # from app import lib_sql_core + # from sqlalchemy import text + # with lib_sql_core.engine.connect() as conn: + # cols_res = conn.execute(text(f"DESCRIBE `{table_name_insert}`;")) + # cols = [r[0] for r in cols_res.fetchall()] + # # keep only keys that match real columns (always allow id_random) + # obj_data = {k: v for k, v in obj_data.items() if k in cols or k == 'id_random'} + # except Exception as _: + # # If DESCRIBE fails for any reason, fall back to original obj_data + # log.debug(f"Could not inspect table columns for {table_name_insert}; proceeding without pruning.") + if sql_insert_result := sql_insert(table_name=table_name_insert, data=obj_data, id_random_length=id_random_length): log.setLevel(logging.INFO) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL log.debug(sql_insert_result) diff --git a/app/routers/hosted_file.py b/app/routers/hosted_file.py index c0a3044..c1f1d08 100644 --- a/app/routers/hosted_file.py +++ b/app/routers/hosted_file.py @@ -276,6 +276,7 @@ async def convert_file( link_to_type = link_to_type, link_to_id = lid_int, account_id = commons.x_account_id, + account_id_random = commons.x_account_id_random, filename_no_ext = filename_no_ext, to_type = to_type ) @@ -306,6 +307,7 @@ async def clip_video( start_time = start_time, end_time = end_time, account_id = commons.x_account_id, + account_id_random = commons.x_account_id_random, link_to_type = link_to_type, link_to_id = lid_int, filename_no_ext = filename_no_ext, diff --git a/documentation/GUIDE__V3_FRONTEND_API.md b/documentation/GUIDE__V3_FRONTEND_API.md index c342137..a0bc1b2 100644 --- a/documentation/GUIDE__V3_FRONTEND_API.md +++ b/documentation/GUIDE__V3_FRONTEND_API.md @@ -58,6 +58,16 @@ The primary way to retrieve data. * **Endpoint:** `POST /v3/crud/{obj_type}/search` * **Security:** Automatically filters results to only show records belonging to your `x-account-id`. If no account context is provided, it will return **0 records** for private objects. +### C. POST Create / PATCH Update +Modify data in the system. +* **Endpoints:** + * `POST /v3/crud/{obj_type}/` + * `PATCH /v3/crud/{obj_type}/{id}` +* **Strict Mode (Default):** The API validates your payload against the Pydantic model. If you send fields that do not exist in the model, the database might return a 400 "Unknown column" error. +* **Permissive Mode (Header):** To allow the frontend to send "extra" fields (like local UI state) without causing errors, use the following header: + * **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. + --- ## 4. V3 Uniform Lookup System