Saving first versions to use the new CRUD V3 endpoints.

This commit is contained in:
Scott Idem
2026-01-02 18:27:12 -05:00
parent 2c7ed476af
commit 33d42ed85b
3 changed files with 212 additions and 203 deletions

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

@@ -140,7 +140,7 @@ export async function load_ae_obj_id__journal({
return ae_promises.load__journal_obj;
}
// Updated 2025-03-15
// Updated 2026-01-02
export async function load_ae_obj_li__journal({
api_cfg,
for_obj_type = 'account',
@@ -182,77 +182,93 @@ export async function load_ae_obj_li__journal({
);
}
const params_json: key_val = {};
let promise;
if (qry_person_id) {
const qry_param = {
type: 'AND',
field: 'person_id_random',
operator: '=',
value: qry_person_id
const search_query: any = {
and: [{ field: 'person_id_random', op: 'eq', value: qry_person_id }]
};
params_json['qry'].push(qry_param);
}
if (log_lvl) {
console.log('params_json:', params_json);
}
if (for_obj_id) {
search_query.and.push({
field: `${for_obj_type}_id_random`,
op: 'eq',
value: for_obj_id
});
}
ae_promises.load__journal_obj_li = await api
.get_ae_obj_li_for_obj_id_crud_v2({
api_cfg: api_cfg,
// Add enabled/hidden filters to search query as V3 search is explicit
if (enabled === 'enabled') {
search_query.and.push({ field: 'enabled', op: 'eq', value: true });
} else if (enabled === 'not_enabled') {
search_query.and.push({ field: 'enabled', op: 'eq', value: false });
}
if (hidden === 'hidden') {
search_query.and.push({ field: 'hidden', op: 'eq', value: true });
} else if (hidden === 'not_hidden') {
search_query.and.push({ field: 'hidden', op: 'eq', value: false });
}
if (log_lvl) {
console.log('Using search_ae_obj_v3 with query:', search_query);
}
promise = api.search_ae_obj_v3({
api_cfg,
obj_type: 'journal',
for_obj_type: for_obj_type,
for_obj_id: for_obj_id,
use_alt_tbl: false,
use_alt_mdl: false,
use_alt_exp: false,
enabled: enabled,
hidden: hidden,
order_by_li: order_by_li,
limit: limit,
offset: offset,
params_json: params_json,
params: params,
log_lvl: log_lvl
})
.then(async function (journal_obj_li_get_result) {
if (journal_obj_li_get_result) {
if (try_cache) {
// Process the results first
const processed_obj_li = await process_ae_obj__journal_props({
obj_li: journal_obj_li_get_result,
log_lvl: log_lvl
});
if (log_lvl) {
console.log('Processed object list:', processed_obj_li);
}
// Save the updated results list to the database
if (log_lvl) {
console.log('Saving to DB...');
}
await db_save_ae_obj_li__ae_obj({
db_instance: db_journals,
table_name: 'journal',
obj_li: processed_obj_li,
properties_to_save: properties_to_save,
log_lvl: log_lvl
});
if (log_lvl) {
console.log('DB save completed.');
}
// db_save_ae_obj_li__journal({
// obj_type: 'journal',
// obj_li: journal_obj_li_get_result,
// log_lvl: log_lvl
// });
}
return journal_obj_li_get_result;
} else {
return [];
}
search_query,
order_by_li,
limit,
offset,
log_lvl
});
} else {
promise = api.get_ae_obj_li_v3({
api_cfg,
obj_type: 'journal',
for_obj_type,
for_obj_id,
enabled,
hidden,
limit,
offset,
order_by_li,
log_lvl
});
}
ae_promises.load__journal_obj_li = await promise.then(async function (journal_obj_li_get_result) {
if (journal_obj_li_get_result) {
if (try_cache) {
// Process the results first
const processed_obj_li = await process_ae_obj__journal_props({
obj_li: journal_obj_li_get_result,
log_lvl: log_lvl
});
if (log_lvl) {
console.log('Processed object list:', processed_obj_li);
}
// Save the updated results list to the database
if (log_lvl) {
console.log('Saving to DB...');
}
await db_save_ae_obj_li__ae_obj({
db_instance: db_journals,
table_name: 'journal',
obj_li: processed_obj_li,
properties_to_save: properties_to_save,
log_lvl: log_lvl
});
if (log_lvl) {
console.log('DB save completed.');
}
}
return journal_obj_li_get_result;
} else {
return [];
}
});
if (log_lvl) {
console.log('ae_promises.load__journal_obj_li:', ae_promises.load__journal_obj_li);
@@ -505,8 +521,8 @@ export async function update_ae_obj__journal({
}
}
// This new function is using CRUD v2. This should allow for more flexibility in the queries.
// Updated 2025-03-15
// This new function is using CRUD V3 Search.
// Updated 2026-01-02
export async function qry__journal({
api_cfg,
journal_id,
@@ -540,49 +556,42 @@ export async function qry__journal({
);
}
// let enabled: string = (params.qry__enabled ?? 'enabled'); // all, disabled, enabled
// let hidden: string = (params.qry__hidden ?? 'not_hidden'); // all, hidden, not_hidden
// let limit: number = (params.qry__limit ?? 25); // 99
// let offset: number = (params.qry__offset ?? 0); // 0
const params_json: key_val = {};
// if (qry_str && qry_str.length > 2) {
// params_json['ft_qry'] = {};
// params_json['ft_qry']['default_qry_str'] = qry_str;
// }
params_json['qry'] = [];
const search_query: any = { and: [] };
if (qry_files === true) {
const qry_param = {
type: 'AND',
field: 'file_count_all',
operator: '>',
value: 0
};
params_json['qry'].push(qry_param);
search_query.and.push({ field: 'file_count_all', op: 'gt', value: 0 });
} else if (qry_files === false) {
const qry_param = {
type: 'AND',
field: 'file_count_all',
operator: 'IS',
value: null
};
params_json['qry'].push(qry_param);
search_query.and.push({ field: 'file_count_all', op: 'is', value: null });
}
if (qry_start_datetime) {
const qry_param = {
type: 'AND',
field: 'start_datetime',
operator: '>',
value: qry_start_datetime
};
params_json['qry'].push(qry_param);
search_query.and.push({ field: 'start_datetime', op: 'gt', value: qry_start_datetime });
}
const order_by_li = {
// if (qry_str && qry_str.length > 2) {
// // V3 Search full text handling would be added here if supported by backend via specific field or op
// }
// Add for_obj_id context (Account ID)
if (journal_id) {
// Assuming journal_id here is actually the account_id as per original usage context
search_query.and.push({ field: 'account_id_random', op: 'eq', value: journal_id });
}
// Add enabled/hidden filters
if (enabled === 'enabled') {
search_query.and.push({ field: 'enabled', op: 'eq', value: true });
} else if (enabled === 'not_enabled') {
search_query.and.push({ field: 'enabled', op: 'eq', value: false });
}
if (hidden === 'hidden') {
search_query.and.push({ field: 'hidden', op: 'eq', value: true });
} else if (hidden === 'not_hidden') {
search_query.and.push({ field: 'hidden', op: 'eq', value: false });
}
const order_by_li: Record<string, 'ASC' | 'DESC'> = {
priority: 'DESC',
sort: 'DESC',
start_datetime: 'ASC',
@@ -592,22 +601,14 @@ export async function qry__journal({
};
ae_promises.load__journal_obj_li = await api
.get_ae_obj_li_for_obj_id_crud_v2({
.search_ae_obj_v3({
api_cfg: api_cfg,
obj_type: 'journal',
for_obj_type: 'account',
for_obj_id: journal_id,
use_alt_tbl: true, // NOTE: We want to use the alt table for journal searching
use_alt_mdl: false,
use_alt_exp: false,
enabled: enabled,
hidden: hidden,
order_by_li: order_by_li,
limit: limit,
offset: offset,
params_json: params_json,
params: params,
log_lvl: log_lvl
search_query,
order_by_li,
limit,
offset,
log_lvl
})
.then(async function (journal_obj_li_get_result) {
if (journal_obj_li_get_result) {
@@ -635,11 +636,6 @@ export async function qry__journal({
console.log('DB save completed.');
}
}
// db_save_ae_obj_li__journal({
// obj_type: 'journal',
// obj_li: journal_obj_li_get_result
// });
return journal_obj_li_get_result;
} else {
return [];
@@ -919,7 +915,7 @@ async function _process_generic_props<T extends Record<string, any>>({
for (const key in processed_obj) {
if (key.endsWith('_random')) {
const newKey = key.slice(0, -7); // Remove '_random' suffix
processed_obj[newKey] = processed_obj[key];
(processed_obj as any)[newKey] = processed_obj[key];
}
}
// Ensure 'id' is set from '[obj_type]_id_random'

View File

@@ -82,7 +82,7 @@ export async function load_ae_obj_id__journal_entry({
return ae_promises.load__journal_entry_obj;
}
// Updated 2025-03-15
// Updated 2026-01-02
export async function load_ae_obj_li__journal_entry({
api_cfg,
for_obj_type = 'journal',
@@ -120,33 +120,37 @@ export async function load_ae_obj_li__journal_entry({
);
}
// let enabled: string = (params.qry__enabled ?? 'enabled'); // all, disabled, enabled
// let hidden: string = (params.qry__hidden ?? 'not_hidden'); // all, hidden, not_hidden
// let limit: number = (params.qry__limit ?? 99); // 99
// let offset: number = (params.qry__offset ?? 0); // 0
let promise;
const params_json: key_val = {};
// console('params_json:', params_json);
ae_promises.load__journal_entry_obj_li = await api
.get_ae_obj_li_for_obj_id_crud_v2({
api_cfg: api_cfg,
if (for_obj_type === 'journal' && for_obj_id) {
promise = api.get_nested_obj_li_v3({
api_cfg,
parent_type: 'journal',
parent_id: for_obj_id,
child_type: 'journal_entry',
limit,
offset,
enabled,
hidden,
order_by_li,
log_lvl
});
} else {
promise = api.get_ae_obj_li_v3({
api_cfg,
obj_type: 'journal_entry',
for_obj_type: for_obj_type,
for_obj_id: for_obj_id,
use_alt_tbl: false,
use_alt_mdl: false,
use_alt_exp: false,
enabled: enabled,
hidden: hidden,
order_by_li: order_by_li,
limit: limit,
offset: offset,
params_json: params_json,
params: params,
log_lvl: log_lvl
})
for_obj_type,
for_obj_id,
enabled,
hidden,
limit,
offset,
order_by_li,
log_lvl
});
}
ae_promises.load__journal_entry_obj_li = await promise
.then(async function (journal_entry_obj_li_get_result) {
if (journal_entry_obj_li_get_result) {
if (try_cache) {
@@ -172,12 +176,6 @@ export async function load_ae_obj_li__journal_entry({
if (log_lvl) {
console.log('DB save completed.');
}
// await db_save_ae_obj_li__journal_entry({
// obj_type: 'journal_entry',
// obj_li: journal_entry_obj_li_get_result,
// log_lvl: log_lvl
// });
}
return journal_entry_obj_li_get_result;
} else {
@@ -340,8 +338,8 @@ export async function delete_ae_obj_id__journal_entry({
return ae_promises.delete__journal_entry_obj;
}
// This new function is using CRUD v2. This should allow for more flexibility in the queries.
// Updated 2025-06-04
// This new function is using CRUD V3 Search.
// Updated 2026-01-02
export async function qry__journal_entry({
api_cfg,
journal_id,
@@ -381,71 +379,55 @@ export async function qry__journal_entry({
try_cache?: boolean;
log_lvl?: number;
}) {
log_lvl = 1;
// log_lvl = 1;
if (log_lvl) {
console.log(`*** qry__journal_entry() *** journal_id=${journal_id}`);
}
const params_json: key_val = {};
params_json['qry'] = [];
const search_query: any = { and: [] };
if (qry_str) {
console.log('qry_str:', qry_str);
// let qry_param =
// {
// type: "AND",
// field: "qry_str",
// operator: "LIKE",
// value: qry_str
// };
const qry_param = {
type: '',
field: 'default_qry_str', // default is really just the standard FT string in the DB
operator: 'MATCH',
value: qry_str
};
params_json['qry'].push(qry_param);
// Using 'match' as per previous code's intent
search_query.and.push({ field: 'default_qry_str', op: 'match', value: qry_str });
}
if (qry_created_on) {
const qry_param = {
type: 'AND',
field: 'created_on',
operator: '>',
value: qry_created_on
};
params_json['qry'].push(qry_param);
search_query.and.push({ field: 'created_on', op: 'gt', value: qry_created_on });
}
if (qry_priority) {
console.log('qry_priority:', qry_priority);
const qry_param = {
type: 'AND',
field: 'priority',
operator: '=',
value: qry_priority
};
params_json['qry'].push(qry_param);
search_query.and.push({ field: 'priority', op: 'eq', value: qry_priority });
}
// Add context
if (journal_id) {
search_query.and.push({ field: 'journal_id_random', op: 'eq', value: journal_id });
}
// Add enabled/hidden filters
if (enabled === 'enabled') {
search_query.and.push({ field: 'enabled', op: 'eq', value: true });
} else if (enabled === 'not_enabled') {
search_query.and.push({ field: 'enabled', op: 'eq', value: false });
}
if (hidden === 'hidden') {
search_query.and.push({ field: 'hidden', op: 'eq', value: true });
} else if (hidden === 'not_hidden') {
search_query.and.push({ field: 'hidden', op: 'eq', value: false });
}
ae_promises.load__journal_entry_obj_li = await api
.get_ae_obj_li_for_obj_id_crud_v2({
.search_ae_obj_v3({
api_cfg: api_cfg,
obj_type: 'journal_entry',
for_obj_type: 'journal',
for_obj_id: journal_id,
use_alt_tbl: true, // NOTE: We want to use the alt table for journal entry searching???
use_alt_mdl: false,
use_alt_exp: false,
enabled: enabled,
hidden: hidden,
order_by_li: order_by_li,
limit: limit,
offset: offset,
params_json: params_json,
params: params,
log_lvl: log_lvl
search_query,
order_by_li,
limit,
offset,
log_lvl
})
.then(function (journal_entry_obj_li_get_result) {
if (journal_entry_obj_li_get_result) {
@@ -935,7 +917,7 @@ async function _process_generic_props<T extends Record<string, any>>({
for (const key in processed_obj) {
if (key.endsWith('_random')) {
const newKey = key.slice(0, -7); // Remove '_random' suffix
processed_obj[newKey] = processed_obj[key];
(processed_obj as any)[newKey] = processed_obj[key];
}
}
// Ensure 'id' is set from '[obj_type]_id_random'