Skip to content

Query Controls And Browsing State

This page records the reusable query architecture established across the Artists, Events, and Venues indexes. It should guide future event series, users, admin tables, and relationship-management surfaces that need searchable, filterable, sortable, or pageable collections.

Relationship sheets are one consumer of these patterns. Association-row modeling and relationship-management boundaries live in Domain Relationships.

Applied query-control contracts live in @wavemap/api-contracts. UI draft state and backend SQL composition should adapt to those contracts instead of each defining unrelated filter and sort shapes.

The durable applied shapes are:

  • TQueryFilterGroup
  • TQueryFilterClause
  • TQuerySortInstruction
  • TQueryControlEndpointCapabilities
  • QueryFilterGroupDTO
  • QuerySortInstructionDTO

Endpoint capability metadata should describe what a route supports:

  • Endpoint identity.
  • Supported criteria.
  • Optional query keys.
  • Label keys for localized UI chrome.
  • Sort capability and default direction.
  • Filter data type and allowed operation codes.
  • Pagination support, default page, default limit, max limit, and allowed limits.

The frontend should derive available sort/filter controls from endpoint capabilities. Page code can translate labels and adapt presentation, but it should not invent unsupported criteria.

The frontend draft model can be richer than the API model. For example, TQueryFilterGroupDraft carries editing-only fields such as condition type, selected option keys, and typeahead/select/combo metadata. The apply boundary normalizes draft groups into TQueryFilterGroup[] and drops incomplete clauses before calling an API or serializing URL state.

Filter semantics are intentionally bounded:

  • Clauses within one filter group use that group’s joinOperator.
  • Separate filter groups are implicitly ANDed together by backend composition.
  • The first artists-page pass does not expose configurable joins between groups.
  • Unsupported criteria, unsupported operations, and empty clauses should be ignored or sanitized at boundaries rather than partially corrupting the query.

Backend query composition should use shared mechanics and explicit entity-specific builders:

  • Shared pagination helpers resolve page, limit, offset, and response pagination metadata.
  • Shared filter composition joins groups and clauses.
  • Shared sort composition appends stable fallback ordering.
  • Entity-specific builders map criteria IDs to SQL clauses and order expressions.

Always append stable fallback sort clauses after user-requested sorts. This keeps paginated browsing deterministic when the active sort field has ties.

Data-heavy index pages should use page-ready summary DTOs instead of returning full nested graphs.

The Artists Index established the page-family pattern, Events proved it across a schedule-heavy domain, and Venues now proves the same ownership split with a third queryable collection:

  • GET /artists supports the page-browsing route.
  • ArtistSummaryDTO includes the row identity, display name, computed profile image URL, optional description, event count, and lightweight external links.
  • The response includes pagination metadata.
  • Full nested event, venue, media, and relationship graphs stay out of the list payload.
  • Profile media and links are hydrated only for the artists on the current page.
  • POST /events/query composes a page-ready EventSummaryDTO with schedule and attendance summaries while preserving stable server ordering, pagination, and capability-owned filters and sorts.
  • POST /venues/query returns compact, page-ready Venue summaries for grid and list modes while preserving Venue-owned status, location, search, sort, and pagination capabilities.
  • POST /venues/map/query is a separate non-paginated collection projection for Venue map mode. It reuses supported Venue filters, returns only active Venues with exact published coordinates, and either returns the complete matching collection or a stable 422 response when the 2,000-result ceiling would be exceeded. It excludes page, page size, and presentation sorting because those values do not change which points belong on the map.

Keep a page-browsing endpoint distinct from a future flexible embedded query endpoint when the use cases are different. For artists, GET /artists remains the index browsing route, while POST /artists/query is deferred until a concrete embedded or secondary artist-querying consumer needs it.

Search is a primary page action, not just another side-panel filter. The Artists and Events pages use a larger typeahead/search surface for elevated search and apply submitted searches to the browsing state. Submitted search should:

  • Reset pagination to page 1.
  • Preserve active filters and sorts.
  • Stay visible in the URL.
  • Produce a combined empty state when search plus filters returns no rows.
  • Keep direct result selection separate from the view all matching results action.

Typeahead and submitted-search matching should use the same domain normalization strategy even when their request shapes differ. Events typeahead and submitted Events Index search both use normalized-name substring matching plus trigram similarity; the frontend selects the supported similar-to operation for the applied name filter. This preserves case-folding, punctuation removal, de-accenting, local-script matching, and typo tolerance across preview and full-result paths without adding a second backend search contract. The shared database normalization function preserves Unicode letters and numbers, replaces other character runs with spaces, collapses whitespace, and then applies lowercase and accent folding. Do not narrow that function to an ASCII-only character class: Artist, Event, and Venue normalized-name columns all depend on it, and existing rows must be backfilled when its behavior changes.

Use compact typeahead components for small form-field search and larger typeahead presentations for modal, page, or dedicated search contexts. Shared typeahead mechanics should live in headless controller hooks, while data fetching and DTO-to-row rendering stay in page/domain wrappers.

Treat route URL state as reloadable, shareable, transient browsing state.

The current reusable URL-state helpers cover:

  • page
  • limit
  • filters
  • sorts

Page-specific helpers should add only page-owned state, such as artist view mode or submitted search query. Invalid pagination values should fall back to defaults. Unsupported sort and filter criteria should be dropped using endpoint capability metadata.

Saved views, also called page query presets, are the durable named layer above URL state. They should be typed by page and schema version.

For artists.index, events.index, and venues.index, the saved state currently includes:

  • View mode.
  • Page.
  • Page size.
  • Search query.
  • Active filters.
  • Active sorts.

Display-only preferences should stay out of saved views until the page exposes them as first-class controls. The Artists page therefore excludes table column visibility/order and gallery density/card preferences for now. The Events page also explicitly projects its durable state before persistence, excluding search-modal visibility, sort/filter-panel visibility, and panel focus from saved views. The Venues page persists its selected grid, list, or map mode, but no map provider, viewport, marker-selection, or runtime state enters the preset contract.

Page-owned preset contracts should use the shared page-query-preset contract-family factory so list and mutation DTOs cannot drift on page key, schema version, or state shape. On the frontend, the shared controller owns guest/account persistence and active-preset lifecycle, the import registry owns authenticated device-to-account discovery, and the shared popover owns interaction. Each page remains responsible for its durable state projection, migration adapter, query-identity changes, URL commit, and domain copy.

Guest users persist saved views in local storage. Authenticated users persist them through the page query preset API. When local guest presets differ from account presets after login, the app can offer an advisory import path without deleting the device-local copies.

When multiple views share query controls, keep their durable state page-owned even when a view needs a different result projection.

The Artists, Events, and Venues pages keep view mode, pagination, search, filters, and sorts in the page frame. Their table and gallery/feed presentations use paginated page-ready results. Venue map mode instead enables the focused map query and disables the paginated Venue query. Search and filters participate in both Venue request identities, while page, page size, and presentation sorts remain in URL and saved-view state only so returning to grid or list restores the prior browsing position.

The Venue map does not currently geofence requests by camera bounds. It receives the complete matching published-point collection, up to the explicit ceiling, then clusters and projects those items in the browser. Panning, zooming, initial camera fit, and marker selection are runtime-local: they neither mutate browser history nor refetch the collection. A viewport query should be introduced only when measured payload, query, or marker costs justify a different completeness contract.

Pages can switch views immediately while URL state catches up, and inactive views may stay mounted when preserving local layout or animation state matters.

Pagination should follow endpoint capabilities:

  • Use endpoint allowedLimits when present.
  • Fall back to shared default allowed limits filtered by max limit.
  • Reset to page 1 when page size, search, or a dataset-changing query control changes.
  • Keep pagination metadata in the API response so views do not infer totals from the current page length.

For new queryable collection pages, prefer coverage at the contract boundaries:

  • API contract tests when DTO constraints or preset migration rules are non-obvious.
  • Backend route tests for pagination parsing, bad query params, response shape, and keyed errors.
  • DB-backed tests for filter SQL, sort SQL, aggregate filters, stable fallback ordering, search behavior, and totals.
  • DB-backed local-script cases when a shared normalization or entity-name query changes.
  • Frontend API and hook tests for serialized query params and query keys.
  • URL-state tests for parsing, sanitization, serialization, unsupported criteria, and invalid limits.
  • Page integration tests for empty/loading/error states, view switching, search submission, saved-view behavior, and query-control state preservation.

The following item is intentionally not treated as settled architecture yet:

  • When POST /artists/query or similar flexible secondary query endpoints become necessary.