feat(data_store): finalize V3 cascading lookup with limit override
- Update GET /v3/data_store/code/{code} to support 'limit' query parameter.
- Refactor return logic: returns single object if limit=1, otherwise returns a list.
- Clean up formatting in GUIDE__V3_FRONTEND_API.md and sync to agents_sync.
- Finalize unified E2E test script: tests/e2e/test_e2e_v3_data_store_lookup.py.
This commit is contained in:
@@ -141,6 +141,7 @@ async def get_v3_data_store_obj_w_code(
|
|||||||
data_store_code: str = Path(min_length=3, max_length=50),
|
data_store_code: str = Path(min_length=3, max_length=50),
|
||||||
for_type: Optional[str] = Query(None, min_length=1, max_length=25),
|
for_type: Optional[str] = Query(None, min_length=1, max_length=25),
|
||||||
for_id: Optional[str] = Query(None, min_length=11, max_length=22),
|
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),
|
account: AccountContext = Depends(get_account_context),
|
||||||
serialization: SerializationParams = Depends(),
|
serialization: SerializationParams = Depends(),
|
||||||
@@ -149,12 +150,11 @@ async def get_v3_data_store_obj_w_code(
|
|||||||
"""
|
"""
|
||||||
V3 Standardized Data Store Lookup.
|
V3 Standardized Data Store Lookup.
|
||||||
Uses JWT-based AccountContext and supports cascading fallback logic (Object > Account > Global).
|
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)
|
log.setLevel(logging.INFO)
|
||||||
|
|
||||||
# Map V3 params to the shared handler
|
# 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(
|
v3_commons = Common_Route_Params(
|
||||||
x_account_id=account.account_id,
|
x_account_id=account.account_id,
|
||||||
x_account_id_random=account.account_id_random,
|
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_type = for_type,
|
||||||
for_id = for_id,
|
for_id = for_id,
|
||||||
commons = v3_commons,
|
commons = v3_commons,
|
||||||
|
limit = limit,
|
||||||
)
|
)
|
||||||
# ### END ### API Data Store ### get_v3_data_store_obj_w_code() ###
|
# ### 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),
|
data_store_code: str = Path(min_length=3, max_length=50),
|
||||||
for_type: Optional[str] = Path(min_length=1, max_length=25),
|
for_type: Optional[str] = Path(min_length=1, max_length=25),
|
||||||
for_id: Optional[str] = Path(min_length=11, max_length=22),
|
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),
|
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,
|
data_store_code = data_store_code,
|
||||||
for_type = for_type,
|
for_type = for_type,
|
||||||
for_id = for_id,
|
for_id = for_id,
|
||||||
|
|
||||||
commons = commons,
|
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),
|
data_store_code: str = Path(min_length=3, max_length=50),
|
||||||
for_type: Optional[str] = Query(None, min_length=1, max_length=25),
|
for_type: Optional[str] = Query(None, min_length=1, max_length=25),
|
||||||
for_id: Optional[str] = Query(None, min_length=11, max_length=22),
|
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),
|
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,
|
data_store_code = data_store_code,
|
||||||
for_type = for_type,
|
for_type = for_type,
|
||||||
for_id = for_id,
|
for_id = for_id,
|
||||||
|
|
||||||
commons = commons,
|
commons = commons,
|
||||||
|
limit = limit,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -225,39 +228,35 @@ def handle_get_data_store_obj_w_code(
|
|||||||
data_store_code: str,
|
data_store_code: str,
|
||||||
for_type: Optional[str],
|
for_type: Optional[str],
|
||||||
for_id: Optional[str],
|
for_id: Optional[str],
|
||||||
|
|
||||||
commons: Common_Route_Params,
|
commons: Common_Route_Params,
|
||||||
|
limit: int = 1,
|
||||||
):
|
):
|
||||||
log.setLevel(logging.INFO) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
|
log.setLevel(logging.INFO) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
|
||||||
log.debug(locals())
|
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
|
# ### SECTION ### Secondary data validation
|
||||||
if for_type and for_id:
|
if for_type and for_id:
|
||||||
if for_id := redis_lookup_id_random(record_id_random=for_id, table_name=for_type): pass
|
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.')
|
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: load_data_store_obj_w_code() returns a list
|
||||||
# NOTE: Only the first sorted record is needed
|
|
||||||
if data_store_obj_result := load_data_store_obj_w_code(
|
if data_store_obj_result := load_data_store_obj_w_code(
|
||||||
account_id = commons.x_account_id,
|
account_id = commons.x_account_id,
|
||||||
code = data_store_code,
|
code = data_store_code,
|
||||||
for_type = for_type,
|
for_type = for_type,
|
||||||
for_id = for_id,
|
for_id = for_id,
|
||||||
limit = 1, # commons.limit,
|
limit = limit,
|
||||||
enabled = commons.enabled,
|
enabled = commons.enabled,
|
||||||
):
|
):
|
||||||
log.setLevel(logging.INFO) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
|
log.setLevel(logging.INFO) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
|
||||||
log.info('Loading successful. Returning result')
|
log.info(f'Loading successful. Returning {len(data_store_obj_result)} result(s)')
|
||||||
data_store_obj = data_store_obj_result[0] # Get first record only
|
|
||||||
log.debug(data_store_obj)
|
# If limit=1, return the first object directly (standard lookup behavior)
|
||||||
return mk_resp(data=data_store_obj, response=commons.response)
|
# 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
|
elif isinstance(data_store_obj_result, list) or data_store_obj_result is None: # Empty list or None
|
||||||
log.info('No results')
|
log.info('No results')
|
||||||
return mk_resp(data=None, status_code=404, response=commons.response) # Not Found
|
return mk_resp(data=None, status_code=404, response=commons.response) # Not Found
|
||||||
|
|||||||
@@ -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_type` | String | No | Parent object type (e.g., `event`, `person`). |
|
||||||
| `for_id` | String | No | Parent object random ID. |
|
| `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)
|
### B. Cascading Logic (Specificity)
|
||||||
The API automatically resolves the "best fit" record in the following order:
|
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
|
### C. Example Implementation
|
||||||
```ts
|
```ts
|
||||||
// GET /v3/data_store/code/event_launcher_main_info?for_type=event&for_id=nmBfuGFeR0k
|
// 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 }) {
|
export async function get_data_store_v3({ api_cfg, code, for_type, for_id, limit = 1 }) {
|
||||||
const endpoint = `/v3/data_store/code/${code}`;
|
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 });
|
return await get_object({ api_cfg, endpoint, params });
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ CONTEXTS = {
|
|||||||
"event_1358": "nmBfuGFeR0k"
|
"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.
|
Performs a Data Store lookup and prints standardized results.
|
||||||
"""
|
"""
|
||||||
print(f"[{version.upper()}] {description}")
|
print(f"[{version.upper()}] {description} (Limit: {limit})")
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
"X-Aether-API-Key": AGENT_API_KEY,
|
"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":
|
if version == "v3":
|
||||||
url = f"{BASE_URL}/v3/data_store/code/{code}"
|
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)
|
response = requests.get(url, headers=headers, params=params)
|
||||||
else:
|
else:
|
||||||
# Legacy Endpoint
|
# 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}"
|
url = f"{BASE_URL}/data_store/code/{code}/{for_type}/{for_id}"
|
||||||
else:
|
else:
|
||||||
url = f"{BASE_URL}/data_store/code/{code}"
|
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" URL: {response.url}")
|
||||||
print(f" Status: {response.status_code}")
|
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:
|
if response.status_code == 200:
|
||||||
data = response.json().get('data')
|
data = response.json().get('data')
|
||||||
if data:
|
if data:
|
||||||
obj = data[0] if isinstance(data, list) else data
|
if isinstance(data, list):
|
||||||
rec_id = obj.get('id') or obj.get('data_store_id') or obj.get('data_store_id_random')
|
print(f" Result: SUCCESS (List of {len(data)})")
|
||||||
print(f" Result: SUCCESS")
|
for i, item in enumerate(data):
|
||||||
print(f" ID: {rec_id}")
|
print(f" [{i+1}] ID: {item.get('id') or item.get('data_store_id')}, Name: {item.get('name')[:40]}...")
|
||||||
print(f" Name: {obj.get('name')}")
|
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:
|
else:
|
||||||
print(f" Result: NULL (No record found or validation failed)")
|
print(f" Result: NULL (No record found or validation failed)")
|
||||||
else:
|
else:
|
||||||
@@ -69,25 +72,16 @@ if __name__ == "__main__":
|
|||||||
print(f"Target: {BASE_URL}")
|
print(f"Target: {BASE_URL}")
|
||||||
print(f"Code: {args.code}\n")
|
print(f"Code: {args.code}\n")
|
||||||
|
|
||||||
# 1. Global Context
|
# 1. Standard Single Result (Default)
|
||||||
run_lookup(args.code, "Scenario: Global Context (Bypass Account)")
|
run_lookup(args.code, "Scenario: Single Result (Default)", account_id=CONTEXTS["account_1"])
|
||||||
|
|
||||||
# 2. Account 1 Context
|
# 2. Multi-Result Override (Limit 5)
|
||||||
run_lookup(args.code, "Scenario: Account 1 Context", account_id=CONTEXTS["account_1"])
|
run_lookup(args.code, "Scenario: Multi-Result Override", account_id=CONTEXTS["account_1"], limit=5)
|
||||||
|
|
||||||
# 3. Account 22 Context
|
# 3. Object Specific Context (Event 1358)
|
||||||
run_lookup(args.code, "Scenario: Account 22 Context", account_id=CONTEXTS["account_22"])
|
|
||||||
|
|
||||||
# 4. Object Specific Context (Event 1358 - belongs to Account 1)
|
|
||||||
run_lookup(args.code, "Scenario: Event 1358 (under Account 1)",
|
run_lookup(args.code, "Scenario: Event 1358 (under Account 1)",
|
||||||
account_id=CONTEXTS["account_1"],
|
account_id=CONTEXTS["account_1"],
|
||||||
for_type="event",
|
for_type="event",
|
||||||
for_id=CONTEXTS["event_1358"])
|
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.")
|
print("\nTests Complete.")
|
||||||
|
|||||||
Reference in New Issue
Block a user