fix(standardization): enforce V3 String-Only ID mapping and harden account isolation

- Added 'account_id_random' to persistent property list to fix local search isolation.
- Standardized search body and helpers to support mandatory x-account-id headers.
- Refactored LiveQuery to use synchronous dependency tracking ($derived.by) for reliable search updates.
- Broadened server-side search to handle inclusive OR logic on the client, preventing disappearing results.
- Updated task list with IDAA Recovery Meeting testing status.
This commit is contained in:
Scott Idem
2026-02-05 17:56:13 -05:00
parent f4f3f99927
commit 6c2c37ff06
5 changed files with 55 additions and 69 deletions

15
TODO.md
View File

@@ -19,6 +19,21 @@ This is a list of tasks to be completed before the next event/show/conference.
- [x] **Session Search:** Modernized logic, fixed layout bugs and icon exports. (Completed 2026-01-27) - [x] **Session Search:** Modernized logic, fixed layout bugs and icon exports. (Completed 2026-01-27)
- [x] **Exhibit Search:** Standardized Exhibitor and Lead Tracking reactive search. (Completed 2026-01-28) - [x] **Exhibit Search:** Standardized Exhibitor and Lead Tracking reactive search. (Completed 2026-01-28)
## Urgent Tasks (Feb 5, 2026)
1. **IDAA Module Verification:**
- [ ] **Bulletin Board (BB):** Verify Post/Comment create/edit; test email notifications for staff and original posters; verify anonymous toggle; test file attachments on Posts; optimize inline image display (collapse/expand).
- [ ] **Recovery Meetings:** Verify all search filters; audit full editing workflow; verify special code/copy buttons for Zoom/Jitsi.
- [ ] **View and Edit:** View and Edit of an IDAA Recovery Meeting still needs to be fully tested (70% done).
2. **Events - Presentation Management:**
- [ ] **Basics:** Ensure all core presentation management features are fully functional.
- [ ] **Uploads:** Verify direct file uploads to Presenter, Session, Location, and Event.
3. **Events - Badges:**
- [ ] **Rendering:** Verify Badge Template pulling and correct rendering.
- [ ] **Editing:** Ensure basic fields (name, affiliations, badge type) are editable.
## Urgent Tasks (Feb 4, 2026) ## Urgent Tasks (Feb 4, 2026)
1. **IDAA Module Verification:** 1. **IDAA Module Verification:**

View File

@@ -97,6 +97,7 @@ interface GetAeObjLiV3Params {
offset?: number; offset?: number;
order_by_li?: Record<string, 'ASC' | 'DESC'> | Record<string, 'ASC' | 'DESC'>[] | null; order_by_li?: Record<string, 'ASC' | 'DESC'> | Record<string, 'ASC' | 'DESC'>[] | null;
delay_ms?: number; delay_ms?: number;
headers?: key_val;
log_lvl?: number; log_lvl?: number;
} }
@@ -112,6 +113,7 @@ export async function get_ae_obj_li_v3({
offset = 0, offset = 0,
order_by_li = null, order_by_li = null,
delay_ms = 0, delay_ms = 0,
headers = {},
log_lvl = 0 log_lvl = 0
}: GetAeObjLiV3Params) { }: GetAeObjLiV3Params) {
// 1. Build V3 Endpoint // 1. Build V3 Endpoint
@@ -135,12 +137,14 @@ export async function get_ae_obj_li_v3({
console.log('*** get_ae_obj_li_v3 ***'); console.log('*** get_ae_obj_li_v3 ***');
console.log('Endpoint:', endpoint); console.log('Endpoint:', endpoint);
console.log('Params:', params); console.log('Params:', params);
console.log('Headers:', headers);
} }
return await get_object({ return await get_object({
api_cfg, api_cfg,
endpoint, endpoint,
params, params,
headers,
log_lvl log_lvl
}); });
} }

View File

@@ -455,11 +455,9 @@ export async function update_ae_obj__event({
* Unified Search for Events (V3 API) * Unified Search for Events (V3 API)
* *
* STRATEGY: Hybrid Search/Filter * STRATEGY: Hybrid Search/Filter
* 1. Server-side (V3 Search): Used for high-level filtering (Account, Type, String Search). * 1. Server-side (V3 Search): Used for text search (qry_str) to reduce payload.
* This reduces the data payload significantly. * 2. Client-side (Filter Layer): Handles all other filters (Type, Location, Person)
* 2. Client-side (Filter Layer): Used for complex logic that the backend Search API * to ensure correct inclusive OR logic and stable ID matching.
* may not handle natively yet, such as inclusive 'OR' logic for Physical/Virtual
* meetings or specific person ID matching.
*/ */
export async function search__event({ export async function search__event({
api_cfg, api_cfg,
@@ -502,50 +500,30 @@ export async function search__event({
let result_li: ae_Event[] | null = null; let result_li: ae_Event[] | null = null;
// Use V3 Search if ANY filter is active to ensure we query the full database if (qry_str && qry_str.trim().length > 0) {
const has_active_filters = (qry_str && qry_str.trim().length > 0) || // Option A: Active Text Search
qry_physical === true ||
qry_virtual === true ||
(qry_type && qry_type !== 'all' && qry_type !== '') ||
qry_person_id ||
qry_conference !== null;
if (has_active_filters) {
// Option A: Active Search (Server-side filtering)
const search_query: any = { const search_query: any = {
and: [] and: []
}; };
const params: key_val = {}; const params: key_val = {};
if (qry_str && qry_str.trim().length > 0) { search_query.and.push({ field: 'default_qry_str', op: 'like', value: `%${qry_str.trim()}%` });
// Use default_qry_str for searching as requested params['lk_qry'] = { 'default_qry_str': qry_str.trim() };
// Using 'like' with wildcards and populating params['lk_qry'] to match working event_session logic
search_query.and.push({ field: 'default_qry_str', op: 'like', value: `%${qry_str.trim()}%` }); if (for_obj_id) {
params['lk_qry'] = { 'default_qry_str': qry_str.trim() }; // V3 Standard: Use random string ID for body filters.
// The API resolves this to the integer column automatically.
search_query.and.push({ field: `${for_obj_type}_id_random`, op: 'eq', value: for_obj_id });
} }
// NOTE: We do NOT push 'physical' and 'virtual' to the server-side query here. // NOTE: We do NOT push 'physical' and 'virtual' to the server-side query here.
// The V3 Search API uses AND logic, which would exclude meetings that are // The V3 Search API uses AND logic for the body, which would exclude
// only physical or only virtual. We let the Client-side Filter handle this below. // meetings that are only physical or only virtual if both filters are active.
// We handle this in the Client-side Filter Layer below for correct OR logic.
if (qry_type && qry_type !== 'all' && qry_type !== '') {
search_query.and.push({ field: 'type', op: 'eq', value: qry_type });
}
if (for_obj_id) {
search_query.and.push({ field: `${for_obj_type}_id`, op: 'eq', value: for_obj_id });
}
if (enabled === 'enabled') search_query.and.push({ field: 'enable', op: 'eq', value: 1 });
else if (enabled === 'not_enabled') search_query.and.push({ field: 'enable', op: 'eq', value: 0 });
if (hidden === 'hidden') search_query.and.push({ field: 'hide', op: 'eq', value: 1 });
else if (hidden === 'not_hidden') search_query.and.push({ field: 'hide', op: 'eq', value: 0 });
result_li = await api.search_ae_obj_v3({ result_li = await api.search_ae_obj_v3({
api_cfg, api_cfg,
obj_type: 'event', obj_type: 'event',
// Inject header context for Auth but keep body context for Filtering
headers: { 'x-account-id': for_obj_id }, headers: { 'x-account-id': for_obj_id },
search_query, search_query,
params, params,
@@ -558,8 +536,7 @@ export async function search__event({
log_lvl log_lvl
}); });
} else { } else {
// Option B: List All (No filters active) // Option B: List All
// Fallback to standard list retrieval to ensure we get results when the search bar is empty.
result_li = await api.get_ae_obj_li_v3({ result_li = await api.get_ae_obj_li_v3({
api_cfg, api_cfg,
obj_type: 'event', obj_type: 'event',
@@ -571,18 +548,18 @@ export async function search__event({
limit, limit,
offset, offset,
order_by_li, order_by_li,
headers: { 'x-account-id': for_obj_id },
log_lvl log_lvl
}); });
} }
// Handle potential V3 API envelope { data: [], meta: ... } or direct array // Handle potential V3 API envelope
let valid_result_li: ae_Event[] = []; let valid_result_li: ae_Event[] = [];
if (Array.isArray(result_li)) { if (Array.isArray(result_li)) {
valid_result_li = result_li; valid_result_li = result_li;
} else if (result_li && typeof result_li === 'object' && Array.isArray((result_li as any).data)) { } else if (result_li && typeof result_li === 'object' && Array.isArray((result_li as any).data)) {
valid_result_li = (result_li as any).data; valid_result_li = (result_li as any).data;
} else { } else {
// If null, undefined, or unknown format, return empty list to prevent iteration errors
return []; return [];
} }
@@ -601,19 +578,11 @@ export async function search__event({
}); });
} }
/** // Client-side Filter Layer
* Client-side Filter Layer
*
* WHY: The V3 Search API defaults to 'AND' logic for its top-level filters.
* Some UI requirements (like showing meetings that are EITHER physical OR virtual)
* are more reliably handled here after the broad server-side fetch.
* This also ensures that complex person-id matching across multiple legacy fields
* remains consistent without requiring massive backend search indices.
*/
const filtered_obj_li = processed_obj_li.filter((ev: any) => { const filtered_obj_li = processed_obj_li.filter((ev: any) => {
// Handle conference filter // Handle conference filter
if (qry_conference != null) { if (qry_conference != null) {
const ev_conf = ev.conference === true || ev.conference === 1 || ev.conference === '1'; const ev_conf = ev.conference == true;
if (ev_conf !== !!qry_conference) return false; if (ev_conf !== !!qry_conference) return false;
} }
@@ -624,8 +593,8 @@ export async function search__event({
// Location Filtering (Inclusive OR logic) // Location Filtering (Inclusive OR logic)
if (qry_physical === true || qry_virtual === true) { if (qry_physical === true || qry_virtual === true) {
const ev_physical = ev.physical === true || ev.physical === 1 || ev.physical === '1'; const ev_physical = ev.physical == true;
const ev_virtual = ev.virtual === true || ev.virtual === 1 || ev.virtual === '1'; const ev_virtual = ev.virtual == true;
let match = false; let match = false;
if (qry_physical === true && ev_physical) match = true; if (qry_physical === true && ev_physical) match = true;
@@ -650,7 +619,7 @@ export async function search__event({
}); });
if (log_lvl) { if (log_lvl) {
console.log(`Filter results (V3 Search): Input=${processed_obj_li.length}, Output=${filtered_obj_li.length}`); console.log(`Filter results (Hybrid): Input=${processed_obj_li.length}, Output=${filtered_obj_li.length}`);
} }
return filtered_obj_li.slice(0, limit); return filtered_obj_li.slice(0, limit);
@@ -797,6 +766,7 @@ export const properties_to_save = [
'event_id', 'event_id',
'code', 'code',
'account_id', 'account_id',
'account_id_random',
'conference', 'conference',
'type', 'type',
'name', 'name',

View File

@@ -42,7 +42,7 @@ export async function load({ params, parent }) {
qry_conference: false, // IDAA Recovery Meetings are not standard conferences qry_conference: false, // IDAA Recovery Meetings are not standard conferences
enabled: 'enabled', enabled: 'enabled',
hidden: 'not_hidden', hidden: 'not_hidden',
limit: 199, limit: 499,
order_by_li: { order_by_li: {
priority: 'DESC', priority: 'DESC',
sort: 'DESC', sort: 'DESC',

View File

@@ -42,18 +42,15 @@
/** /**
* Stable LiveQuery Pattern (Aether UI V3) * Stable LiveQuery Pattern (Aether UI V3)
* *
* WHY: We wrap liveQuery in $derived to ensure that Svelte recreates the * WHY: We use $derived.by to synchronously track the 'event_id_random_li' prop.
* Dexie observable whenever the input props (event_id_random_li) change. * This ensures that whenever the parent search logic updates the ID list,
* * Svelte recreates this Dexie observable and triggers a fresh UI render.
* TWO SCENARIOS:
* 1. Specific IDs: If event_id_random_li is provided (from the Search Orchestrator),
* we use bulkGet for high-performance targeted retrieval.
* 2. Fallback: If no IDs are provided, we perform a broad search for the account.
*/ */
let lq__event_obj_li = $derived( let lq__event_obj_li = $derived.by(() => {
liveQuery(async () => { // SVELTE 5 REACTIVITY: Track IDs synchronously
const ids = event_id_random_li; const ids = event_id_random_li;
return liveQuery(async () => {
// SCENARIO 1: Specific IDs provided (from Search Fast Path or API) // SCENARIO 1: Specific IDs provided (from Search Fast Path or API)
if (Array.isArray(ids)) { if (Array.isArray(ids)) {
if (ids.length > 0) { if (ids.length > 0) {
@@ -72,7 +69,7 @@
if (log_lvl) console.log(`Wrapper LQ: Fallback search for ${link_to_type}: ${link_to_id}`); if (log_lvl) console.log(`Wrapper LQ: Fallback search for ${link_to_type}: ${link_to_id}`);
const base_query = db_events.event const base_query = db_events.event
.where(dq__where_type_id_val) .where(dq__where_type_id_val)
.equals(dq__where_eq_id_val); .equals(link_to_id);
if (order_by == 'name') { if (order_by == 'name') {
return await base_query return await base_query
@@ -87,8 +84,8 @@
} }
} }
return null; return null;
}) });
); });
</script> </script>
{#if $lq__event_obj_li} {#if $lq__event_obj_li}