This file highlights the important, discoverable conventions and workflows an AI coding agent needs to be productive in this Rails app.
-
Big picture: TIMDEX UI is a Rails 7 app that orchestrates searches across two backends: TIMDEX (GraphQL) and Primo (legacy API). Core request flow is implemented in
app/controllers/search_controller.rbwhich: validates params, builds an enhanced query (Enhancer->QueryBuilder), then routes to Primo or Timdex fetchers (or both for thealltab). Results are normalized byNormalizePrimoResults/NormalizeTimdexResultsand analyzed byAnalyzer. -
Fulfillment links: This application considers a link a fulfillment link if it takes the user to the resource directly. Sometimes we don't have a fulfillment link, so the user will click the Title of the result to view a full record view in the source system. Fulfillment links are provided from multiple ways. Primo data sometimes returns PDF or HTML links to the resource directly. If we have a DOI or PMID, we lookup fulfillment links via LibKey; in data comes back from LibKey, we prefer these links over the Primo links. If LibKey does not provide data, or if
FEATURE_OA_ALWAYSis enabled, we will look for OpenAccess links in OpenAlex via DOI or PMID. For journal records that have an ISSN, we also use Browzine to get a fulfillment link. -
GraphQL integration: GraphQL queries live on the Ruby side using
graphql-clientandTimdexBase::Client. Seeapp/models/timdex_search.rbfor the queries (BaseQuery,GeoboxQuery,GeodistanceQuery,AllQuery). The canonical schema is stored atconfig/schema/schema.json. Update schema via the Rails console:GraphQL::Client.dump_schema(TimdexBase::HTTP, 'config/schema/schema.json')
-
Caching & query keys:
SearchController#query_timdexusesRails.cacheand generates stable cache keys withgenerate_cache_key(MD5 of a sorted query hash). When changing query shape, update cache key logic or clear cache accordingly. -
Feature flags & environment: Feature toggles are read with
Feature.enabled?(:name)via theFeatureclass (seeapp/models/feature.rb). This replaces the older flipflop gem-based approach with a simpler, stateless environment variable system. Valid flags are:Flag Purpose FEATURE_BOOLEAN_PICKERAllow users to choose AND/OR boolean logic in searches FEATURE_GEODATAEnable geospatial search (bounding box and radius-based queries); defaults to false FEATURE_OA_ALWAYSAlways do OpenAlex lookups when DOI or PMID is detected rather than only when LibKey does not return data FEATURE_RECORD_LINKShow "View full record" link in search results FEATURE_SIMULATE_SEARCH_LATENCYAdd 1s minimum delay to search results for testing UX behavior FEATURE_TAB_PRIMO_ALLDisplay combined Primo (CDI + Alma) results tab FEATURE_TAB_TIMDEX_ALLDisplay combined TIMDEX results tab FEATURE_TAB_TIMDEX_ALMADisplay Alma-only TIMDEX results tab Essential ENV vars for core functionality:
TIMDEX_GRAPHQL,PRIMO_API_URL,PRIMO_API_KEY,RESULTS_PER_PAGE,TIMDEX_INDEX,TIMDEX_SOURCES. Filter customization:FILTER_*(e.g.,FILTER_LANGUAGE,FILTER_CONTENT_TYPE) andACTIVE_FILTERS(comma-separated list controlling visibility/order of filters; note that filter aggregation keys in the schema use*Filtersuffix, e.g.,languageFilter,contentTypeFilter). Tests rely on.env.testvalues for VCR cassette generation and useClimateControlgem to mock feature flags. -
Parallel fetching & multi-source pagination: The
alltab usesMergedSearchService(withMergedSearchPaginator) to fetch Primo and Timdex concurrently viaThread.new, then intelligently merges paginated results. Primo has a practical offset limit (~960 records); when this limit is reached, the UI shows ashow_continuationflag to indicate search is exhausted. Merged totals are cached for 12 hours. Be careful when refactoring to preserve thread-safety, caching semantics, and offset limit handling. -
JS stack & conventions: Rails importmap is in use (
importmap-rails). JavaScript entry isapp/javascript/application.js. Stimulus controllers live inapp/javascript/controllersand are imported byimportmapviaconfig/importmap.rb. Key controllers includecontent_loader_controller.js(dynamic content loading) and tab management viasource_tabs.js(which handles geospatial UI state). Prefer small, focused changes to Stimulus controllers rather than heavy bundler-based rewrites. -
Geospatial search pattern: When
FEATURE_GEODATAis enabled,SearchControllersupports two additional query types beyond keyword search: geobox (bounding box with min/max latitude/longitude) and geodistance (radius search with distance, latitude, longitude). These are implemented viaGeoboxQueryandGeodistanceQueryinTimdexSearchand routed to a dedicatedresults_geoview. Use case: GEODATA (geographic discovery tool) app uses this for location-based discovery; other apps can leverage the same pattern for their own needs. Input validation guards these features:validate_geobox_*andvalidate_geodistance_*methods check coordinate ranges and required params before querying. When changing geospatial UX or params, update these validations and corresponding flash messages. -
Errors & UX flows: Search errors are extracted in
SearchController(extract_errors) and rendered to the UI. When implementing new search types or filters, ensure error handling covers both success and failure cases, and update flash messages for clarity. -
Naming patterns and responsibilities: Look for classes with these roles and names:
- Query composition:
Enhancer,QueryBuilder - API clients:
TimdexBase,TimdexSearch,PrimoSearch - Normalizers:
NormalizeTimdexResults,NormalizePrimoResults - Analysis / pagination:
Analyzer - Controllers orchestrate flow:
app/controllers/search_controller.rb,basic_search_controller.rb,record_controller.rb.
- Query composition:
-
Testing & VCR: Tests use
minitest,vcr, andwebmock. When creating or updating VCR cassettes:- Update
.env.testwith fakeTIMDEX_GRAPHQL/TIMDEX_HOST(as documented in README) before recording. - Commit
.env.test(it should not contain real credentials). - Run the test that exercises the request to generate new cassettes.
- Update
-
Testing geospatial features: Geospatial tests follow the same VCR pattern. When writing tests for geobox or geodistance queries:
- Use
ClimateControl.modify(FEATURE_GEODATA: 'true')to enable the feature flag in test blocks (see test helper for patterns). - Create VCR cassettes named with
geobox_*orgeodistance_*suffixes to keep them organized separately from standard search cassettes. - Test both validation (e.g., missing coordinates, invalid ranges) and successful query paths (verify aggregations include
placesfor geobox). - Disable
FEATURE_GEODATAby default in.env.testand only enable in specific test cases to avoid unintended side effects.
- Use
-
Common pitfalls to avoid:
- Don’t assume GraphQL responses are serializable — code converts
GraphQL::Client::Responseto hashes (raw.data.to_h,raw.errors.details.to_h). Keep that conversion when changing callers. - When adding or reordering filters, note
ACTIVE_FILTERSimpactsextract_filters/reorder_filtersflow. The schema uses*Filtersuffix for aggregation keys (e.g.,contentTypeFilter,languageFilter); ensure ENV variable filter name maps correctly to schema aggregation name. - Primo has offset limits;
Analyzer::PRIMO_MAX_OFFSET(960) is used byMergedSearchPaginatorto prevent invalid requests and to enableshow_continuationbehavior. - When modifying
SearchControllerrouting, ensure geospatial (geobox/geodistance) branches are only reached whenFEATURE_GEODATAis enabled, and standard keyword/filter search still works when geospatial is disabled.
- Don’t assume GraphQL responses are serializable — code converts
-
Developer workflows / commands:
- Run tests:
bin/rails test(or via your devcontainer). The project expects high test coverage. UseSPEC_REPORTERfor verbose test output. - Update GraphQL schema (see example above).
- Use devcontainers if available (see README) to match developer environment.
- Run tests:
-
Where to look for more context:
README.md— env vars, VCR and schema notesapp/controllers/search_controller.rb— primary request orchestration, including geospatial routing and error handlingapp/models/timdex_search.rbandapp/models/timdex_base.rb— GraphQL client and all four query types (BaseQuery,AllQuery,GeoboxQuery,GeodistanceQuery)app/models/merged_search_service.rbandapp/models/merged_search_paginator.rb— multi-source result merging and intelligent paginationapp/models/feature.rb— feature flag class and implementationapp/models/primo_search.rb— Primo client behaviorapp/javascript/*andconfig/importmap.rb— JS/Stimulus usage, including geospatial UI state insource_tabs.js