From c7335bbc3ec70f683c24d3d7649ef9c24019e2d3 Mon Sep 17 00:00:00 2001 From: Scott Idem Date: Fri, 15 May 2026 12:32:26 -0400 Subject: [PATCH] fix(event_session): restore event_presentation/presenter_li_qry_str fields These fields from v_event_session_w_file_count were lost during the v1/v2 -> v3 migration. Added to Event_Session_Base model and to searchable_fields in the event_session object definition. Fields are only available via the alt view (v_event_session_w_file_count). To search: use ?view=alt on the nested search endpoint. To retrieve: use ?inc_file_count=true on the GET endpoint. Also: - Updated ARCH__V3_DEVELOPMENT_STANDARDS.md: expanded Field Evolution Checklist with alt-view field rules, Docker restart requirement, and documented the ?view= parameter as a live (not proposed) feature. - Updated TODO__Agents.md: marked migration gap audit as complete. - Added regression test to test_e2e_v3_search_engine.py. --- app/models/event_session_models.py | 2 + app/object_definitions/events_presentation.py | 3 +- .../ARCH__V3_DEVELOPMENT_STANDARDS.md | 15 ++++- documentation/TODO__Agents.md | 1 + tests/e2e/test_e2e_v3_search_engine.py | 59 ++++++++++++++++++- 5 files changed, 75 insertions(+), 5 deletions(-) diff --git a/app/models/event_session_models.py b/app/models/event_session_models.py index 350caee..5df6332 100644 --- a/app/models/event_session_models.py +++ b/app/models/event_session_models.py @@ -139,6 +139,8 @@ class Event_Session_Base(BaseModel): created_on: Optional[datetime.datetime] = None updated_on: Optional[datetime.datetime] = None default_qry_str: Optional[str] # Default query string used for searching and filtering sessions. Updated using SQL triggers and a SQL function + event_presentation_li_qry_str: Optional[str] # Concatenated query string of presentation data for this session (from v_event_session_w_file_count) + event_presenter_li_qry_str: Optional[str] # Concatenated query string of presenter data for this session (from v_event_session_w_file_count) # Including convenience data # This is only for convenience. Probably going to keep unless it causes a problem. diff --git a/app/object_definitions/events_presentation.py b/app/object_definitions/events_presentation.py index bfe161a..2e3d404 100644 --- a/app/object_definitions/events_presentation.py +++ b/app/object_definitions/events_presentation.py @@ -135,7 +135,8 @@ events_presentation_obj_li = { 'poc_person_full_name', 'public', 'public_hide', 'hide_event_launcher', 'priority', 'sort', 'group', 'notes', 'created_on', 'updated_on', - 'default_qry_str', 'event_location_name' + 'default_qry_str', 'event_location_name', + 'event_presentation_li_qry_str', 'event_presenter_li_qry_str' ], }, 'event_track': { diff --git a/documentation/ARCH__V3_DEVELOPMENT_STANDARDS.md b/documentation/ARCH__V3_DEVELOPMENT_STANDARDS.md index 73d58b3..68afaa1 100644 --- a/documentation/ARCH__V3_DEVELOPMENT_STANDARDS.md +++ b/documentation/ARCH__V3_DEVELOPMENT_STANDARDS.md @@ -45,14 +45,25 @@ When a table or view gains, loses, or renames fields, keep the API contract and 3. Update `searchable_fields` in `app/object_definitions/` only for fields that should be searchable. 4. Add write-only, virtual, or view-only fields to `fields_to_exclude_from_db` when they must not be persisted. 5. Run the schema/search E2E tests that cover the object type before handing the change off. +6. **Restart the Docker API containers** (`docker compose restart ae_api`) — Python file changes inside containers are not picked up until restart. For `archive_content`, the public field set now includes `external_id` and `code`, and future additions should follow the same order of operations. -### Response Views (Proposed) -- Implement a `view` parameter (e.g., `?view=rich`) to allow clients to request joined data without using legacy `use_alt_tbl` flags. +#### Alt-view fields (fields only in `tbl_alt`) +Some objects have a richer alternate SQL view (`tbl_alt`) that adds JOINed/computed columns absent from the default view (`tbl_default`). For example, `event_session` uses `v_event_session_w_file_count` as its alt view (triggered by `?view=alt` on search, or `?inc_file_count=true` on GET). + +- Fields from `tbl_alt` **must still be declared in the Pydantic model** and in `searchable_fields` — Pydantic strips undeclared fields, and the search whitelist rejects unknown field names regardless of the view. +- When adding such a field, add a comment noting which view provides it (e.g., `# from v_event_session_w_file_count`). +- Searching by an alt-view field on the default endpoint returns `400 Unknown column` — this is correct behaviour. Clients must pass `?view=alt` to use those fields in a search. +- **Known alt-view fields restored May 2026:** `event_presentation_li_qry_str`, `event_presenter_li_qry_str` (event_session); `account_name`, `account_code`, and related convenience fields (site_domain). + +### Response Views (`?view=` parameter) +- The nested search router (`api_crud_v3_nested.py`) already supports `?view=` to switch between registered views. `view=default` uses `tbl_default`; `view=alt` uses `tbl_alt`; additional named views can be added to the object registry as `tbl_` / `mdl_`. +- Flat search (`api_crud_v3.py`) does not yet support `?view=` — it always uses `tbl_default`. ## 4. Stability Rules 1. **Baby Step Testing**: Restart Docker and verify root health after *every* modular change. 2. **Avoid Shadowing**: Never name a module part of the `app.` package the same as a common instance variable (e.g., avoid `app.middleware` package if you use `app = FastAPI()`). 3. **Deferred Imports**: Use `from app.db_sql import ...` *inside* functions in library modules to prevent circular dependency traps. +4. **Model changes require container restart**: Editing Python files on the host does not hot-reload inside Docker. Always run `docker compose restart ae_api` after model or object-definition changes, then re-run E2E tests. diff --git a/documentation/TODO__Agents.md b/documentation/TODO__Agents.md index b75be2e..65fdc93 100644 --- a/documentation/TODO__Agents.md +++ b/documentation/TODO__Agents.md @@ -34,6 +34,7 @@ - [x] Whitelist `account_id` in all Event search definitions. - [x] Audit Relational "Low-Priority" Models (Address, Contact, DataStore). - [x] **V3 Uniform Lookup System:** Phase 1 & 2 Complete. + - [x] **Restore alt-view convenience fields lost in v1→v3 migration (May 2026):** `site_domain` (`account_name`, `account_code`, `account_enable`, `account_enable_from/to`, `site_enable_from/to`, `site_domain_access_key`, `logo_path`, `style_href`, `script_src`, `google_tracking_id`) and `event_session` (`event_presentation_li_qry_str`, `event_presenter_li_qry_str`). Fields added to Pydantic models and `searchable_fields`. Alt-view fields require `?view=alt` for search. - [ ] Verify SQL Views join in all required `_random` IDs for performance. - [ ] **Step 2:** Coordination (Verify Frontend uses `x-account-id` instead of token). - [ ] **Step 3:** Frontend V3 WebSocket integration test — queued after IDAA-specific work. Backend is ready (auth wired, heartbeat presence refresh confirmed, unit tests passing). Frontend guide updated at `GUIDE__AE_API_V3_for_Frontend_websockets.md`. diff --git a/tests/e2e/test_e2e_v3_search_engine.py b/tests/e2e/test_e2e_v3_search_engine.py index 99d6a25..8d5f804 100644 --- a/tests/e2e/test_e2e_v3_search_engine.py +++ b/tests/e2e/test_e2e_v3_search_engine.py @@ -64,17 +64,72 @@ def test_extra_filters(): resp = requests.get(f"{API_BASE}/user/?enabled=all&hidden=all", headers=get_headers()) print_result("Bypass Filters (enabled=all)", resp.status_code == 200) +def test_event_session_qry_str_fields(): + """ + Regression test for event_presentation_li_qry_str and event_presenter_li_qry_str. + These fields were lost during the v1/v2 -> v3 migration and restored May 2026. + They live in v_event_session_w_file_count (triggered by ?inc_file_count=true). + + Demo session: DOW3h7v6H42 "How To Do Things" under Demo event pjrcghqwert + """ + print("\n--- Testing event_session qry_str fields (regression: May 2026) ---") + + EVENT_ID = "pjrcghqwert" + SESSION_ID = "DOW3h7v6H42" + + headers = { + "Content-Type": "application/json", + "X-Aether-API-Key": API_KEY, + "x-no-account-id": "bypass" + } + + # 1. Verify fields are returned in the GET response when inc_file_count=true + url = f"{API_BASE}/event_session/{SESSION_ID}?inc_file_count=true" + resp = requests.get(url, headers=headers) + ok = resp.status_code == 200 + print_result("GET event_session with inc_file_count", ok, f"(status={resp.status_code})") + if ok: + data = resp.json().get("data", {}) + has_pres = "event_presentation_li_qry_str" in data + has_presenter = "event_presenter_li_qry_str" in data + print_result("Field present: event_presentation_li_qry_str", has_pres, + f"(value={data.get('event_presentation_li_qry_str')!r})") + print_result("Field present: event_presenter_li_qry_str", has_presenter, + f"(value={data.get('event_presenter_li_qry_str')!r})") + + # 2. Verify searching by event_presentation_li_qry_str via ?view=alt (v_event_session_w_file_count) + # These fields only exist in the alt view, so ?view=alt is required. + search_url = f"{API_BASE}/event/{EVENT_ID}/event_session/search?view=alt" + query = {"and": [{"field": "event_presentation_li_qry_str", "op": "like", "value": "%"}]} + resp = requests.post(search_url, headers=headers, json=query) + print_result("Search by event_presentation_li_qry_str (?view=alt)", resp.status_code == 200, + f"(status={resp.status_code})") + + # 3. Verify searching by event_presenter_li_qry_str via ?view=alt + query = {"and": [{"field": "event_presenter_li_qry_str", "op": "like", "value": "%"}]} + resp = requests.post(search_url, headers=headers, json=query) + print_result("Search by event_presenter_li_qry_str (?view=alt)", resp.status_code == 200, + f"(status={resp.status_code})") + + # 4. Confirm search on default view still rejects these fields (expected 400 — not in v_event_session) + search_url_default = f"{API_BASE}/event/{EVENT_ID}/event_session/search" + query = {"and": [{"field": "event_presentation_li_qry_str", "op": "like", "value": "%"}]} + resp = requests.post(search_url_default, headers=headers, json=query) + print_result("Search on default view correctly rejects qry_str field (expect 400)", resp.status_code == 400, + f"(status={resp.status_code})") + if __name__ == "__main__": print(f"Starting Consolidated Search Engine E2E Suite") print(f"Target: {API_BASE}") - + start_time = time.time() try: test_basic_operators() test_registry_fields() test_nested_search() test_extra_filters() + test_event_session_qry_str_fields() except Exception as e: print(f"💥 Suite Error: {e}") - + print(f"\nSuite completed in {time.time() - start_time:.2f}s")