Skip to content

Comments

Replace GraphQL with REST for related content management#550

Merged
jrhoads merged 71 commits intomasterfrom
search-related-rest
Feb 4, 2026
Merged

Replace GraphQL with REST for related content management#550
jrhoads merged 71 commits intomasterfrom
search-related-rest

Conversation

@jrhoads
Copy link
Contributor

@jrhoads jrhoads commented Jan 9, 2026

This PR significantly refactors how related content and works are fetched and managed, moving from monolithic GraphQL queries to targeted REST-based queries supported by a new client-side manager architecture.

Purpose

The primary goal is to replace complex GraphQL queries with REST API calls to improve data retrieval performance and flexibility. This change introduces a more modular approach to handling "Related Works" for DOIs, including support for new connection types like "Versions"
and "Is Version Of".

Approach

  • REST Migration: Replaced the primary GraphQL logic for fetching related DOIs with REST API calls.
  • Manager Pattern: Introduced RelatedContentManager, ConnectionTypeManager, and PaginationManager to encapsulate business logic previously handled within components or large GraphQL schemas.
  • State Management: Integrated @tanstack/react-query for better caching and loading state management of DOI data.
  • Backend Support: Added a new Flask route /api/doi/related-list/<path:doi> to facilitate fetching related work attributes via REST.

Key Modifications

  • Frontend Components:
    • Updated RelatedContent.tsx to use useRelatedContentManager, simplifying its internal logic.
    • Enhanced WorkFacets.tsx and WorksListing.tsx to handle more connection types (versions, versionOf).
    • Modified SankeyGraph.tsx to handle undefined facets gracefully.
  • Data Management:
    • Created ConnectionCountManager.ts to perform parallel count-only queries for different relationship types.
    • Updated searchDoiQuery.ts to include a robust query builder (buildRelatedDoiQuery) for various relationship types (citations, parts, versions, etc.).
  • Types:
    • Expanded Work and Works types to include explicit counts and optional facet fields.
    • Added ConnectionTypeCounts and Pagination types.

Important Technical Details

  • The logic for identifying related works now explicitly checks for DOIs within relatedIdentifiers.
  • The buildRelatedDoiQuery utility now accounts for multiple URI formats (e.g., https://doi.org/, http://doi.org/, or the raw DOI string) to ensure comprehensive results.
  • Facets and search results are now fetched via separate hooks (useSearchDoiQuery and useSearchDoiFacetsQuery), allowing the UI to render the list while facets continue to load.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Reviewer, please remember our guidelines:

  • Be humble in the language and feedback you give, ask don't tell.
  • Consider using positive language as opposed to neutral when offering feedback. This is to avoid the negative bias that can occur with neutral language appearing negative.
  • Offer suggestions on how to improve code e.g. simplification or expanding clarity.
  • Ensure you give reasons for the changes you are proposing.

Summary by CodeRabbit

  • New Features

    • New API endpoint returning raw related-works payload by DOI.
    • Client-driven related-content flow with connection-type selection, connection counts, pagination, and a beta "No related works found" state.
    • Optional sort for DOI searches and improved Sankey/analytics visibility rules.
  • Refactor

    • Centralized managers for related content, connection types, connection counts, and pagination.
    • DOI metadata path consolidated to REST-only.
  • Bug Fixes

    • Safer handling of missing fields, resilient facet/Sankey fallbacks, and more robust CSV export.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 9, 2026

Walkthrough

Adds a raw DOI attributes REST endpoint and a client-side related-works manager flow: new managers/hooks for connection types, counts, pagination, and related-content orchestration; removes GraphQL DOI fetchs and extends types and defensive guards across UI components.

Changes

Cohort / File(s) Summary
Backend API
\api/app.py``
New Flask route related_works_list at /api/doi/related-list/<path:doi> returning raw DOI attributes with same validation/error semantics as existing DOI endpoints.
Related content UI
\src/app/(main)/doi.org/[...doi]/RelatedContent.tsx``
Replaced server-driven query with client-side useRelatedContentManager; component now uses manager state for loading/error/data and sankey/analytics flags; Props interface removed.
Search params
\src/app/(main)/doi.org/[...doi]/mapSearchParams.ts``
Added sort?: SortOption to SearchParams and passed sort into mapped query variables.
Managers (new)
\src/data/managers/*``
New modules: ConnectionTypeManager.ts, ConnectionCountManager.ts, PaginationManager.ts, RelatedContentManager.ts centralize connection-type logic, per-type counts, pagination, and related-content orchestration.
Query layer
\src/data/queries/*``
Removed GraphQL DOI fetchs; added REST-driven composite useDoiRelatedContentQuery (GQL variant renamed), added related-DOI query builder and QueryVar fields (relatedDoi, connectionType, uidList), extended useSearchDoiQuery with optional count, and exposed facetsLoading in repository hook.
Types & mapping
\src/data/types.ts`, `src/utils/helpers.ts``
Extended Work/Works with connection counts/relationships and optional facet arrays; added Pagination and ConnectionTypeCounts types; mapJsonToWork now includes relationships and uses optional chaining.
Connection counts & hooks
\src/data/managers/ConnectionCountManager.ts`, `src/components/WorkFacets/*``
New hooks useConnectionCounts / useConnectionCountsIndividual for per-type counts; components switched to ConnectionTypeCounts, added versions/versionOf entries, and made many facets optional with nullish guards.
Sankey / visuals
\src/components/SankeyGraph/SankeyGraph.tsx`, `src/components/WorksListing/WorksListing.tsx``
multilevelToSankey accepts undefined and returns []; sankey rendering gated by showSankey and loadingFacets.
Robustness / guards
\src/components/WorksDashboard/`, `src/components/Person/`, `src/components/WorkMetadata/*``
Added optional chaining and default fallbacks across components to avoid runtime errors when fields are undefined.
Download reports route
\src/app/(main)/download-reports/ror/funders/route.ts``
Use getFundersArray(works) and default to [] when funders missing for CSV generation.
Constants
\src/data/constants.ts``
Added FACETS.SANKEY containing personToWorkTypesMultilevel.
GraphQL removal
\src/data/queries/doiQuery.ts``
Removed GraphQL fetch/query functions while retaining REST fetchDoi logic.

Sequence Diagram(s)

sequenceDiagram
    participant UI as RelatedContent Component
    participant ManagerFactory as useRelatedContentManager
    participant Query as useDoiRelatedContentQuery
    participant CountHook as useConnectionCounts
    participant DOIAPI as REST DOI API

    UI->>ManagerFactory: init with doi + vars
    ManagerFactory->>Query: fetch related content + facets
    Query->>DOIAPI: REST requests for content and facets
    DOIAPI-->>Query: content + facets
    Query-->>ManagerFactory: combined data, facetsLoading, loading, error
    ManagerFactory->>CountHook: request per-type counts (uidList)
    CountHook->>DOIAPI: count-only queries per connection type
    DOIAPI-->>CountHook: counts per type
    CountHook-->>ManagerFactory: counts, loading, errors
    ManagerFactory-->>UI: manager instance (selectedContent, title, pagination, flags)
    UI->>UI: render listing, sankey, error, or empty state based on manager
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • codycooperross
  • bklaing2
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Replace GraphQL with REST for related content management' clearly and concisely summarizes the main architectural change across the codebase. It accurately reflects the primary objective of the PR: migrating from GraphQL-based queries to REST API calls and introducing a new manager-based architecture.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/WorkFacets/WorkFacets.tsx (1)

66-77: Duplicate/typo in defaultActiveKeys.

Line 68 has "connection-type-facets" and line 75 has "conection-type-facets" (missing 'n'). This appears to be a typo that creates a duplicate entry with a misspelling.

Proposed fix
   const defaultActiveKeys = [
     "authors-facets",
     "connection-type-facets",
     "published-facets",
     "work-type-facets",
     "license-facets",
     "language-facets",
     "field-of-science-facets",
     "registration-agency-facets",
-    "conection-type-facets",
     "repository-type-facets"
   ]
🤖 Fix all issues with AI agents
In @api/app.py:
- Around line 30-41: The DOI validation and fetching in related_works_list
duplicates the logic in related_works; extract that shared behavior into a
helper (e.g., validate_and_get_full_doi_attributes) that takes the incoming doi
path, calls extract_doi, returns the same (400) for invalid format, calls
get_full_corpus_doi_attributes with RelatedWorkReports.parser and DOI_API and
returns the same (404) when not found; then replace the body of both
related_works_list and related_works to call this helper and return its result
(or the helper can return the full_doi_attributes for the route to jsonify),
preserving existing responses and references to extract_doi,
get_full_corpus_doi_attributes, RelatedWorkReports.parser, and DOI_API.

In @src/app/(main)/doi.org/[...doi]/RelatedContent.tsx:
- Line 69: Fix the typo in the error message string inside the RelatedContent
component: change "An error occured loading related content." to "An error
occurred loading related content." so the Error component receives the correctly
spelled message prop (reference: RelatedContent.tsx and the Error component
usage).
- Around line 10-11: The imported UI component named Error shadows the global
Error constructor; rename the import (for example to ErrorComponent or
RelatedContentError) in the import statement and update all usages of the JSX
tag (the <Error ... /> instance(s) in RelatedContent.tsx) to use the new
identifier so the global Error constructor is not shadowed.

In @src/components/WorksListing/WorksListing.tsx:
- Around line 23-32: Replace the inline prop type for connectionTypesCounts in
WorksListing.tsx with the centralized ConnectionTypeCounts type from
src/data/types.ts: remove the inline object shape, add an import for
ConnectionTypeCounts, and update the component prop/interface to use
ConnectionTypeCounts for the connectionTypesCounts prop so the file references
the shared type instead of duplicating it.

In @src/data/managers/RelatedContentManager.ts:
- Line 9: Replace the loose any types in RelatedContentManager with explicit
interfaces: define interfaces like RelatedContentVars (e.g., id: string, other
required query params), RelatedContentData (e.g., work?: Work, related fields
typed), and ConnectionCounts (map of string to number or specific shape), then
update the class property declarations (the private readonly data, private
readonly vars, private readonly connectionCounts) and the constructor signature
to use these interfaces instead of any; ensure usages inside methods match the
new shapes (adjust property accessors or types like Work, WorkEdge, etc.) and
import or reference existing domain types where applicable.

In @src/data/queries/doiRelatedContentQuery.ts:
- Around line 17-26: The work object currently hardcodes resourceTypeGeneral:
"TEXT", which can be inaccurate; replace this with the actual resource type from
the primary DOI data (e.g., use content.data?.works?.resourceTypeGeneral or
another authoritative field from the DOI response) or remove the property
entirely if no reliable type is available; ensure you preserve merging logic for
[variables.connectionType || "allRelated"] which spreads ...content.data?.works
and ...facets.data?.works so the correct resourceTypeGeneral comes from those
sources rather than a hardcoded string.

In @src/data/queries/searchDoiQuery.ts:
- Around line 72-76: In searchDoiQuery.ts update the typo in the inline comment
after the connectionType branch: change "retrun" to "return" so the comment
reads "By default only return positive part"; this is the comment adjacent to
the connectionType === "otherRelated" branch that returns `(${positivePart})`.
- Around line 218-219: The query key for useSearchDoiQuery omits the count
parameter so different counts can return stale cached results; update the
queryKey in useSearchDoiQuery to include count (e.g., ['doiSearch', variables,
count]) so React Query distinguishes queries, and ensure fetchDois(variables,
count) remains the queryFn to match the key.

In @src/data/types.ts:
- Around line 75-83: Optional facet properties (published, resourceTypes,
languages, licenses, fieldsOfScience, registrationAgencies, repositories,
affiliations, funders, personToWorkTypesMultilevel) are accessed directly in
several components causing runtime errors when not loaded; update all usages to
use optional chaining or null coalescing so code handles undefined values safely
— e.g., replace direct .map() calls on works.published / works.resourceTypes /
works.licenses / works.fieldsOfScience / works.languages and
data.publications.published with works.published?.map(...) or (works.published
?? []).map(...), and when calling multilevelToSankey pass
works.personToWorkTypesMultilevel ?? [] (or use optional chaining if function
accepts undefined) so components like WorksDashboard.tsx, Content.tsx
(statistics page), and WorksListing.tsx no longer assume facets are present.

In @src/utils/helpers.ts:
- Around line 189-191: Replace direct property access of json.relationships and
json.relationships.client.data with defensive optional chaining: change any uses
of json.relationships.client.data (e.g., where client data is read) to
json.relationships?.client?.data and set the returned object’s relationships
field to json.relationships ?? {} (instead of json.relationships) so missing
relationships won’t throw at runtime; update all occurrences in the helpers
where json.relationships is accessed.
🧹 Nitpick comments (9)
src/data/queries/repositoryRelatedContentQuery.ts (1)

11-14: Inconsistent loading behavior compared to personRelatedContentQuery.ts.

The loading logic here only considers results.loading, while the similar personRelatedContentQuery.ts uses results.loading || facets.loading. Additionally, the commented-out early return (line 14) should either be removed or restored with a comment explaining why it's disabled.

If intentional (to allow partial rendering), consider documenting this difference or aligning personRelatedContentQuery.ts to the same pattern for consistency.

Suggested cleanup
-  const loading = results.loading;
+  // Only block on results loading; facets load independently for partial rendering
+  const loading = results.loading;
   const error = results.error || facets.error;
 
-  // if (loading || error) return { loading, data: undefined, error }
src/data/managers/PaginationManager.ts (1)

13-32: LGTM – clean encapsulation of pagination logic.

The class correctly handles optional pageInfo with safe defaults. The getters provide read-only access to pagination state.

Consider adding a convenience getter that returns the full Pagination object for consumers that need all fields at once:

Optional: Add toPagination() getter
   get endCursor(): string {
     return this.works.pageInfo?.endCursor || ''
   }
+
+  get toPagination(): Pagination {
+    return {
+      hasPagination: this.hasPagination,
+      hasNextPage: this.hasNextPage,
+      endCursor: this.endCursor
+    }
+  }
 }
src/data/managers/ConnectionCountManager.ts (1)

82-102: DRY violation: error arrays are constructed twice.

The same array of errors is built twice (lines 82-91 for isError check and lines 93-102 for errors list). Extract to a single variable.

Proposed refactor
+  const allErrors = [
+    allRelatedQuery.error,
+    referencesQuery.error,
+    citationsQuery.error,
+    partsQuery.error,
+    partOfQuery.error,
+    versionsQuery.error,
+    versionOfQuery.error,
+    otherRelatedQuery.error
+  ]
+
-  const isError = [
-    allRelatedQuery.error,
-    referencesQuery.error,
-    citationsQuery.error,
-    partsQuery.error,
-    partOfQuery.error,
-    versionsQuery.error,
-    versionOfQuery.error,
-    otherRelatedQuery.error
-  ].some(Boolean)
-
-  const errors = [
-    allRelatedQuery.error,
-    referencesQuery.error,
-    citationsQuery.error,
-    partsQuery.error,
-    partOfQuery.error,
-    versionsQuery.error,
-    versionOfQuery.error,
-    otherRelatedQuery.error
-  ].filter(Boolean)
+  const isError = allErrors.some(Boolean)
+  const errors = allErrors.filter(Boolean)
src/data/queries/searchDoiQuery.ts (1)

13-18: Potential DRY violation with VALID_CONNECTION_TYPES.

This array duplicates CONNECTION_TYPES from ConnectionTypeManager.ts. Consider importing and reusing the single source of truth to prevent drift.

Suggested refactor
+import { CONNECTION_TYPES } from 'src/data/managers/ConnectionTypeManager'
+
-const VALID_CONNECTION_TYPES = ['references', 'citations', 'parts', 'partOf', 'versions', 'versionOf', 'allRelated', 'otherRelated'];
-
 function buildRelatedDoiQuery(relatedDoi: string | undefined, uidList: string[] | undefined, connectionType="allRelated" ): string {
   if (!relatedDoi) return ''
-  if (!VALID_CONNECTION_TYPES.includes(connectionType)) return ''
+  if (!CONNECTION_TYPES.includes(connectionType as any)) return ''
src/app/(main)/doi.org/[...doi]/RelatedContent.tsx (2)

24-35: Type safety bypass with as any for relatedIdentifiers.

The comment acknowledges relatedIdentifiers may not be in the Work type, but casting to any loses all type safety. Consider extending the Work type to include relatedIdentifiers or creating a proper type guard.

Safer approach with type extension
// In types.ts or locally
interface WorkWithRelatedIdentifiers extends Work {
  relatedIdentifiers?: Array<{
    relatedIdentifierType: string
    relatedIdentifier: string
  }>
}

function extractRelatedDois(work: Work | undefined): string[] {
  if (!work) return []
  
  const workWithRI = work as WorkWithRelatedIdentifiers
  if (!workWithRI.relatedIdentifiers) return []
  
  return workWithRI.relatedIdentifiers
    .filter(identifier => identifier.relatedIdentifierType === 'DOI')
    .map(identifier => identifier.relatedIdentifier)
    .filter(Boolean)
}

63-64: Consider combining loading states.

Two sequential loading checks could be combined for cleaner code:

-  if (doiQuery.isLoading) return <Row><Loading /></Row>
-  if (manager.isLoading) return <Row><Loading /></Row>
+  if (doiQuery.isLoading || manager.isLoading) return <Row><Loading /></Row>
src/data/managers/RelatedContentManager.ts (1)

38-95: Add explicit return types to all getter methods.

All getter methods lack explicit return types, which reduces code clarity and type safety. Adding explicit return types helps catch type mismatches early and serves as inline documentation.

📝 Example with explicit return types
-  get isLoading() {
+  get isLoading(): boolean {
     return this.loading
   }

-  get facetsAreLoading() {
+  get facetsAreLoading(): boolean {
     return this.facetsLoading
   }

-  get hasError() {
+  get hasError(): boolean {
     return !!this.error
   }

-  get errorMessage() {
+  get errorMessage(): string | undefined {
     return this.error?.message
   }

-  get hasData() {
+  get hasData(): boolean {
     return !!this.data
   }

-  get hasAnyRelatedWorks() {
+  get hasAnyRelatedWorks(): boolean {
     return this.connectionManager?.hasAnyRelatedWorks() || false
   }

-  get showSankey() {
+  get showSankey(): boolean {
     return this.data?.work ? (isDMP(this.data.work) || isProject(this.data.work)) : false
   }

-  get selectedContent() {
+  get selectedContent(): { works: Works, title: string } {
     if (!this.connectionManager) return {works: EMPTY_WORKS, title: ''}
     return this.connectionManager.getWorksAndTitle(this.connectionType)
   }

-  get pagination() {
+  get pagination(): Pagination {
     if (!this.paginationManager) return EMPTY_PAGINATION
     return {
       hasPagination: this.paginationManager.hasPagination,
       hasNextPage: this.paginationManager.hasNextPage,
       endCursor: this.paginationManager.endCursor
     }
   }

-  get url() {
+  get url(): string {
     return '/doi.org/' + this.vars.id + '/?'
   }
src/data/managers/ConnectionTypeManager.ts (2)

108-112: Add runtime validation before type assertion.

Line 110 uses a type assertion (as Works | undefined) to access this.work[connectionType]. Since connectionType is validated by getValidConnectionType, the access should be safe, but the type assertion could hide issues if the Work type is incomplete or the connection type isn't present on the work object.

Consider adding defensive runtime checks or using a type guard:

🛡️ Optional defensive approach
  getWorks(connectionType: string): Works {
    connectionType = getValidConnectionType(connectionType)
-   const works = this.work[connectionType] as Works | undefined;
+   const works = (connectionType in this.work) 
+     ? this.work[connectionType] as Works | undefined
+     : undefined;
    return works ?? EMPTY_WORKS
  }

81-119: Add explicit return types to public methods.

Public methods lack explicit return types, which reduces type safety and code documentation. Adding explicit return types helps catch type mismatches and serves as inline documentation.

📝 Methods with explicit return types
-  getCounts(): ConnectionTypeCounts {
+  getCounts(): ConnectionTypeCounts {
     return this.counts
   }

-  hasAnyRelatedWorks(): boolean {
+  hasAnyRelatedWorks(): boolean {
     return Object.values(this.counts).some(count => count > 0)
   }

-  getDefaultConnectionType(): string {
+  getDefaultConnectionType(): string {
     const { allRelated, references, citations, parts, partOf, versions, versionOf } = this.counts
     if (allRelated > 0) return 'allRelated'
     if (references > 0) return 'references'
     if (citations > 0) return 'citations'
     if (parts > 0) return 'parts'
     if (partOf > 0) return 'partOf'
     if (versions > 0) return 'versions'
     if (versionOf > 0) return 'versionOf'
     return 'otherRelated'
   }

-  formatTitle(connectionType: string): string {
+  formatTitle(connectionType: string): string {
     if (connectionType === 'allRelated') return 'All Related Works'
     if (connectionType === 'otherRelated') return 'Other Works'
     return connectionType.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())
   }

-  getWorks(connectionType: string): Works {
+  getWorks(connectionType: string): Works {
     connectionType = getValidConnectionType(connectionType)
     const works = this.work[connectionType] as Works | undefined;
     return works ?? EMPTY_WORKS
   }

-  getWorksAndTitle(connectionType: string | undefined ): { works: Works, title: string } {
+  getWorksAndTitle(connectionType: string | undefined): { works: Works; title: string } {
     const validConnectionType = getValidConnectionType(connectionType)
     const works = this.getWorks(validConnectionType)
     const title = this.formatTitle(validConnectionType)
     return { works, title }
   }
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5250c2a and b572765.

📒 Files selected for processing (16)
  • api/app.py
  • src/app/(main)/doi.org/[...doi]/RelatedContent.tsx
  • src/app/(main)/doi.org/[...doi]/mapSearchParams.ts
  • src/components/SankeyGraph/SankeyGraph.tsx
  • src/components/WorkFacets/WorkFacets.tsx
  • src/components/WorksListing/WorksListing.tsx
  • src/data/managers/ConnectionCountManager.ts
  • src/data/managers/ConnectionTypeManager.ts
  • src/data/managers/PaginationManager.ts
  • src/data/managers/RelatedContentManager.ts
  • src/data/queries/doiQuery.ts
  • src/data/queries/doiRelatedContentQuery.ts
  • src/data/queries/repositoryRelatedContentQuery.ts
  • src/data/queries/searchDoiQuery.ts
  • src/data/types.ts
  • src/utils/helpers.ts
💤 Files with no reviewable changes (1)
  • src/data/queries/doiQuery.ts
🧰 Additional context used
🧬 Code graph analysis (11)
src/components/SankeyGraph/SankeyGraph.tsx (2)
src/data/types.ts (1)
  • MultilevelFacet (340-342)
src/components/SankeyGraph/SankeySpec.ts (1)
  • SankeyGraphData (13-16)
src/data/managers/PaginationManager.ts (2)
src/data/types.ts (2)
  • PageInfo (349-352)
  • Pagination (354-356)
src/components/Pager/Pager.tsx (1)
  • Pager (15-36)
src/components/WorkFacets/WorkFacets.tsx (2)
src/data/types.ts (1)
  • Facet (334-338)
src/components/FacetList/FacetList.tsx (1)
  • FacetListProps (14-27)
src/data/managers/ConnectionCountManager.ts (3)
src/data/types.ts (1)
  • ConnectionTypeCounts (363-372)
src/data/managers/ConnectionTypeManager.ts (1)
  • CONNECTION_TYPES (30-30)
src/data/queries/searchDoiQuery.ts (2)
  • fetchDois (170-190)
  • useSearchDoiQuery (218-222)
src/data/queries/repositoryRelatedContentQuery.ts (4)
src/data/queries/searchDoiQuery.ts (1)
  • QueryData (271-273)
src/data/queries/searchDoiFacetsQuery.ts (1)
  • QueryData (90-92)
src/data/queries/repositoryQuery.ts (1)
  • QueryData (133-135)
src/data/queries/personRelatedContentQuery.ts (1)
  • usePersonRelatedContentQuery (8-29)
src/utils/helpers.ts (1)
src/data/queries/doiQuery.ts (1)
  • convertToQueryData (21-25)
src/data/queries/doiRelatedContentQuery.ts (5)
src/data/queries/searchDoiQuery.ts (3)
  • QueryVar (275-295)
  • useSearchDoiQuery (218-222)
  • QueryData (271-273)
src/data/queries/doiQuery.ts (2)
  • QueryVar (57-59)
  • QueryData (53-55)
src/data/queries/personRelatedContentQuery.ts (3)
  • QueryVar (81-81)
  • QueryData (81-81)
  • usePersonRelatedContentQuery (8-29)
src/data/queries/searchDoiFacetsQuery.ts (1)
  • useSearchDoiFacetsQuery (78-87)
src/data/queries/repositoryRelatedContentQuery.ts (1)
  • QueryData (31-33)
src/data/managers/RelatedContentManager.ts (4)
src/data/managers/ConnectionTypeManager.ts (4)
  • ConnectionTypeManager (51-120)
  • getValidConnectionType (36-42)
  • EMPTY_CONNECTION_TYPE_COUNTS (9-18)
  • EMPTY_WORKS (20-27)
src/data/managers/PaginationManager.ts (2)
  • PaginationManager (13-33)
  • EMPTY_PAGINATION (7-11)
src/utils/helpers.ts (2)
  • isDMP (55-57)
  • isProject (59-66)
src/data/types.ts (1)
  • ConnectionTypeCounts (363-372)
src/app/(main)/doi.org/[...doi]/mapSearchParams.ts (1)
src/data/queries/searchDoiQuery.ts (1)
  • SortOption (127-127)
src/data/managers/ConnectionTypeManager.ts (2)
src/data/types.ts (3)
  • ConnectionTypeCounts (363-372)
  • Works (72-99)
  • Work (19-70)
src/data/queries/searchSummaryQuery.ts (1)
  • convertSummaryToCounts (12-20)
src/app/(main)/doi.org/[...doi]/RelatedContent.tsx (3)
src/app/(main)/doi.org/[...doi]/mapSearchParams.ts (1)
  • mapSearchparams (19-37)
src/data/queries/doiQuery.ts (1)
  • fetchDoi (24-47)
src/data/managers/RelatedContentManager.ts (1)
  • useRelatedContentManager (102-107)
🪛 Biome (2.1.2)
src/app/(main)/doi.org/[...doi]/RelatedContent.tsx

[error] 10-10: Do not shadow the global "Error" property.

Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.

(lint/suspicious/noShadowRestrictedNames)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: test / cypress (5)
  • GitHub Check: test / cypress (2)
  • GitHub Check: test / cypress (3)
🔇 Additional comments (13)
src/app/(main)/doi.org/[...doi]/mapSearchParams.ts (1)

1-1: LGTM! Sort parameter properly integrated.

The addition of the optional sort parameter is well-typed and correctly propagated to the query variables. TypeScript's type system will ensure only valid SortOption values are passed.

Also applies to: 15-15, 31-31

src/components/SankeyGraph/SankeyGraph.tsx (1)

21-22: LGTM! Proper undefined handling added.

The early return guard prevents runtime errors when facets is undefined and aligns with the PR's goal of making facets optional. The component's existing empty-data handling at line 77 ensures a graceful fallback UI.

src/data/queries/repositoryRelatedContentQuery.ts (1)

22-28: LGTM – return surface aligns with the new manager pattern.

Exposing loading, error, and facetsLoading separately enables consumers to handle partial states (e.g., render results while facets are still loading). This aligns with the broader PR goal of flexible, independent data loading.

src/data/queries/doiRelatedContentQuery.ts (1)

8-15: Consistent return shape on early exit.

Good pattern: returning facetsLoading even on early exit ensures consumers always have a consistent shape. This enables partial state handling.

src/components/WorkFacets/WorkFacets.tsx (1)

62-64: LGTM – clean predicate for connection type visibility.

The hasAnyConnections function clearly expresses the intent and the logic is correct.

src/data/managers/ConnectionCountManager.ts (1)

36-40: No type safety concern exists here. CONNECTION_TYPES is derived directly from Object.keys(EMPTY_CONNECTION_TYPE_COUNTS), which is explicitly typed as ConnectionTypeCounts. The keys maintain consistent order (ES2015+), and since the queries array maps CONNECTION_TYPES in the same order, the index-based assignment is guaranteed to match correctly. The code follows the intended "single source of truth" pattern.

src/app/(main)/doi.org/[...doi]/RelatedContent.tsx (1)

44-60: Hook ordering verified – code complies with Rules of Hooks and handles empty data gracefully.

The hooks are called unconditionally at the top level, and useRelatedContentManager safely receives an empty uidList on first render (before doiQuery completes). The manager's constructor checks if (data?.work) and sets both connectionManager and paginationManager to null when data is unavailable, with all getters providing proper fallbacks (EMPTY_WORKS, EMPTY_PAGINATION, EMPTY_CONNECTION_TYPE_COUNTS). Query building also handles empty uidList with a length check before inclusion.

src/data/managers/RelatedContentManager.ts (1)

97-99: Verify the trailing /? in URL construction.

The URL ends with /? which appears incomplete. If query parameters are intended to be appended by consumers, this should be documented. Otherwise, consider removing the trailing /? or using a URL builder utility.

src/data/types.ts (2)

56-68: LGTM! New Work fields support extended connection types.

The new optional fields (referenceCount, partCount, partOfCount, versionsCount, versionOfCount, allRelatedCount, versions, versionOf) follow the existing naming conventions and are consistently marked as optional, which aligns with the PR's goal of supporting additional connection types.


354-356: LGTM! New types support the manager architecture.

The Pagination type appropriately extends PageInfo with an additional hasPagination flag, and ConnectionTypeCounts provides type-safe structure for all connection type counts. Both types are well-defined and support the new manager-based architecture.

Also applies to: 363-372

src/data/managers/ConnectionTypeManager.ts (3)

1-42: LGTM! Well-structured constants and helpers.

The constants and helper functions establish a single source of truth for connection types. Deriving CONNECTION_TYPES from EMPTY_CONNECTION_TYPE_COUNTS (line 30) ensures consistency, and the helper functions (isConnectionType, getValidConnectionType) provide robust validation.


44-49: LGTM! Well-defined external counts type.

The ExternalConnectionCounts type appropriately encapsulates connection counts along with their loading and error states, supporting the dual-source count logic in the manager.


51-79: LGTM! Constructor and count calculation are well-designed.

The constructor appropriately accepts both internal (Work) and external (ExternalConnectionCounts) count sources. The calculateCounts method correctly prioritizes external counts (lines 64-66) and safely falls back to work data using optional chaining and nullish coalescing.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
src/app/(main)/download-reports/ror/funders/route.ts (1)

5-8: Use proper types instead of any for type safety.

The defensive helper is a good addition for handling optional funders, but using any bypasses TypeScript's type checking. Consider using the Works type from src/data/types.ts and specifying a proper return type.

♻️ Suggested refactor with proper types

Import the Works type at the top of the file:

import type { Works } from 'src/data/types'

Then update the helper:

-function getFundersArray(works: any): any[] {
-  return works.funders ? works.funders : []
+function getFundersArray(works: Works): any[] {
+  return works.funders ?? []
 }

Note: Using the nullish coalescing operator (??) is more idiomatic than a ternary for default values.

src/components/WorksDashboard/WorksDashboard.tsx (1)

35-82: LGTM! Robust defensive data handling.

The optional chaining and empty array fallbacks ensure charts render safely even when data fields are missing or still loading. This aligns perfectly with the PR's goal of allowing facets and results to render independently.

The pattern is applied consistently across all data transformations:

  • published, resourceTypes, and licensesData all gracefully handle undefined values
  • Chart components receive safe empty arrays rather than undefined
  • Existing chart logic (getTopFive, VerticalBarChart) already handles empty data appropriately
💡 Optional: Consider nullish coalescing for clarity

For modern JavaScript clarity, you could use the nullish coalescing operator (??) instead of logical OR (||), though functionally they're equivalent here since arrays are always truthy:

-  const published = works.published?.map((x) => ({
-    title: x.title,
-    count: x.count
-  })) || []
+  const published = works.published?.map((x) => ({
+    title: x.title,
+    count: x.count
+  })) ?? []

Same pattern could apply to lines 40, 41, 79, and 82. This is purely a style preference—the current implementation is correct.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b572765 and 72a71bd.

📒 Files selected for processing (2)
  • src/app/(main)/download-reports/ror/funders/route.ts
  • src/components/WorksDashboard/WorksDashboard.tsx
🧰 Additional context used
🧬 Code graph analysis (2)
src/app/(main)/download-reports/ror/funders/route.ts (1)
src/data/queries/searchDoiFacetsQuery.ts (1)
  • fetchDoisFacets (57-76)
src/components/WorksDashboard/WorksDashboard.tsx (2)
src/utils/helpers.ts (2)
  • getTopFive (70-106)
  • toBarRecord (108-110)
src/components/VerticalBarChart/VerticalBarChart.tsx (1)
  • VerticalBarChart (29-143)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: test / cypress (3)
  • GitHub Check: test / cypress (5)
  • GitHub Check: test / cypress (4)
  • GitHub Check: test / cypress (1)
  • GitHub Check: test / cypress (2)
🔇 Additional comments (1)
src/app/(main)/download-reports/ror/funders/route.ts (1)

16-18: LGTM! Defensive handling aligns with optional funders field.

The CSV generation correctly uses the safely-extracted funders array, providing graceful handling when funders are missing. This aligns well with the broader data model changes in the PR.

@cypress
Copy link

cypress bot commented Jan 9, 2026

akita    Run #1843

Run Properties:  status check passed Passed #1843  •  git commit 9e58c010c4 ℹ️: Merge ac4c0a04a1e8998918a167a0222e0ff0d7764851 into 526b2bafa7612862b6a89d73b651...
Project akita
Branch Review search-related-rest
Run status status check passed Passed #1843
Run duration 01m 45s
Commit git commit 9e58c010c4 ℹ️: Merge ac4c0a04a1e8998918a167a0222e0ff0d7764851 into 526b2bafa7612862b6a89d73b651...
Committer Joseph Rhoads
View all properties for this run ↗︎

Test results
Tests that failed  Failures 0
Tests that were flaky  Flaky 1
Tests that did not run due to a developer annotating a test with .skip  Pending 3
Tests that did not run due to a failure in a mocha hook  Skipped 0
Tests that passed  Passing 47
View all changes introduced in this branch ↗︎

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In @src/app/(main)/doi.org/[...doi]/RelatedContent.tsx:
- Around line 63-71: The component currently checks doiQuery.isLoading but never
handles doiQuery errors; update RelatedContent.tsx to mirror the manager error
handling by checking for doiQuery.error (or doiQuery.hasError) after the loading
checks and returning the same error UI (use CommonsError with a title like "An
error occurred loading DOI data." and message from doiQuery.error.message or
doiQuery.errorMessage); keep the existing manager error branch as-is so both
data sources render the CommonsError when they fail.

In @src/data/queries/searchDoiQuery.ts:
- Around line 25-30: The DOI interpolations are inconsistently quoted which
risks breaking DataCite/OpenSearch queries; update searchDoiQuery.ts so every
place the DOI is injected (e.g., the relatedIdentifiers
array/relatedIdentifierPart and all occurrences like citation_ids:${doi},
reference_ids:${doi}, part_of_ids:${doi}, etc.) uses a single, consistent
strategy: either wrap the DOI in double quotes or escape Lucene/OpenSearch
special characters before interpolation; add or reuse a helper (e.g.,
escapeLuceneQuery or quoteIdentifier) and apply it to the DOI in all query
fragments to ensure every field interpolation is safely quoted/escaped.
🧹 Nitpick comments (9)
src/components/WorksListing/WorksListing.tsx (1)

23-23: Good refactor to use the shared type.

Using ConnectionTypeCounts from src/data/types improves consistency across the codebase and supports the new versions/versionOf connection types.

Note: WorkFacets still uses an inline type for this prop (see src/components/WorkFacets/WorkFacets.tsx lines 10-15). Consider updating it to also use ConnectionTypeCounts for consistency.

src/data/queries/searchDoiQuery.ts (3)

13-13: Consider exporting VALID_CONNECTION_TYPES for reuse.

Based on the AI summary, this constant is intended for export to constrain connection types across the codebase. However, it's currently not exported, which limits its reusability in other modules like ConnectionTypeManager.

♻️ Suggested change
-const VALID_CONNECTION_TYPES = ['references', 'citations', 'parts', 'partOf', 'versions', 'versionOf', 'allRelated', 'otherRelated'];
+export const VALID_CONNECTION_TYPES = ['references', 'citations', 'parts', 'partOf', 'versions', 'versionOf', 'allRelated', 'otherRelated'] as const;
+export type ConnectionType = typeof VALID_CONNECTION_TYPES[number];

20-20: Remove redundant variable assignment.

The assignment const doi = relatedDoi is unnecessary since relatedDoi is already validated as non-empty on line 16.

♻️ Suggested change
-  const doi = relatedDoi;

Then use relatedDoi directly throughout the function.


280-282: Consider stricter typing for connectionType.

Using a string literal union type derived from VALID_CONNECTION_TYPES would provide compile-time safety and better IDE autocompletion.

♻️ Suggested change

If you export the constant as suggested above:

-  connectionType?: string
+  connectionType?: ConnectionType
src/data/managers/RelatedContentManager.ts (4)

8-17: Consider replacing any types with proper type definitions.

Multiple fields use any (data, vars, connectionCounts), which undermines TypeScript's type safety. Since this is a new class, establishing proper types now will prevent future type-related bugs and improve maintainability.

♻️ Suggested approach
+import { QueryVar } from 'src/data/queries/searchDoiQuery'
+import { DoiRelatedContentData } from 'src/data/queries/doiRelatedContentQuery'
+
 export class RelatedContentManager {
-  private readonly data: any
+  private readonly data: DoiRelatedContentData | undefined
   private readonly loading: boolean
   private readonly error: Error | undefined | null
   private readonly connectionManager: ConnectionTypeManager | null
   private readonly paginationManager: PaginationManager | null
   private readonly connectionType: string
-  private readonly vars: any
+  private readonly vars: QueryVar & { id: string }
   private readonly facetsLoading: boolean
-  private readonly connectionCounts: any
+  private readonly connectionCounts: ReturnType<typeof useConnectionCounts> | undefined

19-36: Constructor parameter types should match field types.

The constructor parameters use any types extensively. Consider defining an interface for the constructor parameters to improve code clarity and enable better IDE support.


83-86: The works return type is any which propagates type unsafety.

Consider defining a proper return type for selectedContent to maintain type safety through the component chain.

♻️ Suggested approach
-  get selectedContent() : {works: any, title: string} {
+  get selectedContent() : {works: Works, title: string} {

This requires importing Works from src/data/types and ensuring ConnectionTypeManager.getWorksAndTitle returns the proper type.


97-99: URL construction could be more robust.

The URL is built via string concatenation. Consider using encodeURIComponent for the DOI to handle special characters safely.

♻️ Suggested change
   get url() {
-    return '/doi.org/' + this.vars.id + '/?'
+    return `/doi.org/${encodeURIComponent(this.vars.id)}?`
   }
src/app/(main)/doi.org/[...doi]/RelatedContent.tsx (1)

24-35: Add relatedIdentifiers property to the Work type.

The relatedIdentifiers property is accessed in this function via a type assertion to any (line 28) because it's missing from the Work type definition in src/data/types.ts. Since the API returns this data (as evidenced by its usage in search queries), adding the property to the Work type would improve type safety and eliminate the type assertion. Define it as an optional array of objects with relatedIdentifierType and relatedIdentifier fields.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 72a71bd and dffe4e3.

📒 Files selected for processing (5)
  • src/app/(main)/doi.org/[...doi]/RelatedContent.tsx
  • src/components/WorksListing/WorksListing.tsx
  • src/data/managers/RelatedContentManager.ts
  • src/data/queries/searchDoiQuery.ts
  • src/utils/helpers.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/utils/helpers.ts
🧰 Additional context used
🧬 Code graph analysis (3)
src/data/queries/searchDoiQuery.ts (7)
src/data/queries/doiQuery.ts (3)
  • QueryVar (57-59)
  • buildDoiSearchParams (9-19)
  • QueryVar (144-146)
src/data/queries/repositoryQuery.ts (1)
  • QueryVar (137-139)
src/data/queries/searchPersonQuery.ts (1)
  • QueryVar (93-96)
src/data/queries/organizationQuery.ts (1)
  • QueryVar (95-97)
src/data/queries/gridQuery.ts (1)
  • QueryVar (24-26)
src/data/queries/claimQuery.ts (1)
  • QueryVar (96-98)
src/data/queries/searchDoiFacetsQuery.ts (1)
  • buildDoiSearchParams (7-21)
src/app/(main)/doi.org/[...doi]/RelatedContent.tsx (3)
src/app/(main)/doi.org/[...doi]/mapSearchParams.ts (1)
  • mapSearchparams (19-37)
src/data/queries/doiQuery.ts (1)
  • fetchDoi (24-47)
src/data/managers/RelatedContentManager.ts (1)
  • useRelatedContentManager (102-107)
src/components/WorksListing/WorksListing.tsx (2)
src/data/types.ts (1)
  • ConnectionTypeCounts (363-372)
src/components/WorkFacets/WorkFacets.tsx (2)
  • Props (11-16)
  • WorkFacets (37-158)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: test / cypress (2)
  • GitHub Check: test / cypress (3)
  • GitHub Check: test / cypress (4)
  • GitHub Check: test / cypress (1)
  • GitHub Check: test / cypress (5)
🔇 Additional comments (10)
src/components/WorksListing/WorksListing.tsx (1)

9-9: LGTM!

Clean import of the shared ConnectionTypeCounts type alongside Works.

src/data/queries/searchDoiQuery.ts (4)

36-57: LGTM!

The query mapping is well-structured. The filter(Boolean) on line 69 correctly handles cases where uidPart is empty.


59-76: LGTM!

The otherRelated query correctly excludes specific relationship types using the AND NOT clause, ensuring only non-categorized relationships are returned.


91-91: LGTM!

The integration with buildQuery correctly passes the new variables with sensible defaults.


218-219: LGTM!

Including count in the queryKey array ensures proper cache separation for different page sizes, which is essential for TanStack Query's caching behavior.

src/app/(main)/doi.org/[...doi]/RelatedContent.tsx (4)

1-17: LGTM!

Imports are well-organized and appropriate for the component's needs.


19-22: LGTM!

The helper correctly transforms URL search params into query variables.


88-114: LGTM!

The render logic cleanly extracts data from the manager and passes appropriate props to WorksListing. The loading state combination on line 101 correctly reflects all pending data sources.


43-60: Manager hook initializes before DOI data is available, triggering refetch when data arrives.

useRelatedContentManager is called with potentially empty uidList since it executes before doiQuery completes. Once primaryWork is loaded, varsWithRelatedDois changes, triggering a refetch via TanStack Query's queryKey dependency (line 219 in searchDoiQuery.ts). The component properly handles loading states with early returns (lines 63–64), so no incorrect content is rendered during the initial fetch, but this two-step fetch pattern is expected given the hook architecture.

src/data/managers/RelatedContentManager.ts (1)

102-107: Memoization is not necessary here.

The manager instance is only used within the component to access properties and is not passed to child components or used as a dependency in other hooks (like useEffect). Since WorksListing receives individual extracted properties rather than the manager object itself, creating a new instance on each render has no performance impact. The inner hooks (useDoiRelatedContentQuery, useConnectionCounts) already handle their own memoization.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/components/WorkFacets/WorkFacets.tsx (1)

98-101: Inconsistent nullish handling between lines 99 and 100.

Line 99 uses || for data.authors while line 100 uses ?? for data.creatorsAndContributors. For consistency and correct null/undefined semantics (preserving empty arrays), consider using ?? on both lines.

♻️ Suggested fix for consistency
       {model == "person"
-        ? <AuthorsFacet authors={data.authors || []} title="Co-Authors" url={url} model={model} />
+        ? <AuthorsFacet authors={data.authors ?? []} title="Co-Authors" url={url} model={model} />
         : <AuthorsFacet authors={data.creatorsAndContributors ?? []} title="Creators & Contributors" url={url} model={model} />
       }
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dffe4e3 and 67eae7a.

📒 Files selected for processing (5)
  • src/components/Person/Person.tsx
  • src/components/WorkFacets/WorkFacets.tsx
  • src/components/WorkMetadata/WorkMetadata.tsx
  • src/components/WorksDashboard/WorksDashboard.tsx
  • src/components/WorksListing/WorksListing.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/components/WorksListing/WorksListing.tsx
  • src/components/WorksDashboard/WorksDashboard.tsx
🧰 Additional context used
🧬 Code graph analysis (2)
src/components/Person/Person.tsx (2)
src/data/queries/searchSummaryQuery.ts (1)
  • convertSummaryToCounts (12-20)
src/data/types.ts (1)
  • PersonWorks (255-259)
src/components/WorkFacets/WorkFacets.tsx (3)
src/data/types.ts (1)
  • Facet (334-338)
src/components/AuthorsFacet/AuthorsFacet.tsx (1)
  • AuthorsFacet (12-55)
src/components/FacetList/FacetList.tsx (2)
  • FacetList (44-70)
  • FacetListProps (14-27)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: test / cypress (2)
  • GitHub Check: test / cypress (4)
  • GitHub Check: test / cypress (3)
  • GitHub Check: test / cypress (5)
  • GitHub Check: test / cypress (1)
🔇 Additional comments (10)
src/components/WorkMetadata/WorkMetadata.tsx (1)

254-266: LGTM — defensive optional chaining.

The added ?. on line 256 is technically redundant since the && guard on line 254 already ensures fieldsOfScience is truthy. However, it's a harmless defensive pattern that aligns with the PR's goal of improving robustness when data is missing.

src/components/Person/Person.tsx (3)

24-38: LGTM!

Good use of optional chaining to guard against undefined openLicenseResourceTypes. This prevents a runtime TypeError if the array is missing from the API response.


40-45: LGTM!

Safe defaults for totalOpenLicenses and totalContentUrl ensure these values are always defined for subsequent comparisons and calculations. The pattern is consistent with similar fallbacks used elsewhere in the codebase (e.g., searchSummaryQuery.ts).


47-48: LGTM!

Properly guards against division by zero using || 1 for the denominator. When totalCount is 0 or undefined, this yields 0% rather than NaN or Infinity, which is the expected behavior for display purposes.

src/components/WorkFacets/WorkFacets.tsx (6)

15-15: LGTM!

The type extension for connectionTypesCounts to include versions and versionOf is consistent with the new connection type support.


18-31: LGTM!

Making published and resourceTypes optional aligns with the flexible data-fetching approach described in the PR objectives and is properly handled with nullish coalescing in the rendering logic.


56-58: LGTM!

New connection type entries for versions and versionOf follow the established pattern and are logically positioned in the list.


62-64: LGTM!

The hasAnyConnections helper with NonNullable<typeof connectionTypesCounts> is well-typed, and the logic correctly determines whether any connection counts exist before rendering the facet section.


86-96: LGTM!

Using hasConnectionTypes as the rendering condition provides clearer intent and correctly prevents rendering the Connection Types facet when no connections exist.


104-158: LGTM!

The consistent use of nullish coalescing (??) for all optional facet fields provides robust handling of undefined data while preserving empty arrays when present.

@jrhoads jrhoads force-pushed the search-related-rest branch from 67eae7a to b4e33ab Compare January 19, 2026 16:05
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/components/WorkFacets/WorkFacets.tsx`:
- Line 15: The prop definition in WorkFacets.tsx uses an inline type for
connectionTypesCounts that duplicates the existing ConnectionTypeCounts type;
replace the inline shape with the centralized ConnectionTypeCounts type by
importing ConnectionTypeCounts from src/data/types and using it for the
connectionTypesCounts prop (wherever connectionTypesCounts?: ... is declared),
and add the corresponding import statement at the top of the file so the
component uses the shared type.
♻️ Duplicate comments (3)
src/data/queries/searchDoiQuery.ts (1)

13-76: Normalize and escape DOI values before interpolating into query fragments.

doi is used raw in citation_ids, reference_ids, etc. If relatedDoi is a full URL (or contains Lucene special characters), those fields likely won’t match, and otherRelated won’t exclude non-target relations correctly. Normalize to the DOI name and consistently quote/escape all DOI interpolations.

🔧 Suggested fix (normalize + quote/escape)
+function normalizeDoi(input: string): string {
+  return input.replace(/^https?:\/\/doi\.org\//i, '')
+}
+
+function quoteLuceneTerm(term: string): string {
+  const escaped = term.replace(/([+\-!(){}\[\]^"~*?:\\/]|&&|\|\|)/g, '\\$1')
+  return `"${escaped}"`
+}
 ...
-  const doi = relatedDoi;
+  const doi = normalizeDoi(relatedDoi);
+  const doiTerm = quoteLuceneTerm(doi);
 ...
-    `"${baseURI}${doi}"`,
-    `"${doi}"`,
-    `"${httpBaseURI}${doi}"`
+    quoteLuceneTerm(`${baseURI}${doi}`),
+    doiTerm,
+    quoteLuceneTerm(`${httpBaseURI}${doi}`)
 ...
-    references: [`citation_ids:${doi}`],
+    references: [`citation_ids:${doiTerm}`],
src/data/queries/doiRelatedContentQuery.ts (1)

9-27: Avoid hardcoding resourceTypeGeneral: "TEXT".

This can be incorrect for many DOIs. Prefer deriving it from authoritative work data or omit the field when unavailable.

🔧 Suggested fix (omit hardcoded type)
-  const work = {
-    types: {
-      resourceTypeGeneral: "TEXT",
-
-    },
-    [variables.connectionType || "allRelated"]: {
-      ...content.data?.works || {},
-      ...facets.data?.works
-    }
-  }
+  const work = {
+    [variables.connectionType || "allRelated"]: {
+      ...content.data?.works || {},
+      ...facets.data?.works
+    }
+  }
src/app/(main)/doi.org/[...doi]/RelatedContent.tsx (1)

71-79: doiQuery.error is not handled.

The component checks doiQuery.isLoading but doesn't handle doiQuery.error. If the initial DOI fetch fails, primaryWork will be undefined, and the component will proceed to show "No related works found" instead of an error message.

🐛 Proposed fix
   if (doiQuery.isLoading) return <Row><Loading /></Row>
+  if (doiQuery.error) return <Row>
+    <Col md={{ offset: 3 }} className="panel panel-transparent">
+      <CommonsError title="An error occurred loading DOI data." message={(doiQuery.error as Error)?.message} />
+    </Col>
+  </Row>
   if (manager.isLoading) return <Row><Loading /></Row>
🧹 Nitpick comments (5)
src/data/managers/ConnectionCountManager.ts (2)

50-53: Consider removing or deprecating the alternative implementation.

The comment suggests this is a fallback "if useQueries causes issues." If useQueries is working correctly, consider removing useConnectionCountsIndividual to avoid maintenance burden, or add a @deprecated JSDoc tag with context on when to use it.


112-112: Fix inconsistent spacing.

There's an extra space before 0 in the nullish coalescing expression.

-    versionOf: versionOfQuery.data?.works?.totalCount ??  0
+    versionOf: versionOfQuery.data?.works?.totalCount ?? 0
src/app/(main)/doi.org/[...doi]/RelatedContent.tsx (2)

67-67: Typo in comment: "determin" → "determine".

-  // Need to determin showSankey based on primaryWork data
+  // Need to determine showSankey based on primaryWork data

25-36: Type assertion for relatedIdentifiers is pragmatic but could be improved.

The double as any cast works for runtime checking, but consider defining a more specific type or extending the Work type to include relatedIdentifiers if this property is expected from the API.

+interface WorkWithRelatedIdentifiers extends Work {
+  relatedIdentifiers?: Array<{
+    relatedIdentifierType: string
+    relatedIdentifier: string
+  }>
+}
+
 function extractRelatedDois(work: Work | undefined): string[] {
   if (!work) return []
   
-  // Check if relatedIdentifiers exists on the work object (even if not in the type)
-  const workWithRelatedIdentifiers = work as any
-  if (!workWithRelatedIdentifiers?.relatedIdentifiers) return []
+  const workWithRelatedIdentifiers = work as WorkWithRelatedIdentifiers
+  if (!workWithRelatedIdentifiers.relatedIdentifiers) return []
   
   return workWithRelatedIdentifiers.relatedIdentifiers
-    .filter((identifier: any) => identifier.relatedIdentifierType === 'DOI')
-    .map((identifier: any) => identifier.relatedIdentifier)
+    .filter(identifier => identifier.relatedIdentifierType === 'DOI')
+    .map(identifier => identifier.relatedIdentifier)
     .filter(Boolean)
 }
src/data/managers/ConnectionTypeManager.ts (1)

62-79: Consider using nullish coalescing (??) instead of logical OR (||) for count fallbacks.

Using || 0 could mask valid 0 counts if the property exists but is explicitly 0. While functionally equivalent here (since 0 || 0 = 0), ?? 0 is semantically more precise for handling potentially undefined values.

♻️ Suggested improvement
   private calculateCounts(): ConnectionTypeCounts {
     if (this.externalCounts?.counts) {
       return this.externalCounts.counts
     }
     
     return {
-      allRelated: this.work.allRelated?.totalCount || 0,
-      references: this.work.references?.totalCount || 0,
-      citations: this.work.citations?.totalCount || 0,
-      parts: this.work.parts?.totalCount || 0,
-      partOf: this.work.partOf?.totalCount || 0,
-      versions: this.work.versions?.totalCount || 0,
-      versionOf: this.work.versionOf?.totalCount || 0,
-      otherRelated: this.work.otherRelated?.totalCount || 0
+      allRelated: this.work.allRelated?.totalCount ?? 0,
+      references: this.work.references?.totalCount ?? 0,
+      citations: this.work.citations?.totalCount ?? 0,
+      parts: this.work.parts?.totalCount ?? 0,
+      partOf: this.work.partOf?.totalCount ?? 0,
+      versions: this.work.versions?.totalCount ?? 0,
+      versionOf: this.work.versionOf?.totalCount ?? 0,
+      otherRelated: this.work.otherRelated?.totalCount ?? 0
     }
   }

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/data/queries/searchDoiQuery.ts`:
- Around line 17-19: The quoteIdentifier function currently wraps the identifier
in double quotes but doesn't escape embedded double quotes, producing malformed
queries for identifiers like 10.5555/test"value; update quoteIdentifier to
escape internal double-quote characters (replace " with \" per Lucene syntax)
before surrounding the string with quotes so that functions using
quoteIdentifier (e.g., searchDoiQuery generator) produce valid queries; ensure
the replacement targets all occurrences and preserves other characters.
🧹 Nitpick comments (3)
src/data/queries/searchDoiQuery.ts (1)

40-42: UID values should be quoted for query safety.

The uidList items are joined without quoting, but they could contain special characters. For consistency with DOI handling and to prevent query injection, consider quoting each UID.

♻️ Proposed fix
   const uidPart = uidList && uidList.length > 0
-    ? `uid:(${uidList.join(OR)})`
+    ? `uid:(${uidList.map(quoteIdentifier).join(OR)})`
     : '';
src/app/(main)/doi.org/[...doi]/RelatedContent.tsx (1)

25-36: Type assertion to any bypasses type safety.

The work as any cast sidesteps TypeScript's type checking. If relatedIdentifiers exists on the Work type but isn't currently declared, consider extending the type definition in src/data/types.ts instead.

♻️ Proposed approach

If relatedIdentifiers is a valid field returned by the API:

// In src/data/types.ts, extend Work type:
interface RelatedIdentifier {
  relatedIdentifier: string
  relatedIdentifierType: string
  // ... other fields
}

// Add to Work type:
relatedIdentifiers?: RelatedIdentifier[]

Then update the function:

 function extractRelatedDois(work: Work | undefined): string[] {
   if (!work) return []
-  
-  // Check if relatedIdentifiers exists on the work object (even if not in the type)
-  const workWithRelatedIdentifiers = work as any
-  if (!workWithRelatedIdentifiers?.relatedIdentifiers) return []
-  
-  return workWithRelatedIdentifiers.relatedIdentifiers
-    .filter((identifier: any) => identifier.relatedIdentifierType === 'DOI')
-    .map((identifier: any) => identifier.relatedIdentifier)
+  if (!work.relatedIdentifiers) return []
+  
+  return work.relatedIdentifiers
+    .filter(identifier => identifier.relatedIdentifierType === 'DOI')
+    .map(identifier => identifier.relatedIdentifier)
     .filter(Boolean)
 }
src/data/managers/RelatedContentManager.ts (1)

104-108: Manager instance created on every render.

The hook creates a new RelatedContentManager instance on every render. While this is a lightweight class with no side effects, consider memoizing if performance becomes a concern with complex re-renders.

💡 Optional optimization using useMemo
+import { useMemo } from 'react'
+
 export function useRelatedContentManager(vars: QueryVar & { id: string }, connectionType: string | undefined) {
   const { loading, data, error, facetsLoading } = useDoiRelatedContentQuery(vars)
   const connectionCounts = useConnectionCounts(vars)
-  return new RelatedContentManager(vars, connectionType, data, loading, error, facetsLoading, connectionCounts)
+  return useMemo(
+    () => new RelatedContentManager(vars, connectionType, data, loading, error, facetsLoading, connectionCounts),
+    [vars, connectionType, data, loading, error, facetsLoading, connectionCounts]
+  )
 }

Comment on lines +17 to +19
function quoteIdentifier(identifier: string): string {
return `"${identifier}"`
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

quoteIdentifier doesn't escape embedded double quotes.

If the identifier contains a double quote character (e.g., 10.5555/test"value), the resulting query "10.5555/test"value" will be malformed. Consider escaping internal double quotes with a backslash per Lucene syntax.

🐛 Proposed fix
 function quoteIdentifier(identifier: string): string {
-  return `"${identifier}"`
+  // Escape internal double quotes for Lucene query syntax
+  const escaped = identifier.replace(/"/g, '\\"')
+  return `"${escaped}"`
 }
🤖 Prompt for AI Agents
In `@src/data/queries/searchDoiQuery.ts` around lines 17 - 19, The quoteIdentifier
function currently wraps the identifier in double quotes but doesn't escape
embedded double quotes, producing malformed queries for identifiers like
10.5555/test"value; update quoteIdentifier to escape internal double-quote
characters (replace " with \" per Lucene syntax) before surrounding the string
with quotes so that functions using quoteIdentifier (e.g., searchDoiQuery
generator) produce valid queries; ensure the replacement targets all occurrences
and preserves other characters.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/app/`(main)/doi.org/[...doi]/RelatedContent.tsx:
- Around line 54-77: The pagination URL is built using the raw doi variable when
passing the url prop to WorksListing, which can produce invalid URLs for DOIs
with reserved characters; update RelatedContent (the component rendering
WorksListing) to construct the URL from encoded DOI path segments (use
encodeURIComponent on each segment of doi.split('/') or otherwise encode the
DOI) and pass the encoded path to the url prop (replace the current
url={'/doi.org/' + doi + '/?'} expression) so all pagination links are safe and
valid.

In `@src/data/managers/RelatedContentManager.ts`:
- Around line 128-132: The current build of varsWithRelatedDois overwrites any
incoming uidList by setting uidList: relatedDois; instead merge the
caller-provided vars.uidList with relatedDois and dedupe so existing filters
aren’t lost. In the RelatedContentManager code that creates varsWithRelatedDois,
read the existing vars.uidList (if any), concat with relatedDois, remove
duplicates (e.g., via a Set) and assign that merged array to uidList; keep all
other properties from vars unchanged. Ensure safe handling when vars.uidList or
relatedDois are undefined/null.
🧹 Nitpick comments (1)
src/data/managers/RelatedContentManager.ts (1)

101-112: Avoid any when reading relatedIdentifiers.
This bypasses type-safety. Consider a narrow helper type or update Work to include relatedIdentifiers if the API guarantees it. Please verify the response shape.

[գն

Details

♻️ Suggested tightening
+type WorkWithRelatedIdentifiers = Work & {
+  relatedIdentifiers?: Array<{ relatedIdentifierType?: string; relatedIdentifier?: string }>
+}
+
 function extractRelatedDois(work: Work | undefined): string[] {
   if (!work) return []
   
   // Check if relatedIdentifiers exists on the work object (even if not in the type)
-  const workWithRelatedIdentifiers = work as any
+  const workWithRelatedIdentifiers = work as WorkWithRelatedIdentifiers
   if (!workWithRelatedIdentifiers?.relatedIdentifiers) return []
   
   return workWithRelatedIdentifiers.relatedIdentifiers
-    .filter((identifier: any) => identifier.relatedIdentifierType === 'DOI')
-    .map((identifier: any) => identifier.relatedIdentifier)
+    .filter(
+      (identifier): identifier is { relatedIdentifierType: string; relatedIdentifier: string } =>
+        identifier?.relatedIdentifierType === 'DOI' && !!identifier.relatedIdentifier
+    )
+    .map((identifier) => identifier.relatedIdentifier)
     .filter(Boolean) // Remove any undefined/null values
 }

@jrhoads jrhoads requested review from a team, bklaing2 and codycooperross January 20, 2026 08:36
Copy link
Member

@digitaldogsbody digitaldogsbody left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work! One question about the bot protection, but non-blocking.

@jrhoads jrhoads merged commit 0d06337 into master Feb 4, 2026
13 checks passed
@jrhoads jrhoads deleted the search-related-rest branch February 4, 2026 10:26
@cypress
Copy link

cypress bot commented Feb 4, 2026

akita    Run #1864

Run Properties:  status check passed Passed #1864  •  git commit 0d06337113: Merge pull request #550 from datacite/search-related-rest
Project akita
Branch Review master
Run status status check passed Passed #1864
Run duration 01m 49s
Commit git commit 0d06337113: Merge pull request #550 from datacite/search-related-rest
Committer Joseph Rhoads
View all properties for this run ↗︎

Test results
Tests that failed  Failures 0
Tests that were flaky  Flaky 1
Tests that did not run due to a developer annotating a test with .skip  Pending 3
Tests that did not run due to a failure in a mocha hook  Skipped 0
Tests that passed  Passing 47
View all changes introduced in this branch ↗︎

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants