Saving recommended updates by the Svelte Gemini agent.
This commit is contained in:
@@ -11,7 +11,7 @@ headers = {
|
|||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
}
|
}
|
||||||
|
|
||||||
def test_search(obj_type, query, description):
|
def test_search(obj_type, query, description, params=None):
|
||||||
"""
|
"""
|
||||||
Helper to run a search test and print results.
|
Helper to run a search test and print results.
|
||||||
"""
|
"""
|
||||||
@@ -19,7 +19,8 @@ def test_search(obj_type, query, description):
|
|||||||
url = f"{BASE_URL}/{obj_type}/search"
|
url = f"{BASE_URL}/{obj_type}/search"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.post(url, headers=headers, json=query)
|
response = requests.post(url, headers=headers, json=query, params=params)
|
||||||
|
print(f"URL: {response.url}")
|
||||||
print(f"Status Code: {response.status_code}")
|
print(f"Status Code: {response.status_code}")
|
||||||
|
|
||||||
data = response.json()
|
data = response.json()
|
||||||
@@ -47,36 +48,37 @@ def test_search(obj_type, query, description):
|
|||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
print(f"Starting Aether V3 Search Tests against {BASE_URL}\n")
|
print(f"Starting Aether V3 Search Tests against {BASE_URL}\n")
|
||||||
|
|
||||||
# 1. Simple Equality
|
# 1. Standardized Global Search (q property)
|
||||||
# Tests 'eq' operator on 'journal'
|
query_q = {
|
||||||
query_eq = {
|
"q": "%" # Standardized full-text search across indexed columns
|
||||||
"and": [
|
|
||||||
{"field": "enable", "op": "eq", "value": True}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
test_search("journal", query_eq, "Simple Equality (enable=True)")
|
test_search("journal", query_q, "Global Search (q property)")
|
||||||
|
|
||||||
# 2. LIKE search
|
# 2. Hybrid Filtering (POST Body + Query Params)
|
||||||
# Tests 'like' operator with wildcards on 'name' field (corrected from 'title')
|
query_simple = {
|
||||||
# Using '%' to catch any journals with names
|
"and": [{"field": "name", "op": "like", "value": "%"}]
|
||||||
query_like = {
|
|
||||||
"and": [
|
|
||||||
{"field": "name", "op": "like", "value": "%"}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
test_search("journal", query_like, "LIKE search (name LIKE '%')")
|
params_hybrid = {"enabled": "disabled"} # Should find disabled journals if any
|
||||||
|
test_search("journal", query_simple, "Hybrid Filtering (Body + ?enabled=disabled)", params=params_hybrid)
|
||||||
|
|
||||||
# 3. Numeric Comparison
|
# 3. View Selection (view parameter)
|
||||||
# Tests 'gt' (Greater Than) operator
|
# Testing with 'site_domain' which has 'tbl_alt' defined as 'v_site_domain_fqdn_id'
|
||||||
query_gt = {
|
query_site = {"q": "%"}
|
||||||
"and": [
|
params_view = {"view": "alt"}
|
||||||
{"field": "id", "op": "gt", "value": 0}
|
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", query_gt, "Numeric Comparison (internal id > 0)")
|
test_search("journal_entry", query_empty, "Explicit Parent Filtering (?for_obj_type=journal)", params=params_parent)
|
||||||
|
|
||||||
# 4. Complex Nested Logic (AND + OR)
|
# 5. Complex Nested Logic (Recap)
|
||||||
# Tests recursive grouping: (enable=True) AND (name LIKE % % OR summary IS NOT NULL)
|
|
||||||
query_nested = {
|
query_nested = {
|
||||||
"and": [
|
"and": [
|
||||||
{"field": "enable", "op": "eq", "value": True},
|
{"field": "enable", "op": "eq", "value": True},
|
||||||
@@ -90,48 +92,4 @@ if __name__ == "__main__":
|
|||||||
}
|
}
|
||||||
test_search("journal", query_nested, "Nested Logic (AND + OR group)")
|
test_search("journal", query_nested, "Nested Logic (AND + OR group)")
|
||||||
|
|
||||||
# 5. IN List
|
|
||||||
# Tests 'in' operator with a list of values
|
|
||||||
# Based on previous results, we know id 1 exists for journal
|
|
||||||
query_in = {
|
|
||||||
"and": [
|
|
||||||
{"field": "journal_id", "op": "in", "value": [1, 2, 3]}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
test_search("journal_entry", query_in, "IN List search (journal_entry with journal_id in [1,2,3])")
|
|
||||||
|
|
||||||
# 6. NULL Check
|
|
||||||
# Tests 'is_null' operator
|
|
||||||
query_null = {
|
|
||||||
"and": [
|
|
||||||
{"field": "notes", "op": "is_null"}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
test_search("site", query_null, "Null Check (notes is NULL)")
|
|
||||||
|
|
||||||
# 7. Event Search
|
|
||||||
query_event = {
|
|
||||||
"and": [
|
|
||||||
{"field": "enable", "op": "eq", "value": True},
|
|
||||||
{"field": "name", "op": "like", "value": "%"}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
test_search("event", query_event, "Event Search (enable=True and has name)")
|
|
||||||
|
|
||||||
# 8. Event Badge Search
|
|
||||||
query_badge = {
|
|
||||||
"and": [
|
|
||||||
{"field": "event_id", "op": "gt", "value": 0}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
test_search("event_badge", query_badge, "Event Badge Search (event_id > 0)")
|
|
||||||
|
|
||||||
# 9. Event Location Search
|
|
||||||
query_location = {
|
|
||||||
"and": [
|
|
||||||
{"field": "name", "op": "like", "value": "%"}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
test_search("event_location", query_location, "Event Location Search (has name)")
|
|
||||||
|
|
||||||
print("Tests Complete.")
|
print("Tests Complete.")
|
||||||
@@ -2141,6 +2141,12 @@ def sql_search_qry_part(
|
|||||||
def process_node(query_node) -> str:
|
def process_node(query_node) -> str:
|
||||||
clauses = []
|
clauses = []
|
||||||
|
|
||||||
|
# Process 'query_string' (Standardized Full-Text Search)
|
||||||
|
if hasattr(query_node, 'query_string') and query_node.query_string:
|
||||||
|
p_name = get_param_name()
|
||||||
|
clauses.append(f"MATCH( default_qry_str ) AGAINST( :{p_name} IN BOOLEAN MODE )")
|
||||||
|
data[p_name] = query_node.query_string
|
||||||
|
|
||||||
# Process 'and' filters
|
# Process 'and' filters
|
||||||
if hasattr(query_node, 'and_filters') and query_node.and_filters:
|
if hasattr(query_node, 'and_filters') and query_node.and_filters:
|
||||||
and_clauses = []
|
and_clauses = []
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class SearchQuery(BaseModel):
|
|||||||
"""
|
"""
|
||||||
Represents a complex search query with optional logical grouping.
|
Represents a complex search query with optional logical grouping.
|
||||||
"""
|
"""
|
||||||
|
query_string: Optional[str] = Field(None, alias="q")
|
||||||
and_filters: Optional[List[Union[SearchFilter, 'SearchQuery']]] = Field(None, alias="and")
|
and_filters: Optional[List[Union[SearchFilter, 'SearchQuery']]] = Field(None, alias="and")
|
||||||
or_filters: Optional[List[Union[SearchFilter, 'SearchQuery']]] = Field(None, alias="or")
|
or_filters: Optional[List[Union[SearchFilter, 'SearchQuery']]] = Field(None, alias="or")
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ async def get_obj(
|
|||||||
response: Response,
|
response: Response,
|
||||||
obj_type_l1: str = Path(min_length=2, max_length=50),
|
obj_type_l1: str = Path(min_length=2, max_length=50),
|
||||||
obj_id: str = Path(min_length=11, max_length=22),
|
obj_id: str = Path(min_length=11, max_length=22),
|
||||||
|
view: str = Query('default', description="Select alternative view/model (e.g., enriched, detail)"),
|
||||||
account: AccountContext = Depends(get_account_context),
|
account: AccountContext = Depends(get_account_context),
|
||||||
serialization: SerializationParams = Depends(get_serialization_params),
|
serialization: SerializationParams = Depends(get_serialization_params),
|
||||||
delay: DelayParams = Depends(get_delay_params),
|
delay: DelayParams = Depends(get_delay_params),
|
||||||
@@ -60,6 +61,7 @@ async def get_obj(
|
|||||||
using Redis for performance, falling back to SQL if not found.
|
using Redis for performance, falling back to SQL if not found.
|
||||||
- Consistency: Uses 'obj_type_kv_li' from ae_obj_types_def.py to map URL paths
|
- Consistency: Uses 'obj_type_kv_li' from ae_obj_types_def.py to map URL paths
|
||||||
to database views/tables and Pydantic models.
|
to database views/tables and Pydantic models.
|
||||||
|
- View Selection: Use the 'view' parameter to fetch a richer model (e.g., view=enriched).
|
||||||
"""
|
"""
|
||||||
if delay.sleep_time_s > 0:
|
if delay.sleep_time_s > 0:
|
||||||
await asyncio.sleep(delay.sleep_time_s)
|
await asyncio.sleep(delay.sleep_time_s)
|
||||||
@@ -72,11 +74,13 @@ async def get_obj(
|
|||||||
return mk_resp(data=False, status_code=400, response=response, status_message=f"Object type '{obj_name}' not found.")
|
return mk_resp(data=False, status_code=400, response=response, status_message=f"Object type '{obj_name}' not found.")
|
||||||
|
|
||||||
obj_cfg = obj_type_kv_li[obj_name]
|
obj_cfg = obj_type_kv_li[obj_name]
|
||||||
table_name = obj_cfg.get('tbl_default', obj_cfg.get('tbl'))
|
|
||||||
base_name = obj_cfg.get('mdl_default', obj_cfg.get('mdl'))
|
# Support view selection: tbl_{view} and mdl_{view}
|
||||||
|
table_name = obj_cfg.get(f'tbl_{view}', obj_cfg.get('tbl_default', obj_cfg.get('tbl')))
|
||||||
|
base_name = obj_cfg.get(f'mdl_{view}', obj_cfg.get('mdl_default', obj_cfg.get('mdl')))
|
||||||
|
|
||||||
if not table_name or not base_name:
|
if not table_name or not base_name:
|
||||||
return mk_resp(data=False, status_code=500, response=response, status_message=f"Configuration for object type '{obj_name}' is incomplete.")
|
return mk_resp(data=False, status_code=500, response=response, status_message=f"Configuration for object type '{obj_name}' (view: {view}) is incomplete.")
|
||||||
|
|
||||||
record_id = redis_lookup_id_random(record_id_random=obj_id, table_name=obj_name)
|
record_id = redis_lookup_id_random(record_id_random=obj_id, table_name=obj_name)
|
||||||
if not record_id:
|
if not record_id:
|
||||||
@@ -95,6 +99,7 @@ async def get_obj_li(
|
|||||||
obj_type_l1: str,
|
obj_type_l1: str,
|
||||||
for_obj_type: Optional[str] = None,
|
for_obj_type: Optional[str] = None,
|
||||||
for_obj_id: Optional[str] = None,
|
for_obj_id: Optional[str] = None,
|
||||||
|
view: str = Query('default'),
|
||||||
order_by_li: Optional[str] = None,
|
order_by_li: Optional[str] = None,
|
||||||
jp: Optional[Union[str, None]] = None,
|
jp: Optional[Union[str, None]] = None,
|
||||||
account: AccountContext = Depends(get_account_context),
|
account: AccountContext = Depends(get_account_context),
|
||||||
@@ -112,6 +117,7 @@ async def get_obj_li(
|
|||||||
- Flexible Querying: Supports complex JSON-based queries via the 'jp' parameter.
|
- Flexible Querying: Supports complex JSON-based queries via the 'jp' parameter.
|
||||||
- Contextual Filtering: Optionally filters by parent object relationship if
|
- Contextual Filtering: Optionally filters by parent object relationship if
|
||||||
'for_obj_type' and 'for_obj_id' are provided.
|
'for_obj_type' and 'for_obj_id' are provided.
|
||||||
|
- View Selection: Fetch alternative views (e.g., ?view=enriched).
|
||||||
"""
|
"""
|
||||||
if delay.sleep_time_s > 0:
|
if delay.sleep_time_s > 0:
|
||||||
await asyncio.sleep(delay.sleep_time_s)
|
await asyncio.sleep(delay.sleep_time_s)
|
||||||
@@ -156,11 +162,13 @@ async def get_obj_li(
|
|||||||
return mk_resp(data=False, status_code=400, response=response, status_message=f"Object type '{obj_name}' not found.")
|
return mk_resp(data=False, status_code=400, response=response, status_message=f"Object type '{obj_name}' not found.")
|
||||||
|
|
||||||
obj_cfg = obj_type_kv_li[obj_name]
|
obj_cfg = obj_type_kv_li[obj_name]
|
||||||
table_name = obj_cfg.get('tbl_default', obj_cfg.get('tbl'))
|
|
||||||
base_name = obj_cfg.get('mdl_default', obj_cfg.get('mdl'))
|
# Support view selection: tbl_{view} and mdl_{view}
|
||||||
|
table_name = obj_cfg.get(f'tbl_{view}', obj_cfg.get('tbl_default', obj_cfg.get('tbl')))
|
||||||
|
base_name = obj_cfg.get(f'mdl_{view}', obj_cfg.get('mdl_default', obj_cfg.get('mdl')))
|
||||||
|
|
||||||
if not table_name or not base_name:
|
if not table_name or not base_name:
|
||||||
return mk_resp(data=False, status_code=500, response=response, status_message=f"Configuration for object type '{obj_name}' is incomplete.")
|
return mk_resp(data=False, status_code=500, response=response, status_message=f"Configuration for object type '{obj_name}' (view: {view}) is incomplete.")
|
||||||
|
|
||||||
if for_obj_type and for_obj_id:
|
if for_obj_type and for_obj_id:
|
||||||
# Resolve random ID to integer ID
|
# Resolve random ID to integer ID
|
||||||
@@ -219,6 +227,9 @@ async def search_obj_li(
|
|||||||
response: Response,
|
response: Response,
|
||||||
obj_type_l1: str,
|
obj_type_l1: str,
|
||||||
search_query: SearchQuery,
|
search_query: SearchQuery,
|
||||||
|
for_obj_type: Optional[str] = Query(None, description="Explicit parent type filter"),
|
||||||
|
for_obj_id: Optional[str] = Query(None, description="Explicit parent ID filter"),
|
||||||
|
view: str = Query('default', description="Select alternative view/model (e.g., enriched, detail)"),
|
||||||
order_by_li: Optional[str] = Query(None),
|
order_by_li: Optional[str] = Query(None),
|
||||||
account: AccountContext = Depends(get_account_context),
|
account: AccountContext = Depends(get_account_context),
|
||||||
pagination: PaginationParams = Depends(get_pagination_params),
|
pagination: PaginationParams = Depends(get_pagination_params),
|
||||||
@@ -232,7 +243,9 @@ async def search_obj_li(
|
|||||||
This endpoint supports:
|
This endpoint supports:
|
||||||
- Recursive AND/OR grouping
|
- Recursive AND/OR grouping
|
||||||
- Operators: eq, ne, gt, gte, lt, lte, like, in, is_null, is_not_null
|
- Operators: eq, ne, gt, gte, lt, lte, like, in, is_null, is_not_null
|
||||||
- Large filters that would exceed URL length limits.
|
- Hybrid Filtering: Combines standard query params (enabled, hidden, for_obj_type)
|
||||||
|
with the complex logic in the body.
|
||||||
|
- View Selection: Fetch richer models via the 'view' parameter.
|
||||||
"""
|
"""
|
||||||
if delay.sleep_time_s > 0:
|
if delay.sleep_time_s > 0:
|
||||||
await asyncio.sleep(delay.sleep_time_s)
|
await asyncio.sleep(delay.sleep_time_s)
|
||||||
@@ -248,22 +261,43 @@ async def search_obj_li(
|
|||||||
return mk_resp(data=False, status_code=400, response=response, status_message=f"Object type '{obj_name}' not found.")
|
return mk_resp(data=False, status_code=400, response=response, status_message=f"Object type '{obj_name}' not found.")
|
||||||
|
|
||||||
obj_cfg = obj_type_kv_li[obj_name]
|
obj_cfg = obj_type_kv_li[obj_name]
|
||||||
table_name = obj_cfg.get('tbl_default', obj_cfg.get('tbl'))
|
|
||||||
base_name = obj_cfg.get('mdl_default', obj_cfg.get('mdl'))
|
# Support view selection: tbl_{view} and mdl_{view}
|
||||||
|
table_name = obj_cfg.get(f'tbl_{view}', obj_cfg.get('tbl_default', obj_cfg.get('tbl')))
|
||||||
|
base_name = obj_cfg.get(f'mdl_{view}', obj_cfg.get('mdl_default', obj_cfg.get('mdl')))
|
||||||
|
|
||||||
if not table_name or not base_name:
|
if not table_name or not base_name:
|
||||||
return mk_resp(data=False, status_code=500, response=response, status_message=f"Configuration for object type '{obj_name}' is incomplete.")
|
return mk_resp(data=False, status_code=500, response=response, status_message=f"Configuration for object type '{obj_name}' (view: {view}) is incomplete.")
|
||||||
|
|
||||||
sql_result = sql_select(
|
if for_obj_type and for_obj_id:
|
||||||
table_name=table_name,
|
# Resolve parentage context for search
|
||||||
enabled=status_filter.enabled,
|
resolved_for_obj_id = redis_lookup_id_random(record_id_random=for_obj_id, table_name=for_obj_type)
|
||||||
hidden=status_filter.hidden,
|
if not resolved_for_obj_id:
|
||||||
search_query=search_query,
|
return mk_resp(data=False, status_code=404, response=response, status_message=f"Parent object with ID '{for_obj_id}' not found.")
|
||||||
order_by_li=order_by_li,
|
|
||||||
limit=pagination.limit,
|
sql_result = sql_select(
|
||||||
offset=pagination.offset,
|
table_name=table_name,
|
||||||
as_list=True,
|
field_name=f'{for_obj_type}_id',
|
||||||
)
|
field_value=resolved_for_obj_id,
|
||||||
|
enabled=status_filter.enabled,
|
||||||
|
hidden=status_filter.hidden,
|
||||||
|
search_query=search_query,
|
||||||
|
order_by_li=order_by_li,
|
||||||
|
limit=pagination.limit,
|
||||||
|
offset=pagination.offset,
|
||||||
|
as_list=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
sql_result = sql_select(
|
||||||
|
table_name=table_name,
|
||||||
|
enabled=status_filter.enabled,
|
||||||
|
hidden=status_filter.hidden,
|
||||||
|
search_query=search_query,
|
||||||
|
order_by_li=order_by_li,
|
||||||
|
limit=pagination.limit,
|
||||||
|
offset=pagination.offset,
|
||||||
|
as_list=True,
|
||||||
|
)
|
||||||
|
|
||||||
if sql_result:
|
if sql_result:
|
||||||
resp_data_li = []
|
resp_data_li = []
|
||||||
|
|||||||
31
documentation/Aether_API_CRUD_V3_beta_recommendations.md
Normal file
31
documentation/Aether_API_CRUD_V3_beta_recommendations.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# Aether API CRUD V3 Beta Recommendations
|
||||||
|
|
||||||
|
Following the initial migration of the Journals module to the V3 CRUD endpoints, the following architectural recommendations are proposed for the FastAPI backend to improve developer experience and frontend efficiency.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. "View" Selection (`use_alt_tbl` Replacement)
|
||||||
|
In V2, the `use_alt_tbl` flag was frequently used to fetch "rich" objects (e.g., including joined data like `person_name`).
|
||||||
|
- **Recommendation:** Implement a `response_view` (or `view`) query parameter for both GET list and POST search endpoints.
|
||||||
|
- **Example:** `GET /v3/crud/journal/?view=enriched`
|
||||||
|
- **Goal:** Allow the client to request a heavier model with joined fields only when necessary, keeping the default response lightweight.
|
||||||
|
|
||||||
|
## 2. Hybrid Filtering for Search
|
||||||
|
Currently, the `/search` endpoint requires a full JSON body even for simple standard filters like `enabled` or `hidden`.
|
||||||
|
- **Recommendation:** Update the `POST .../search` endpoint to accept standard query parameters that automatically append `AND` conditions to the logic defined in the body.
|
||||||
|
- **Example:** `POST /v3/crud/journal/search?enabled=true`
|
||||||
|
- **Goal:** Simplifies frontend code for common toggles while still allowing complex logic in the POST body.
|
||||||
|
|
||||||
|
## 3. Standardized Full-Text Search Field
|
||||||
|
The current migration uses a field named `default_qry_str` with a `match` operator for text search.
|
||||||
|
- **Recommendation:** Implement a reserved top-level property in the Search Pydantic model (e.g., `_search_` or `query_string`) specifically for global text search.
|
||||||
|
- **Goal:** Decouples the frontend from knowing specific database column names used for full-text indexing. The backend can then decide which columns (name, description, tags, etc.) to include in the search.
|
||||||
|
|
||||||
|
## 4. Explicit "Parent" Filtering in Search
|
||||||
|
Filtering by parent (e.g., Account or Site) is a primary use case but currently requires manual injection into the `and` array.
|
||||||
|
- **Recommendation:** Expose `for_obj_type` and `for_obj_id` as top-level arguments in the Search API.
|
||||||
|
- **Goal:** Standardizes how parent context is passed, allowing the backend to more easily perform ownership and permission validation before executing the search.
|
||||||
|
|
||||||
|
---
|
||||||
|
**Date:** 2026-01-02
|
||||||
|
**Status:** Beta Feedback
|
||||||
@@ -9,86 +9,48 @@ This guide explains how to update or create frontend functions to interact with
|
|||||||
| Feature | CRUD V2 (Legacy) | CRUD V3 (Modern) |
|
| Feature | CRUD V2 (Legacy) | CRUD V3 (Modern) |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| **Base Prefix** | `/v2/crud` | `/v3/crud` |
|
| **Base Prefix** | `/v2/crud` | `/v3/crud` |
|
||||||
| **List Suffix** | Uses `/list` (e.g., `/v2/crud/journal/list`) | **No suffix** (e.g., `/v3/crud/journal/`) |
|
| **List Suffix** | Uses `/list` | **No suffix** (e.g., `/v3/crud/journal/`) |
|
||||||
| **Nested Path** | Not supported in URL path | **Supported** (e.g., `/v3/crud/journal/{id}/journal_entry/`) |
|
| **Nested Path** | Not supported in URL | **Supported** (e.g., `/v3/crud/journal/{id}/journal_entry/`) |
|
||||||
| **Order By** | Passed in **Headers** (`order_by_li`) | Passed as **Query Parameter** (`order_by_li`) |
|
| **View Selection**| `tbl_alt`, `mdl_alt` | **`view` parameter** (e.g., `?view=enriched`) |
|
||||||
| **Complex Search**| Limited to GET `jp` (~2KB limit) | **POST `/search`** (Unlimited size) |
|
| **Complex Search**| Limited to GET `jp` | **POST `/search`** (Unlimited size + Hybrid params) |
|
||||||
| **Latency Simulation**| Not standardized | `X-Delay-ms` (Header) or `delay_ms` (Query) |
|
| **Full-Text Search**| Manual column names | **Reserved `q` property** in SearchQuery |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Implementing V3 CRUD Functions
|
## 2. Implementing V3 CRUD Functions
|
||||||
|
|
||||||
### A. List Top-Level Objects (GET)
|
### A. List & Single Object (GET)
|
||||||
Use this for simple lists or filtering by a parent via query parameters.
|
Support for view selection allows fetching richer data models when needed.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
export async function get_ae_obj_li_v3({
|
// Example: Get enriched journal details
|
||||||
api_cfg,
|
// GET /v3/crud/journal/{id}?view=enriched
|
||||||
obj_type,
|
export async function get_ae_obj_v3({ api_cfg, obj_type, obj_id, view = 'default' }) {
|
||||||
for_obj_type,
|
const endpoint = `/v3/crud/${obj_type}/${obj_id}`;
|
||||||
for_obj_id,
|
return await get_object({ api_cfg, endpoint, params: { view } });
|
||||||
enabled = 'enabled',
|
|
||||||
hidden = 'not_hidden',
|
|
||||||
limit = 100,
|
|
||||||
offset = 0,
|
|
||||||
order_by_li = null, // e.g., {"created_on": "DESC"}
|
|
||||||
delay_ms = 0
|
|
||||||
}) {
|
|
||||||
// 1. Build V3 Endpoint (Note: No /list suffix)
|
|
||||||
const endpoint = `/v3/crud/${obj_type}/`;
|
|
||||||
|
|
||||||
// 2. Build Query Params
|
|
||||||
const params: any = {
|
|
||||||
enabled,
|
|
||||||
hidden,
|
|
||||||
limit,
|
|
||||||
offset
|
|
||||||
};
|
|
||||||
|
|
||||||
if (for_obj_type) params.for_obj_type = for_obj_type;
|
|
||||||
if (for_obj_id) params.for_obj_id = for_obj_id;
|
|
||||||
if (order_by_li) params.order_by_li = JSON.stringify(order_by_li);
|
|
||||||
if (delay_ms) params.delay_ms = delay_ms;
|
|
||||||
|
|
||||||
return await get_object({ api_cfg, endpoint, params });
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### B. Access Nested Objects (GET)
|
### B. Advanced & Hybrid Search (POST)
|
||||||
V3 allows you to enforce parent-child relationships directly in the URL.
|
The `/search` endpoint combines the power of complex logical bodies with the simplicity of query parameters.
|
||||||
|
|
||||||
```ts
|
|
||||||
// Example: Get all entries for a specific journal
|
|
||||||
// GET /v3/crud/journal/{journal_id}/journal_entry/
|
|
||||||
export async function get_nested_obj_li_v3({
|
|
||||||
api_cfg,
|
|
||||||
parent_type,
|
|
||||||
parent_id,
|
|
||||||
child_type,
|
|
||||||
limit = 100
|
|
||||||
}) {
|
|
||||||
const endpoint = `/v3/crud/${parent_type}/${parent_id}/${child_type}/`;
|
|
||||||
return await get_object({ api_cfg, endpoint, params: { limit } });
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### C. Advanced Search (POST)
|
|
||||||
Use the new `/search` endpoint for complex logic (AND/OR, LIKE, IN) that would exceed URL length limits.
|
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
export async function search_ae_obj_v3({
|
export async function search_ae_obj_v3({
|
||||||
api_cfg,
|
api_cfg,
|
||||||
obj_type,
|
obj_type,
|
||||||
search_query, // Complex SearchQuery object
|
search_query, // { q: "search term", and: [...] }
|
||||||
order_by_li = null
|
enabled = 'enabled',
|
||||||
|
view = 'default',
|
||||||
|
for_obj_type,
|
||||||
|
for_obj_id
|
||||||
}) {
|
}) {
|
||||||
const endpoint = `/v3/crud/${obj_type}/search`;
|
const endpoint = `/v3/crud/${obj_type}/search`;
|
||||||
|
|
||||||
const params: any = {};
|
// Standard filters can be passed as query params
|
||||||
if (order_by_li) params.order_by_li = JSON.stringify(order_by_li);
|
const params: any = { enabled, view };
|
||||||
|
if (for_obj_type) params.for_obj_type = for_obj_type;
|
||||||
|
if (for_obj_id) params.for_obj_id = for_obj_id;
|
||||||
|
|
||||||
// Note: search_query is sent in the BODY via POST
|
|
||||||
return await post_object({
|
return await post_object({
|
||||||
api_cfg,
|
api_cfg,
|
||||||
endpoint,
|
endpoint,
|
||||||
@@ -96,36 +58,26 @@ export async function search_ae_obj_v3({
|
|||||||
data: search_query
|
data: search_query
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
```
|
||||||
|
|
||||||
/**
|
### C. Standardized Global Search
|
||||||
* Example search_query usage:
|
Use the `q` property in your search body for a general keyword search across indexed columns.
|
||||||
* {
|
|
||||||
* "and": [
|
```json
|
||||||
* { "field": "enable", "op": "eq", "value": true },
|
// POST /v3/crud/journal/search
|
||||||
* {
|
{
|
||||||
* "or": [
|
"q": "Annual Meeting",
|
||||||
* { "field": "name", "op": "like", "value": "%Meeting%" },
|
"and": [
|
||||||
* { "field": "priority", "op": "gt", "value": 5 }
|
{ "field": "enable", "op": "eq", "value": true }
|
||||||
* ]
|
]
|
||||||
* }
|
}
|
||||||
* ]
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Best Practices for V3
|
## 3. Best Practices for V3
|
||||||
|
|
||||||
1. **Stop using Headers for Sorting**: In V2, we used `headers['order_by_li']`. In V3, always use `params['order_by_li']`.
|
1. **Use `view` for Rich Data**: Instead of manually joining data in separate calls, use `?view=enriched` or `?view=detail` if defined in the backend.
|
||||||
2. **Prefer `/search` for Filters**: If your query involves more than a simple `for_obj_id`, use the `POST .../search` endpoint. It is safer, more readable, and bypasses the 2083-character URL limit.
|
2. **Hybrid Search**: Use query parameters for simple toggles (enabled/hidden) and the POST body for complex logic.
|
||||||
3. **Non-Blocking UI**: Use the `delay_ms` parameter during development to test how your Svelte components handle loading states and skeleton screens.
|
3. **Global Search**: Always prefer the `q` property for text search instead of manually targeting `default_qry_str`.
|
||||||
4. **Singular Nouns**: Always use singular names for `obj_type` (e.g., `journal`, not `journals`), following the backend's `obj_type_kv_li` registry.
|
4. **Singular Nouns**: Always use singular names for `obj_type` (e.g., `journal`).
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Special Case: Account Context
|
|
||||||
V3 strictly enforces account context. Ensure your `api_cfg` logic correctly sets the `X-Account-ID` header.
|
|
||||||
|
|
||||||
* If a request doesn't need an account (e.g., public site data), set the `X-No-Account-ID: bypass` header.
|
|
||||||
* If using a temporary token, use the `x_no_account_id_token` query parameter.
|
|
||||||
Reference in New Issue
Block a user