Saving recommended updates by the Svelte Gemini agent.

This commit is contained in:
Scott Idem
2026-01-02 18:57:37 -05:00
parent 8c0be931c0
commit bf16f988c5
6 changed files with 160 additions and 178 deletions

View File

@@ -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 print("Tests Complete.")
# 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.")

View File

@@ -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 = []

View File

@@ -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")

View File

@@ -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 = []

View 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

View File

@@ -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.