1. Added 'entry' alias for 'journal_entry' in object definitions.\n2. Updated nested router to resolve physical table names from the registry before ID resolution.\n3. Updated ID resolution helpers to recognize 'entry' prefix.\nThis resolves 404 errors when using shorter aliases in nested paths (e.g., /journal/{id}/entry/).
249 lines
10 KiB
Python
249 lines
10 KiB
Python
"""
|
|
Redis-based ID resolution and caching helpers for Aether.
|
|
"""
|
|
import datetime
|
|
import random
|
|
import redis
|
|
import logging
|
|
|
|
from app.config import settings
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
def redis_lookup_id_random(
|
|
record_id_random: int|str,
|
|
table_name: str,
|
|
check_int_id: bool = False,
|
|
log_lvl: int = logging.WARNING, # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
|
|
minutes: int = 30, # Expire the Redis key after 8 minutes
|
|
reset_rate: int = 10, # 1 in 10 chance of resetting the Redis key
|
|
) -> str|int|bool|None:
|
|
"""
|
|
Looks up a record ID in Redis, falling back to SQL if not found.
|
|
Resolves 'id_random' (URL-safe string) to internal integer 'id'.
|
|
"""
|
|
from app.db_sql import sql_select, get_id_random
|
|
log.setLevel(log_lvl)
|
|
|
|
if isinstance(record_id_random, str) and len(record_id_random) >= 11 and len(record_id_random) <= 22: pass
|
|
elif isinstance(record_id_random, int):
|
|
record_id = record_id_random
|
|
if check_int_id:
|
|
log.info(f'Checking the int ID if exists. Table Name: {table_name} ID: {record_id}')
|
|
if get_id_random_result := get_id_random(
|
|
record_id = record_id,
|
|
table_name = table_name,
|
|
):
|
|
log.info(f'The int ID exists. Returning the int ID. ID Random: {get_id_random_result}')
|
|
return record_id
|
|
else:
|
|
log.info(f'The int ID does not exists. Returning False. Table Name: {table_name} ID: {record_id}')
|
|
return False
|
|
else:
|
|
log.debug(f'Not checking if the int ID exists. Returning the int ID. ID: {record_id}')
|
|
return record_id
|
|
elif record_id_random is None:
|
|
log.info(f'No record ID was passed. Returning None')
|
|
return None
|
|
else:
|
|
log.error(f'Unexpected data type or string format: {type(record_id_random)} Expected type is a string 11 or 22 characters long.')
|
|
return False
|
|
|
|
if record_id_random and table_name:
|
|
if len(record_id_random) < 11:
|
|
log.error(f'The length of id_random is too short: {record_id_random} ({len(record_id_random)} chars)')
|
|
return False
|
|
elif len(record_id_random) > 22:
|
|
log.error(f'The length of id_random is too long: {record_id_random} ({len(record_id_random)} chars)')
|
|
return False
|
|
elif record_id_random:
|
|
log.error(f'Missing table_name to select from for id_random "{record_id_random}"')
|
|
return False
|
|
elif table_name:
|
|
log.error(f'Missing id_random to select from table "{table_name}"')
|
|
return False
|
|
else:
|
|
log.error('Missing table_name and record_id_random')
|
|
return False
|
|
|
|
r = redis.Redis(host=settings.REDIS['server'], port=settings.REDIS['port'], db=7, password=None, decode_responses=True)
|
|
key_name = f'{table_name}:{record_id_random}'
|
|
|
|
record_id = r.get(key_name)
|
|
|
|
if record_id and random.randint(1, reset_rate) == 1:
|
|
log.warning(f'Redis: Randomly (1/{reset_rate}) setting record_id to None. Key="{key_name}" value="{record_id}" TTL={r.ttl(key_name)} seconds')
|
|
record_id = None
|
|
|
|
if record_id:
|
|
r.setex(key_name, datetime.timedelta(minutes=minutes), value=record_id)
|
|
log.info(f'Redis: Entry found for: Key="{key_name}" value="{record_id}" TTL={r.ttl(key_name)} seconds')
|
|
return int(record_id)
|
|
elif table_name:
|
|
data = { 'id_random': record_id_random }
|
|
sql = f"SELECT id FROM `{table_name}` AS `table` WHERE `table`.id_random = :id_random;"
|
|
|
|
if select_results := sql_select(sql=sql, data=data):
|
|
log.debug(f'SQL: SELECT result: {select_results}')
|
|
if isinstance(select_results, dict):
|
|
log.info(f"""SQL: Found ID Random for: {str(record_id_random)} = {str(select_results.get('id'))}""")
|
|
if record_id := select_results.get('id'):
|
|
r.setex(key_name, datetime.timedelta(minutes=minutes), value=record_id)
|
|
return int(record_id)
|
|
else:
|
|
log.error('The SQL result was not what was expected. The ID field was not found.')
|
|
return False
|
|
else:
|
|
log.error(f'SQL: More than one record found in "{table_name}". Duplicate id_random!')
|
|
return redis_lookup_id_random(record_id_random=record_id_random, table_name=table_name)
|
|
else:
|
|
log.warning(f'SQL: ID Random "{record_id_random}" not found in "{table_name}". Returning None.')
|
|
return None
|
|
|
|
log.error('Unexpected state in redis_lookup_id_random.')
|
|
return False
|
|
|
|
|
|
def get_id_random(
|
|
record_id: int,
|
|
table_name: str,
|
|
log_lvl: int = logging.WARNING, # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
|
|
) -> str|bool|None:
|
|
"""
|
|
Looks up the 'id_random' for a given internal integer ID.
|
|
"""
|
|
from app.db_sql import sql_select
|
|
log.setLevel(log_lvl)
|
|
|
|
data = { 'id': record_id }
|
|
sql = f"SELECT id_random FROM `{table_name}` AS `table` WHERE `table`.id = :id;"
|
|
|
|
if select_results := sql_select(sql=sql, data=data):
|
|
if isinstance(select_results, dict):
|
|
if record_id_random := select_results.get('id_random'):
|
|
return str(record_id_random)
|
|
else:
|
|
log.error('The SQL result was not what was expected.')
|
|
return False
|
|
elif isinstance(select_results, list):
|
|
log.exception('More than one record may have been found. Duplicate ID!')
|
|
return False
|
|
else:
|
|
log.exception(f'Got an unexpected result while trying to look up the ID.')
|
|
return False
|
|
elif select_results is None:
|
|
return None
|
|
else:
|
|
return False
|
|
|
|
|
|
def reset_redis():
|
|
"""Flushes the Redis database used for ID caching."""
|
|
r = redis.Redis(host=settings.REDIS['server'], port=settings.REDIS['port'], db=7, password=None, decode_responses=True)
|
|
r.flushdb()
|
|
return True
|
|
|
|
|
|
def lookup_id_random_pop(
|
|
obj_data: dict,
|
|
log_lvl: int = logging.WARNING, # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
|
|
):
|
|
"""
|
|
Look up and resolve id_random values to their id
|
|
Remove the unneeded *_id_random key from the dict
|
|
"""
|
|
log.setLevel(log_lvl)
|
|
|
|
# Common prefixes for ID resolution
|
|
id_prefixes = [
|
|
'account', 'activity_log', 'address', 'address_location', 'archive',
|
|
'contact', 'contact_1', 'contact_2', 'cont_edu_cert', 'cont_edu_cert_person',
|
|
'entry', 'event', 'event_id_random_only', 'event_abstract', 'event_badge',
|
|
'event_badge_template', 'event_exhibit', 'event_file', 'event_location',
|
|
'event_person', 'event_person_profile', 'event_presentation',
|
|
'event_presenter', 'event_registration', 'event_session', 'event_track',
|
|
'grant', 'hosted_file', 'journal', 'journal_entry', 'membership_group',
|
|
'membership_person_group', 'membership_person', 'membership_type',
|
|
'membership_person_type', 'order', 'order_line', 'order_cart',
|
|
'order_cart_line', 'organization', 'page', 'person', 'poc_event_person',
|
|
'poc_person', 'post', 'product', 'sponsorship', 'sponsorship_cfg',
|
|
'site', 'user'
|
|
]
|
|
|
|
for prefix in id_prefixes:
|
|
key_random = f'{prefix}_id_random'
|
|
key_id = f'{prefix}_id'
|
|
|
|
# Table name mapping
|
|
table = prefix
|
|
if prefix == 'address_location': table = 'address'
|
|
elif prefix in ['contact_1', 'contact_2']: table = 'contact'
|
|
elif prefix == 'entry': table = 'journal_entry'
|
|
elif prefix == 'event_id_random_only': table = 'event'
|
|
elif prefix == 'poc_event_person': table = 'event_person'
|
|
elif prefix == 'poc_person': table = 'person'
|
|
|
|
resolved_id = None
|
|
|
|
# Scenario A: Legacy suffix (e.g., account_id_random: "abc")
|
|
if key_random in obj_data:
|
|
resolved_id = redis_lookup_id_random(record_id_random=obj_data[key_random], table_name=table)
|
|
obj_data.pop(key_random)
|
|
|
|
# Scenario B: Vision naming (e.g., account_id: "abc")
|
|
# Only resolve if it's a string of the correct length (random ID format)
|
|
elif key_id in obj_data and isinstance(obj_data[key_id], str) and 11 <= len(obj_data[key_id]) <= 22:
|
|
resolved_id = redis_lookup_id_random(record_id_random=obj_data[key_id], table_name=table)
|
|
|
|
if resolved_id is not None:
|
|
# Set the target ID field
|
|
target_id_key = key_id
|
|
if prefix == 'event_id_random_only': target_id_key = 'event_id_only'
|
|
obj_data[target_id_key] = resolved_id
|
|
|
|
# Removed the short prefix version (e.g., obj_data['account'] = 1)
|
|
# as it causes 'Unknown column' errors in direct table inserts.
|
|
|
|
# Polymorphic links
|
|
polymorphic = [
|
|
('for_type', 'for_id_random', 'for_id'),
|
|
('link_to_type', 'link_to_id_random', 'link_to_id'),
|
|
('object_type', 'object_id_random', 'object_id'),
|
|
('to_object_type', 'to_object_id_random', 'to_object_id'),
|
|
('from_object_type', 'from_object_id_random', 'from_object_id')
|
|
]
|
|
|
|
for type_key, rand_key, id_key in polymorphic:
|
|
# Handle random key if present
|
|
if type_key in obj_data and rand_key in obj_data:
|
|
obj_data[id_key] = redis_lookup_id_random(
|
|
record_id_random=obj_data.get(rand_key),
|
|
table_name=obj_data.get(type_key)
|
|
)
|
|
obj_data.pop(rand_key)
|
|
# Handle Vision naming (id_key contains the string)
|
|
elif type_key in obj_data and id_key in obj_data and isinstance(obj_data[id_key], str) and 11 <= len(obj_data[id_key]) <= 22:
|
|
obj_data[id_key] = redis_lookup_id_random(
|
|
record_id_random=obj_data.get(id_key),
|
|
table_name=obj_data.get(type_key)
|
|
)
|
|
|
|
return obj_data
|
|
|
|
|
|
def get_account_id_w_for_type_id(
|
|
for_type: str, # This is the table name
|
|
for_id: int|str,
|
|
) -> bool|int|None:
|
|
"""Helper to find an account_id associated with an object."""
|
|
from app.db_sql import sql_select
|
|
log.setLevel(logging.WARNING)
|
|
|
|
if fid := redis_lookup_id_random(record_id_random=for_id, table_name=for_type):
|
|
data = {'for_id': fid}
|
|
sql = f"SELECT account_id FROM `{for_type}` WHERE id = :for_id LIMIT 1;"
|
|
if result := sql_select(data=data, sql=sql):
|
|
return result.get('account_id')
|
|
return False
|
|
return None
|