One last round of testing and documentation updates.
This commit is contained in:
@@ -1,26 +1,45 @@
|
|||||||
import requests
|
import requests
|
||||||
import json
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
BASE_URL = "https://dev-api.oneskyit.com/v3/crud"
|
BASE_URL = "https://dev-api.oneskyit.com/v3/crud"
|
||||||
# Valid account id_random provided by user
|
|
||||||
ACCOUNT_ID = "nqOzejLCDXM"
|
|
||||||
|
|
||||||
headers = {
|
# --- AUTHENTICATION CONFIG ---
|
||||||
"X-Account-ID": ACCOUNT_ID,
|
# Option 1: Modern JWT (Preferred)
|
||||||
"Content-Type": "application/json"
|
JWT_TOKEN = "" # PASTE YOUR JWT TOKEN HERE
|
||||||
}
|
|
||||||
|
|
||||||
def test_search(obj_type, query, description, params=None):
|
# Option 2: Legacy Header Fallback
|
||||||
|
ACCOUNT_ID = "nqOzejLCDXM"
|
||||||
|
|
||||||
|
def get_headers(use_jwt=True):
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
if use_jwt and JWT_TOKEN:
|
||||||
|
headers["Authorization"] = f"Bearer {JWT_TOKEN}"
|
||||||
|
else:
|
||||||
|
headers["X-Account-ID"] = ACCOUNT_ID
|
||||||
|
return headers
|
||||||
|
|
||||||
|
def test_search(obj_type, query, description, params=None, use_jwt_query=False):
|
||||||
"""
|
"""
|
||||||
Helper to run a search test and print results.
|
Helper to run a search test and print results.
|
||||||
"""
|
"""
|
||||||
print(f"--- Testing: {description} ---")
|
print(f"--- Testing: {description} ---")
|
||||||
url = f"{BASE_URL}/{obj_type}/search"
|
url = f"{BASE_URL}/{obj_type}/search"
|
||||||
|
|
||||||
|
# Handle JWT in query parameter if requested
|
||||||
|
request_params = params.copy() if params else {}
|
||||||
|
request_headers = get_headers(use_jwt=not use_jwt_query)
|
||||||
|
|
||||||
|
if use_jwt_query and JWT_TOKEN:
|
||||||
|
request_params["jwt"] = JWT_TOKEN
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.post(url, headers=headers, json=query, params=params)
|
response = requests.post(url, headers=request_headers, json=query, params=request_params)
|
||||||
print(f"URL: {response.url}")
|
print(f"URL: {response.url}")
|
||||||
|
print(f"Auth Method: {'JWT Query' if use_jwt_query else ('JWT Header' if JWT_TOKEN else 'Legacy Header')}")
|
||||||
print(f"Status Code: {response.status_code}")
|
print(f"Status Code: {response.status_code}")
|
||||||
|
|
||||||
data = response.json()
|
data = response.json()
|
||||||
@@ -37,6 +56,7 @@ def test_search(obj_type, query, description, params=None):
|
|||||||
print(f"Result Data: {json.dumps(result_data, indent=2)}")
|
print(f"Result Data: {json.dumps(result_data, indent=2)}")
|
||||||
|
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
|
print(f"Error Message: {data.get('status_message')}")
|
||||||
print(f"Meta Details: {json.dumps(data.get('meta', {}), indent=2)}")
|
print(f"Meta Details: {json.dumps(data.get('meta', {}), indent=2)}")
|
||||||
|
|
||||||
except requests.exceptions.ConnectionError:
|
except requests.exceptions.ConnectionError:
|
||||||
@@ -46,58 +66,55 @@ def test_search(obj_type, query, description, params=None):
|
|||||||
print("-" * 40 + "\n")
|
print("-" * 40 + "\n")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
print(f"Starting Aether V3 Search Tests against {BASE_URL}\n")
|
if not JWT_TOKEN:
|
||||||
|
print("WARNING: JWT_TOKEN is empty. Falling back to Legacy ACCOUNT_ID auth.")
|
||||||
|
print("To test JWT, please paste a token into the JWT_TOKEN variable.\n")
|
||||||
|
|
||||||
# 1. Standardized Global Search (q property)
|
print(f"Starting Aether V3 Search & JWT Tests against {BASE_URL}\n")
|
||||||
query_q = {
|
|
||||||
"q": "%" # Standardized full-text search across indexed columns
|
# 1. JWT Header Authentication Test
|
||||||
|
test_search("journal", {"q": "%"}, "Auth: JWT via Authorization Header")
|
||||||
|
|
||||||
|
# 2. JWT Query Parameter Authentication Test
|
||||||
|
if JWT_TOKEN:
|
||||||
|
test_search("journal", {"q": "%"}, "Auth: JWT via 'jwt' Query Parameter", use_jwt_query=True)
|
||||||
|
|
||||||
|
# 3. New Operator: contains / icontains
|
||||||
|
query_contains = {
|
||||||
|
"and": [{"field": "name", "op": "contains", "value": "Journal"}]
|
||||||
}
|
}
|
||||||
test_search("journal", query_q, "Global Search (q property)")
|
test_search("journal", query_contains, "Operator: contains (automatically adds %%")
|
||||||
|
|
||||||
# 2. Hybrid Filtering (POST Body + Query Params)
|
# 4. New Operator: startswith / istartswith
|
||||||
query_simple = {
|
query_start = {
|
||||||
"and": [{"field": "name", "op": "like", "value": "%"}]
|
"and": [{"field": "name", "op": "startswith", "value": "A"}]
|
||||||
}
|
}
|
||||||
params_hybrid = {"enabled": "disabled"} # Should find disabled journals if any
|
test_search("journal", query_start, "Operator: startswith (automatically adds % at end)")
|
||||||
test_search("journal", query_simple, "Hybrid Filtering (Body + ?enabled=disabled)", params=params_hybrid)
|
|
||||||
|
|
||||||
# 3. View Selection (view parameter)
|
# 5. New Operator: endswith / iendswith
|
||||||
# Testing with 'site_domain' which has 'tbl_alt' defined as 'v_site_domain_fqdn_id'
|
query_end = {
|
||||||
query_site = {"q": "%"}
|
"and": [{"field": "name", "op": "endswith", "value": "Test"}]
|
||||||
params_view = {"view": "alt"}
|
|
||||||
test_search("site_domain", query_site, "View Selection (view=alt)", params=params_view)
|
|
||||||
|
|
||||||
# 4. Explicit Parent Filtering
|
|
||||||
# Testing 'journal_entry' belonging to a journal (journal_id=1 exists based on previous tests)
|
|
||||||
# We'll use the 'for_obj_type' and 'for_obj_id' as query params
|
|
||||||
# Assuming id_random 'DCAV-06-35-85' exists for journal_id 1
|
|
||||||
query_empty = {}
|
|
||||||
params_parent = {
|
|
||||||
"for_obj_type": "journal",
|
|
||||||
"for_obj_id": "DCAV-06-35-85"
|
|
||||||
}
|
}
|
||||||
test_search("journal_entry", query_empty, "Explicit Parent Filtering (?for_obj_type=journal)", params=params_parent)
|
test_search("journal", query_end, "Operator: endswith (automatically adds % at start)")
|
||||||
|
|
||||||
# 5. Complex Nested Logic (Recap)
|
# 6. Error Handling: Unsupported Operator
|
||||||
|
query_bad = {
|
||||||
|
"and": [{"field": "name", "op": "invalid_op", "value": "test"}]
|
||||||
|
}
|
||||||
|
test_search("journal", query_bad, "Error Handling: Unsupported Operator (Should return 400)")
|
||||||
|
|
||||||
|
# 7. Complex Nested Logic (Recap)
|
||||||
query_nested = {
|
query_nested = {
|
||||||
"and": [
|
"and": [
|
||||||
{"field": "enable", "op": "eq", "value": True},
|
{"field": "enable", "op": "eq", "value": True},
|
||||||
{
|
{
|
||||||
"or": [
|
"or": [
|
||||||
{"field": "name", "op": "like", "value": "%Journal%"},
|
{"field": "name", "op": "icontains", "value": "aether"},
|
||||||
{"field": "summary", "op": "is_not_null"}
|
{"field": "summary", "op": "is_not_null"}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
test_search("journal", query_nested, "Nested Logic (AND + OR group)")
|
test_search("journal", query_nested, "Complex Logic: Nested AND/OR + icontains")
|
||||||
|
|
||||||
# 6. Hosted File Link Search
|
print("Tests Complete.")
|
||||||
query_file_link = {
|
|
||||||
"and": [
|
|
||||||
{"field": "link_to_type", "op": "eq", "value": "journal"}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
test_search("hosted_file_link", query_file_link, "Hosted File Link Search (link_to_type='journal')")
|
|
||||||
|
|
||||||
print("Tests Complete.")
|
|
||||||
@@ -94,6 +94,61 @@ The `op` property in a `SearchFilter` supports the following values:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 3. Create, Update, & Delete (POST, PATCH, DELETE)
|
||||||
|
|
||||||
|
V3 supports both top-level operations and nested parent/child operations.
|
||||||
|
|
||||||
|
### A. Create (POST)
|
||||||
|
When creating objects, V3 strictly validates the incoming JSON against the `mdl_in` Pydantic model.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// POST /v3/crud/{obj_type}/
|
||||||
|
// POST /v3/crud/journal/
|
||||||
|
export async function create_ae_obj_v3({ api_cfg, obj_type, data }) {
|
||||||
|
const endpoint = `/v3/crud/${obj_type}/`;
|
||||||
|
return await post_object({ api_cfg, endpoint, data });
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /v3/crud/{parent_obj_type}/{parent_obj_id}/{child_obj_type}/
|
||||||
|
// POST /v3/crud/journal/EIAC-40-76-82/journal_entry/
|
||||||
|
// Note: Parent ID is automatically injected into the child record.
|
||||||
|
export async function create_nested_obj_v3({ api_cfg, parent_type, parent_id, child_type, data }) {
|
||||||
|
const endpoint = `/v3/crud/${parent_type}/${parent_id}/${child_type}/`;
|
||||||
|
return await post_object({ api_cfg, endpoint, data });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### B. Update (PATCH)
|
||||||
|
V3 uses `PATCH` for partial updates. Only the fields provided in the body will be modified in the database.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// PATCH /v3/crud/{obj_type}/{obj_id}
|
||||||
|
export async function update_ae_obj_v3({ api_cfg, obj_type, obj_id, data }) {
|
||||||
|
const endpoint = `/v3/crud/${obj_type}/${obj_id}`;
|
||||||
|
return await patch_object({ api_cfg, endpoint, data });
|
||||||
|
}
|
||||||
|
|
||||||
|
// PATCH /v3/crud/{parent_type}/{parent_id}/{child_type}/{child_id}
|
||||||
|
// Verification: The backend ensures the child actually belongs to the parent before updating.
|
||||||
|
export async function update_nested_obj_v3({ api_cfg, parent_type, parent_id, child_type, child_id, data }) {
|
||||||
|
const endpoint = `/v3/crud/${parent_type}/${parent_id}/${child_type}/${child_id}`;
|
||||||
|
return await patch_object({ api_cfg, endpoint, data });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### C. Delete (DELETE)
|
||||||
|
The `DELETE` method is used for removal. The backend may implement soft-delete (hide/disable) depending on the configuration.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// DELETE /v3/crud/{obj_type}/{obj_id}
|
||||||
|
export async function delete_ae_obj_v3({ api_cfg, obj_type, obj_id }) {
|
||||||
|
const endpoint = `/v3/crud/${obj_type}/${obj_id}`;
|
||||||
|
return await delete_object({ api_cfg, endpoint });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 4. Authentication in V3
|
## 4. Authentication in V3
|
||||||
|
|
||||||
V3 supports multiple authentication methods. The backend resolves these automatically.
|
V3 supports multiple authentication methods. The backend resolves these automatically.
|
||||||
|
|||||||
Reference in New Issue
Block a user