feat(search): cross-entity tenant-scoped /api/v1/search endpoint (MAPPS-298) #332

Merged
YousifShkara merged 1 commit from feat/MAPPS-298-global-search-endpoint into main 2026-06-23 07:31:52 +02:00
Owner

A new search module exposes GET /api/v1/search?q=<text> that runs an ILIKE per principal entity table (tickets / contacts / companies / assets / projects), tenant-scopes every query, and returns a grouped envelope with the top 5 matches per kind plus a true count so the SPA can render "more results" affordances when the per-section cap clips.

Per-entity SQL details:

  • Tickets: matches title, ticket_number, or description; secondary line is the linked company name.
  • Contacts: matches first_name, last_name, or email; secondary is the linked company.
  • Companies: matches name; secondary is industry.
  • Assets: matches name, serial_number, or asset_tag; secondary is the linked company, falling back to the serial.
  • Projects: matches name; secondary is the linked company.

The handler is RequireAuth-gated (any authenticated tenant member can search) but the service tenant-scopes every query so users only see their own tenant's data. Per-entity permission boundaries (e.g. RequireFinance on contracts) are deliberately NOT applied to the search preview - clicking through goes to the entity's normal detail page which enforces its own access. Query string is trimmed, capped at 200 chars (matches q caps on other filters), and SQL-LIKE wildcards (%, _, \) are escaped so user-typed special chars match literally.

The client-side <GlobalSearch> top-bar component lands in a sibling PR on mokosh-apps; the SPA fans out one call here per debounced keystroke.

#MAPPS-298

A new `search` module exposes `GET /api/v1/search?q=<text>` that runs an ILIKE per principal entity table (tickets / contacts / companies / assets / projects), tenant-scopes every query, and returns a grouped envelope with the top 5 matches per kind plus a true count so the SPA can render "more results" affordances when the per-section cap clips. Per-entity SQL details: - Tickets: matches `title`, `ticket_number`, or `description`; secondary line is the linked company name. - Contacts: matches `first_name`, `last_name`, or `email`; secondary is the linked company. - Companies: matches `name`; secondary is `industry`. - Assets: matches `name`, `serial_number`, or `asset_tag`; secondary is the linked company, falling back to the serial. - Projects: matches `name`; secondary is the linked company. The handler is `RequireAuth`-gated (any authenticated tenant member can search) but the service tenant-scopes every query so users only see their own tenant's data. Per-entity permission boundaries (e.g. `RequireFinance` on contracts) are deliberately NOT applied to the search preview - clicking through goes to the entity's normal detail page which enforces its own access. Query string is trimmed, capped at 200 chars (matches `q` caps on other filters), and SQL-LIKE wildcards (`%`, `_`, `\`) are escaped so user-typed special chars match literally. The client-side `<GlobalSearch>` top-bar component lands in a sibling PR on mokosh-apps; the SPA fans out one call here per debounced keystroke. #MAPPS-298
feat(search): cross-entity tenant-scoped /api/v1/search endpoint (MAPPS-298)
All checks were successful
E2E / Playwright against staging (pull_request) Successful in 1m10s
Check / fmt + clippy + build + tests (pull_request) Successful in 4m6s
Create release / Create release from merged PR (pull_request) Successful in 4s
Integration / integration tests (pull_request) Successful in 10m14s
7c04a3bf70
A new `search` module exposes `GET /api/v1/search?q=<text>` that runs an ILIKE per principal entity table (tickets / contacts / companies / assets / projects), tenant-scopes every query, and returns a grouped envelope with the top 5 matches per kind plus a true count so the SPA can render "more results" affordances when the per-section cap clips.

Per-entity SQL details:
- Tickets: matches `title`, `ticket_number`, or `description`; secondary line is the linked company name.
- Contacts: matches `first_name`, `last_name`, or `email`; secondary is the linked company.
- Companies: matches `name`; secondary is `industry`.
- Assets: matches `name`, `serial_number`, or `asset_tag`; secondary is the linked company, falling back to the serial.
- Projects: matches `name`; secondary is the linked company.

The handler is `RequireAuth`-gated (any authenticated tenant member can search) but the service tenant-scopes every query so users only see their own tenant's data. Per-entity permission boundaries (e.g. `RequireFinance` on contracts) are deliberately NOT applied to the search preview - clicking through goes to the entity's normal detail page which enforces its own access. Query string is trimmed, capped at 200 chars (matches `q` caps on other filters), and SQL-LIKE wildcards (`%`, `_`, `\`) are escaped so user-typed special chars match literally.

The client-side `<GlobalSearch>` top-bar component lands in a sibling PR on mokosh-apps; the SPA fans out one call here per debounced keystroke.

#MAPPS-298
YousifShkara deleted branch feat/MAPPS-298-global-search-endpoint 2026-06-23 07:31:53 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/mokosh-server!332
No description provided.