fix: exclude account_id and virtual fields from archive_content DB writes
- Adds fields_to_exclude_from_db to Archive_Content_Base to prevent SQL errors on non-existent columns. - Updates documentation for V3 Create/Update patterns and the x-ae-ignore-extra-fields header. - Propagates account_id_random to hosted file and media processing methods.
This commit is contained in:
@@ -272,6 +272,8 @@ async def save_file(
|
|||||||
|
|
||||||
file_info: dict = {}
|
file_info: dict = {}
|
||||||
file_info['saved'] = None
|
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_type'] = link_to_type
|
||||||
file_info['link_to_id'] = link_to_id
|
file_info['link_to_id'] = link_to_id
|
||||||
file_info['link_to_id_random'] = link_to_id_random
|
file_info['link_to_id_random'] = link_to_id_random
|
||||||
@@ -344,6 +346,8 @@ async def save_file_to_hosted_file(
|
|||||||
account_id: int,
|
account_id: int,
|
||||||
link_to_type: str,
|
link_to_type: str,
|
||||||
link_to_id: int,
|
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.setLevel(logging.DEBUG) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
|
||||||
log.debug(locals())
|
log.debug(locals())
|
||||||
@@ -353,8 +357,11 @@ async def save_file_to_hosted_file(
|
|||||||
|
|
||||||
file_info: dict = {}
|
file_info: dict = {}
|
||||||
file_info['saved'] = None
|
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_type'] = link_to_type
|
||||||
file_info['link_to_id'] = link_to_id
|
file_info['link_to_id'] = link_to_id
|
||||||
|
file_info['link_to_id_random'] = link_to_id_random
|
||||||
file_info['filename'] = filename
|
file_info['filename'] = filename
|
||||||
file_info['extension'] = extension
|
file_info['extension'] = extension
|
||||||
file_info['content_type'] = mimetypes.guess_type(filename)[0]
|
file_info['content_type'] = mimetypes.guess_type(filename)[0]
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ async def clip_video_method(
|
|||||||
account_id: int,
|
account_id: int,
|
||||||
link_to_type: str,
|
link_to_type: str,
|
||||||
link_to_id: int,
|
link_to_id: int,
|
||||||
|
account_id_random: str = None,
|
||||||
filename_no_ext: str = 'automated_hosted_file_clip_video',
|
filename_no_ext: str = 'automated_hosted_file_clip_video',
|
||||||
to_type: str = 'mp4',
|
to_type: str = 'mp4',
|
||||||
reencode: bool = False,
|
reencode: bool = False,
|
||||||
@@ -66,6 +67,7 @@ async def clip_video_method(
|
|||||||
filename = new_filename,
|
filename = new_filename,
|
||||||
extension = to_type,
|
extension = to_type,
|
||||||
account_id = account_id,
|
account_id = account_id,
|
||||||
|
account_id_random = account_id_random,
|
||||||
link_to_type = link_to_type,
|
link_to_type = link_to_type,
|
||||||
link_to_id = link_to_id,
|
link_to_id = link_to_id,
|
||||||
)
|
)
|
||||||
@@ -87,6 +89,7 @@ async def convert_file_method(
|
|||||||
link_to_type: str,
|
link_to_type: str,
|
||||||
link_to_id: int,
|
link_to_id: int,
|
||||||
account_id: int,
|
account_id: int,
|
||||||
|
account_id_random: str = None,
|
||||||
filename_no_ext: str = 'automated_hosted_file_conversion',
|
filename_no_ext: str = 'automated_hosted_file_conversion',
|
||||||
to_type: str = 'webp',
|
to_type: str = 'webp',
|
||||||
):
|
):
|
||||||
@@ -115,6 +118,7 @@ async def convert_file_method(
|
|||||||
filename = f'{filename_no_ext}.{to_type}',
|
filename = f'{filename_no_ext}.{to_type}',
|
||||||
extension = to_type,
|
extension = to_type,
|
||||||
account_id = account_id,
|
account_id = account_id,
|
||||||
|
account_id_random = account_id_random,
|
||||||
link_to_type = link_to_type,
|
link_to_type = link_to_type,
|
||||||
link_to_id = link_to_id,
|
link_to_id = link_to_id,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import datetime, pytz
|
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 pydantic import BaseModel, EmailStr, Field, Json, PrivateAttr, ValidationError, validator
|
||||||
|
|
||||||
from app.db_sql import redis_lookup_id_random
|
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_content_type: Optional[str]
|
||||||
hosted_file_size: 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)
|
_processed_at: datetime.datetime = PrivateAttr(default_factory=datetime.datetime.now)
|
||||||
|
|
||||||
@validator('id', always=True)
|
@validator('id', always=True)
|
||||||
|
|||||||
@@ -1323,6 +1323,20 @@ def post_obj_template(
|
|||||||
table_name_select = obj_type_kv_li[obj_type]['table_name']
|
table_name_select = obj_type_kv_li[obj_type]['table_name']
|
||||||
base_name = obj_type_kv_li[obj_type]['base_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):
|
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.setLevel(logging.INFO) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
|
||||||
log.debug(sql_insert_result)
|
log.debug(sql_insert_result)
|
||||||
|
|||||||
@@ -276,6 +276,7 @@ async def convert_file(
|
|||||||
link_to_type = link_to_type,
|
link_to_type = link_to_type,
|
||||||
link_to_id = lid_int,
|
link_to_id = lid_int,
|
||||||
account_id = commons.x_account_id,
|
account_id = commons.x_account_id,
|
||||||
|
account_id_random = commons.x_account_id_random,
|
||||||
filename_no_ext = filename_no_ext,
|
filename_no_ext = filename_no_ext,
|
||||||
to_type = to_type
|
to_type = to_type
|
||||||
)
|
)
|
||||||
@@ -306,6 +307,7 @@ async def clip_video(
|
|||||||
start_time = start_time,
|
start_time = start_time,
|
||||||
end_time = end_time,
|
end_time = end_time,
|
||||||
account_id = commons.x_account_id,
|
account_id = commons.x_account_id,
|
||||||
|
account_id_random = commons.x_account_id_random,
|
||||||
link_to_type = link_to_type,
|
link_to_type = link_to_type,
|
||||||
link_to_id = lid_int,
|
link_to_id = lid_int,
|
||||||
filename_no_ext = filename_no_ext,
|
filename_no_ext = filename_no_ext,
|
||||||
|
|||||||
@@ -58,6 +58,16 @@ The primary way to retrieve data.
|
|||||||
* **Endpoint:** `POST /v3/crud/{obj_type}/search`
|
* **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.
|
* **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
|
## 4. V3 Uniform Lookup System
|
||||||
|
|||||||
Reference in New Issue
Block a user