Skip to content

GQL-118: Query for a collection by a specific revision-id#177

Open
htranho wants to merge 31 commits intomainfrom
GQL-118
Open

GQL-118: Query for a collection by a specific revision-id#177
htranho wants to merge 31 commits intomainfrom
GQL-118

Conversation

@htranho
Copy link
Contributor

@htranho htranho commented Jan 26, 2026

Overview

What is the feature?

This feature adds support for fetching specific revisions of collections in the GraphQL API. It allows querying a particular revision of a collection by its concept ID and revision ID.

What is the Solution?

The solution involves modifying the Collection concept class and related resolvers to handle revision-specific queries. Key changes include:

  1. Update methos to fetch and parse revision-specific data from CMR. For fetching, changes made in case of umm keys are present:
    when revision ID is given, fetch data from the 'concepts' endpoint and also from the revisions endpoint if needed (meta keys given).
    For merging, changes are made because response from the 'concepts' endpoint is different from existing concept type end point.
  2. Adding checks to prevent certain field (revisions) from being queried when requesting a specific revision.
  3. Updating the CollectionInput type to include a revisionId parameter.

What areas of the application does this impact?

This change impacts several areas of the application:

  1. src/cmr/concepts/concept.js: Major updates to the Concept class to support revision-specific queries and merging.
  2. src/resolvers/collection.js: Updates to collection resolvers to handle revision-specific queries and add appropriate error checks.
  3. src/types/collection.graphql: Addition of revisionId to the CollectionInput type.
  4. src/resolvers/__tests__/collection.test.js: New tests added to cover the new functionality and error cases.

Testing

Reproduction steps

  • Environment for testing: Local development environment
  • Collection to test with: Any collection with multiple revisions in CMR
  1. Start the GraphQL server locally.
  2. Use a GraphQL client (e.g., GraphQL Playground) to send queries.
  3. Test querying a specific revision of a collection, example:
    query Collection($params: CollectionInput) {
         collection(params: $params) {
             conceptId
             shortName
             revisionId
             revisionDate
             associationDetails
         }
     }
     with
     {
         "params": {
             "conceptId": "C1200237617-MMT_1",
             "revisionId": "23"
         }
     }
  • Use MMT to revome or add Variable or any other concept type's associations to this collection. check if changes seen in graphql.
  • Request all collection fields. Known error in main branch and also this branch is the field generateVariableDrafts.
  1. Verify that the correct revision is returned with the requested fields.
  2. Test error cases by requesting fields that are not allowed for specific revisions:
    query {
      collection(params: { conceptId: "C1200237617-MMT_1", revisionId: "23" }) {
        conceptId
        revisionId
        revisions {
          count
        }
      }
    }
  3. Verify that appropriate error messages are returned.
  4. Test fetching a collection without specifying a revision ID to ensure backward compatibility.

Checklist

  • I have added automated tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings

@htranho htranho marked this pull request as draft January 26, 2026 23:11
@codecov
Copy link

codecov bot commented Jan 26, 2026

Codecov Report

❌ Patch coverage is 96.96970% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 99.92%. Comparing base (881c63e) to head (f1653a0).

Files with missing lines Patch % Lines
src/cmr/concepts/concept.js 96.07% 2 Missing ⚠️

❌ Your patch check has failed because the patch coverage (96.96%) is below the target coverage (100.00%). You can increase the patch coverage or adjust the target coverage.
❌ Your project check has failed because the head coverage (99.92%) is below the target coverage (100.00%). You can increase the head coverage or adjust the target coverage.

Additional details and impacted files
@@             Coverage Diff             @@
##              main     #177      +/-   ##
===========================================
- Coverage   100.00%   99.92%   -0.08%     
===========================================
  Files          115      115              
  Lines         2706     2765      +59     
  Branches       330      346      +16     
===========================================
+ Hits          2706     2763      +57     
- Misses           0        2       +2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@htranho htranho marked this pull request as ready for review January 30, 2026 22:47
@htranho htranho changed the title GQL-118: Query for a concept by a specific revision-id GQL-118: Query for a collection by a specific revision-id Jan 30, 2026
@htranho htranho marked this pull request as draft February 2, 2026 15:12
@htranho htranho marked this pull request as ready for review February 2, 2026 18:08
* @param {String} revisionId The specific revision ID to extract
* @param {Array} ummKeys Keys that were requested
*/
async parseAndMergeMetaFields(allRevisionsResponse, revisionId, ummKeys) {
Copy link
Contributor

Choose a reason for hiding this comment

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

We need to move most of the logic in this class over to the src/utils/parseRequestedFields.js

Copy link
Contributor

Choose a reason for hiding this comment

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

I think I was wrong that was from the PR notes I'm not really seeing how we'd do that here and this certainly isn't creating new scope in this class behavior like this is already there

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Now moved processing of keys to parseRequestedKeys

Copy link
Contributor

@macrouch macrouch left a comment

Choose a reason for hiding this comment

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

Ed is right, most of what you have in concept.js needs to be moved to parseRequestedFields.js.

The concept class should just be given lists of keys in the requestInfo, and the concept class should only have the knowledge that it needs to fetch the provided keys. It shouldn't be responsible for parsing parameters, or parsing ummKeyMappings to determine if different requests should be made. The concept class logic should be "if ummKeys exist, fetchUmm. if jsonKeys exist, fetchJson. if concept endpoint should be fetched, fetch the concept endpoint"

context.singleRevisionRequest = !!args.params?.revisionId

// Add the conceptId to the context
context.conceptId = args.params?.conceptId
Copy link
Contributor

Choose a reason for hiding this comment

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

This should not be done. source is where the nested resolvers should be getting the conceptId

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done, now reverted unnecessary change: no need to store conceptId in context.

Copy link
Contributor

Choose a reason for hiding this comment

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

This file is still missing the documentation for any fields that can throw an error when a revision id is provided

Copy link
Contributor

Choose a reason for hiding this comment

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

I actually don't know either where those get added. I'd assume its either in magidoc/pages/02.Query Fields/04.Items.md or its something that we just add it on the collection.graphql file and its kind of like @deprecated(reason: "Use params.includeTags") apparently you can actually use custom schema directives https://www.apollographql.com/docs/apollo-server/v3/schema/creating-directives

Copy link
Contributor

Choose a reason for hiding this comment

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

It's just the string that is above every field in this file

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tried @error directive, but the server doesn't like it:
ERROR Uncaught Exception {"errorType":"Error","errorMessage":"Unknown directive "@error".
So I checked in as comment at the field for now.

@mandyparson
Copy link
Contributor

Once you've made those suggested changes, double check that you can query revisions on a collection when you don't pass revisionId as an argument. Right now, that feature isn't working.

)

// Second: If meta-only fields are needed, fetch all revisions from search endpoint
const needsMetaFields = ummKeys.some((key) => this.requestInfo.ummKeyMappings[key]?.startsWith('meta.')
Copy link
Contributor

Choose a reason for hiding this comment

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

We have the list of meta keys or at least it seems like it, on const { jsonKeys, metaKeys, ummKeys } = this.requestInfo

can we use that field for the conditional?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Now moved to parseRequestFields

* @param {String} revisionId The specific revision ID to extract
* @param {Array} ummKeys Keys that were requested
*/
async parseAndMergeMetaFields(allRevisionsResponse, revisionId, ummKeys) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think I was wrong that was from the PR notes I'm not really seeing how we'd do that here and this certainly isn't creating new scope in this class behavior like this is already there

}

// Get the existing item key (should be only one item from concepts endpoint)
const existingItemKeys = Object.keys(this.items)
Copy link
Contributor

Choose a reason for hiding this comment

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

might just be able to use the existing this.getItem() in this case but, not certain

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Now using this.getItems()

@htranho
Copy link
Contributor Author

htranho commented Feb 3, 2026

Once you've made those suggested changes, double check that you can query revisions on a collection when you don't pass revisionId as an argument. Right now, that feature isn't working.

I tried main, that is also broken


describe('Collection', () => {
describe('fetch', () => {
test('fetches a specific revision of a collection', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove the 'fetch' field from under the describe block with capital letter 'Collection' (line 50 in your resolvers) and place all these tests you've added under describe block for lower case 'collection' (line 17). Reason being that these are all done when retrieving a collection, not a field on collection.

"fetches a specific revision of a collection when revisionId is provided in arguments"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't see any new changes?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, missed a 'git push'

})
})

test('fetches all revisions when meta-only fields are needed', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

What is this test supposed to be doing? It wants to fetch all revisions but you passed it a revisions id and did not request revisions in your query.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done, updated the description to make it more clear what been tested.

Copy link
Contributor

@macrouch macrouch left a comment

Choose a reason for hiding this comment

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

I've found a couple of queries that are not working correctly

  1. This query is sending too many requests to CMR.
query Collection($params: CollectionInput) {
  collection(params: $params) {
    shortName
  }
}

By only requesting the shortName, which is a UMM field, cmr-graphql should only make one request to CMR for the UMM (from the concept endpoint). Currently it is making a request to both https://cmr.sit.earthdata.nasa.gov/search/collections.json and https://cmr.sit.earthdata.nasa.gov/search/concepts/C1200237617-MMT_1/23.umm_json. There is no reason to send the request to collections.json

  1. This query is potentially returning incorrect data.
query Collection($params: CollectionInput) {
  collection(params: $params) {
    boxes
  }
}

boxes is only in the json endpoint, so you have to be sure to return the boxes from the correct revision. Currently it is only sending the conceptId to the json endpoint, so that will be returning the boxes from the latest revision, which could be different from the revision the user provided. The only way I can think to solve this is if revisionId is provided and jsonKeys are populated, you would have to include the all_revisions parameter and filter the returned data to the correct revision.

Both of these use cases should have a test that shows the correct requests are being made and the correct data being returned.

Suggested Changes:

  • concept.js: revisionId and conceptId are still being used to determine logic in this file. You are making the logic in this file more complicated by trying to do it this way. parseRequestedFields should be returning everything necessary to send these requests to CMR from concept.js

    Following the standing conventions of parseRequestedFields, I'd return a conceptEndpointKeys, which contains every requested field that can be returned from the concept endpoint. The field should only be present in one of the 3 list of keys returned (json, umm, conceptEndpoint). metaKeys is useful for determining which fields should be in conceptEndpointKeys or ummKeys, but it should not be necessary in concept.js

    Then in concept.js you don't have to look at the request at all, if conceptEndpointKeys exist, call the new fetch function, if ummKeys exist, call fetchUmm and if jsonKeys exist call fetchJson

  • I feel like using revisions in the names of things is slightly limiting. The real thing you are trying to indicate is the concepts endpoint needs to be used instead of the search endpoint. If at some point in the future we have another need to use the concepts endpoint, what you are adding now should be able to be used to accomplish that. I'd rename fetchRevisionUmm to fetchConceptEndpoint, and make the revisionId parameter optional. Then update the path as necessary

  • parseAndMergeMetaFields, I don't understand what this function is doing, but I don't think any of it is necessary. If you make the above changes to parseRequestedFields I think you end up with a parseConceptEndpoint that looks very similar to parseJson and parseUmm

I'm sure there will be other issues in concept.js, like you'll have to filter a list of json revisions down to the correct revision from my second example above, but I think if you start with parseRequestedFields returning the right data, all the logic in concept.js should be simplified

Comment on lines +254 to +255
Error: The 'revisions' field cannot be requested when querying a specific revision.
Remove the 'revisionId' parameter from the collection query to fetch all revisions.
Copy link
Contributor

Choose a reason for hiding this comment

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

This reads like something has gone wrong, when it is just supposed to be informing the user on a condition on this field.

Note: This field is not supported if you are performing a `collection` query and have provided the `revisionId` parameter. `null` will be returned with an error

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Now updated.

Comment on lines +52 to +58
const { revisionId } = info.variableValues?.params ?? {}
if (revisionId) {
throw new Error(
'The "revisions" field cannot be requested when querying a specific revision. '
+ 'Remove the "revisionId" parameter from the collection query to fetch all revisions.'
)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

When I use the example error query in your PR description, this revisionId is undefined and this error is not what is thrown. I get a CMR error: "The mime type [application/json] is not supported when all_revisions = true."

I'm not sure why this is on two lines

Copy link
Contributor Author

Choose a reason for hiding this comment

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

solved issue with revisionId undefined. Now just store it in the context, that cover all ways how the parameter revisionId is placed in the query.

@eudoroolivares2016 eudoroolivares2016 self-requested a review March 2, 2026 22:54
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.

4 participants