diff --git a/app/routers/data_store.py b/app/routers/data_store.py index bfa3141..76f072b 100644 --- a/app/routers/data_store.py +++ b/app/routers/data_store.py @@ -141,6 +141,7 @@ async def get_v3_data_store_obj_w_code( data_store_code: str = Path(min_length=3, max_length=50), for_type: Optional[str] = Query(None, min_length=1, max_length=25), for_id: Optional[str] = Query(None, min_length=11, max_length=22), + limit: int = Query(1, ge=1, description="Number of results to return (Default: 1)"), account: AccountContext = Depends(get_account_context), serialization: SerializationParams = Depends(), @@ -149,12 +150,11 @@ async def get_v3_data_store_obj_w_code( """ V3 Standardized Data Store Lookup. Uses JWT-based AccountContext and supports cascading fallback logic (Object > Account > Global). + Returns a single object if limit=1, otherwise returns a list. """ log.setLevel(logging.INFO) # Map V3 params to the shared handler - # We create a dummy Common_Route_Params object to satisfy the handler's interface - # while using the more secure V3 dependencies. v3_commons = Common_Route_Params( x_account_id=account.account_id, x_account_id_random=account.account_id_random, @@ -167,6 +167,7 @@ async def get_v3_data_store_obj_w_code( for_type = for_type, for_id = for_id, commons = v3_commons, + limit = limit, ) # ### END ### API Data Store ### get_v3_data_store_obj_w_code() ### @@ -184,6 +185,7 @@ async def get_data_store_obj_w_code_path( data_store_code: str = Path(min_length=3, max_length=50), for_type: Optional[str] = Path(min_length=1, max_length=25), for_id: Optional[str] = Path(min_length=11, max_length=22), + limit: int = Query(1, ge=1), commons: Common_Route_Params = Depends(common_route_params), ): @@ -195,8 +197,8 @@ async def get_data_store_obj_w_code_path( data_store_code = data_store_code, for_type = for_type, for_id = for_id, - commons = commons, + limit = limit, ) @@ -205,6 +207,7 @@ async def get_data_store_obj_w_code_query( data_store_code: str = Path(min_length=3, max_length=50), for_type: Optional[str] = Query(None, min_length=1, max_length=25), for_id: Optional[str] = Query(None, min_length=11, max_length=22), + limit: int = Query(1, ge=1), commons: Common_Route_Params = Depends(common_route_params), ): @@ -216,8 +219,8 @@ async def get_data_store_obj_w_code_query( data_store_code = data_store_code, for_type = for_type, for_id = for_id, - commons = commons, + limit = limit, ) @@ -225,39 +228,35 @@ def handle_get_data_store_obj_w_code( data_store_code: str, for_type: Optional[str], for_id: Optional[str], - commons: Common_Route_Params, + limit: int = 1, ): log.setLevel(logging.INFO) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL log.debug(locals()) - log.debug(commons.x_account_id_random) - log.debug(commons.x_account_id) - - # NOTE: WARNING NOTE: WARNING NOTE: WARNING NOTE: WARNING NOTE: WARNING NOTE: WARNING NOTE: WARNING - # time.sleep(2.5) # NOTE: WARNING NOTE: WARNING NOTE: WARNING NOTE: WARNING NOTE: WARNING NOTE: WARNING - # NOTE: WARNING NOTE: WARNING NOTE: WARNING NOTE: WARNING NOTE: WARNING NOTE: WARNING NOTE: WARNING - # ### SECTION ### Secondary data validation if for_type and for_id: if for_id := redis_lookup_id_random(record_id_random=for_id, table_name=for_type): pass else: return mk_resp(data=None, status_code=404, response=commons.response, status_message='The for type and ID was invalid or not found.') - # NOTE: Currently this returns a list: load_data_store_obj_w_code() - # NOTE: Only the first sorted record is needed + # NOTE: load_data_store_obj_w_code() returns a list if data_store_obj_result := load_data_store_obj_w_code( account_id = commons.x_account_id, code = data_store_code, for_type = for_type, for_id = for_id, - limit = 1, # commons.limit, + limit = limit, enabled = commons.enabled, ): log.setLevel(logging.INFO) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL - log.info('Loading successful. Returning result') - data_store_obj = data_store_obj_result[0] # Get first record only - log.debug(data_store_obj) - return mk_resp(data=data_store_obj, response=commons.response) + log.info(f'Loading successful. Returning {len(data_store_obj_result)} result(s)') + + # If limit=1, return the first object directly (standard lookup behavior) + # If limit > 1, return the list of results + data = data_store_obj_result[0] if limit == 1 else data_store_obj_result + + log.debug(data) + return mk_resp(data=data, response=commons.response) elif isinstance(data_store_obj_result, list) or data_store_obj_result is None: # Empty list or None log.info('No results') return mk_resp(data=None, status_code=404, response=commons.response) # Not Found diff --git a/documentation/GUIDE__V3_FRONTEND_API.md b/documentation/GUIDE__V3_FRONTEND_API.md index 4483ea1..fa77bbd 100644 --- a/documentation/GUIDE__V3_FRONTEND_API.md +++ b/documentation/GUIDE__V3_FRONTEND_API.md @@ -152,6 +152,7 @@ V3 provides a specialized endpoint for retrieving configuration or content snipp | :--- | :--- | :--- | :--- | | `for_type` | String | No | Parent object type (e.g., `event`, `person`). | | `for_id` | String | No | Parent object random ID. | +| `limit` | Integer | No | **Dynamic Return:** Default `1` (returns single object). If `> 1`, returns a list. | ### B. Cascading Logic (Specificity) The API automatically resolves the "best fit" record in the following order: @@ -161,10 +162,10 @@ The API automatically resolves the "best fit" record in the following order: ### C. Example Implementation ```ts -// GET /v3/data_store/code/event_launcher_main_info?for_type=event&for_id=nmBfuGFeR0k -export async function get_data_store_v3({ api_cfg, code, for_type, for_id }) { +// GET /v3/data_store/code/event_launcher_main_info?for_type=event&for_id=nmBfuGFeR0k&limit=1 +export async function get_data_store_v3({ api_cfg, code, for_type, for_id, limit = 1 }) { const endpoint = `/v3/data_store/code/${code}`; - const params = { for_type, for_id }; + const params = { for_type, for_id, limit }; return await get_object({ api_cfg, endpoint, params }); } -``` \ No newline at end of file +``` diff --git a/tests/e2e/test_e2e_v3_data_store_lookup.py b/tests/e2e/test_e2e_v3_data_store_lookup.py index 8b21038..0dc41a3 100644 --- a/tests/e2e/test_e2e_v3_data_store_lookup.py +++ b/tests/e2e/test_e2e_v3_data_store_lookup.py @@ -15,11 +15,11 @@ CONTEXTS = { "event_1358": "nmBfuGFeR0k" } -def run_lookup(code, description, account_id=None, for_type=None, for_id=None, version="v3"): +def run_lookup(code, description, account_id=None, for_type=None, for_id=None, limit=1, version="v3"): """ Performs a Data Store lookup and prints standardized results. """ - print(f"[{version.upper()}] {description}") + print(f"[{version.upper()}] {description} (Limit: {limit})") headers = { "X-Aether-API-Key": AGENT_API_KEY, @@ -33,7 +33,7 @@ def run_lookup(code, description, account_id=None, for_type=None, for_id=None, v if version == "v3": url = f"{BASE_URL}/v3/data_store/code/{code}" - params = {"for_type": for_type, "for_id": for_id} + params = {"for_type": for_type, "for_id": for_id, "limit": limit} response = requests.get(url, headers=headers, params=params) else: # Legacy Endpoint @@ -41,7 +41,7 @@ def run_lookup(code, description, account_id=None, for_type=None, for_id=None, v url = f"{BASE_URL}/data_store/code/{code}/{for_type}/{for_id}" else: url = f"{BASE_URL}/data_store/code/{code}" - response = requests.get(url, headers=headers) + response = requests.get(url, headers=headers, params={"limit": limit}) print(f" URL: {response.url}") print(f" Status: {response.status_code}") @@ -49,11 +49,14 @@ def run_lookup(code, description, account_id=None, for_type=None, for_id=None, v if response.status_code == 200: data = response.json().get('data') if data: - obj = data[0] if isinstance(data, list) else data - rec_id = obj.get('id') or obj.get('data_store_id') or obj.get('data_store_id_random') - print(f" Result: SUCCESS") - print(f" ID: {rec_id}") - print(f" Name: {obj.get('name')}") + if isinstance(data, list): + print(f" Result: SUCCESS (List of {len(data)})") + for i, item in enumerate(data): + print(f" [{i+1}] ID: {item.get('id') or item.get('data_store_id')}, Name: {item.get('name')[:40]}...") + else: + print(f" Result: SUCCESS (Single Object)") + print(f" ID: {data.get('id') or data.get('data_store_id')}") + print(f" Name: {data.get('name')}") else: print(f" Result: NULL (No record found or validation failed)") else: @@ -69,25 +72,16 @@ if __name__ == "__main__": print(f"Target: {BASE_URL}") print(f"Code: {args.code}\n") - # 1. Global Context - run_lookup(args.code, "Scenario: Global Context (Bypass Account)") + # 1. Standard Single Result (Default) + run_lookup(args.code, "Scenario: Single Result (Default)", account_id=CONTEXTS["account_1"]) - # 2. Account 1 Context - run_lookup(args.code, "Scenario: Account 1 Context", account_id=CONTEXTS["account_1"]) + # 2. Multi-Result Override (Limit 5) + run_lookup(args.code, "Scenario: Multi-Result Override", account_id=CONTEXTS["account_1"], limit=5) - # 3. Account 22 Context - run_lookup(args.code, "Scenario: Account 22 Context", account_id=CONTEXTS["account_22"]) - - # 4. Object Specific Context (Event 1358 - belongs to Account 1) + # 3. Object Specific Context (Event 1358) run_lookup(args.code, "Scenario: Event 1358 (under Account 1)", account_id=CONTEXTS["account_1"], for_type="event", for_id=CONTEXTS["event_1358"]) - # 5. Cross-Account Security Check (Event 1358 requested by Account 23) - run_lookup(args.code, "Scenario: Security Check (Event 1358 by Account 23 - SHOULD BE NULL)", - account_id=CONTEXTS["account_23"], - for_type="event", - for_id=CONTEXTS["event_1358"]) - print("\nTests Complete.")