Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c5c696e
GQL-118a: Add support for retrieving a collection revision
htranho Mar 18, 2026
7e40ca2
GQL-118: Update json key list, fix issue with non empty jsonKeys when…
htranho Mar 19, 2026
3d5d005
GQL-118: Remove debug logs
htranho Mar 19, 2026
61207d0
GQL-118: Fix issue with 'provider'
htranho Mar 20, 2026
4e4e4db
GQL-118: Add test for revisionId
htranho Mar 20, 2026
ab88041
GQL-118: Remove String conversion, remove error for not matching revi…
htranho Mar 20, 2026
55060f8
GQL-118: Refactoring
htranho Mar 21, 2026
cf41f3a
GQL-118: Remove throwing errors for list of json fields
htranho Mar 23, 2026
c89c92b
GQL-118: Remove error messages for list of fields
htranho Mar 23, 2026
2d0b588
GQL-118: Rorder revvisionId in list of CollectionInput
htranho Mar 23, 2026
538ae29
GQL-118: Add comment
htranho Mar 23, 2026
094e0c4
GQL-118: Add test for case of retrieving collection by revisionId
htranho Mar 23, 2026
ca8193a
GQL-118: Add tests
htranho Mar 23, 2026
29c4287
GQL-118: Get revisionId from requestInfo, empty out jsonKeys
htranho Mar 27, 2026
232c018
Merge branch 'main' into GQL-118-UMM
htranho Mar 27, 2026
db154ce
GQL-118: Update test for revisionId
htranho Mar 29, 2026
924f3a9
GQL-118: Add test for json only field
htranho Mar 29, 2026
084ed25
GQL-118: Add allRvisions to parameter list before calling cmrQuery
htranho Mar 30, 2026
ae8a41d
GQL-118: Add Notes to json only fields
htranho Mar 31, 2026
9dcb673
GQL-118: Add comment
htranho Apr 7, 2026
f49ea27
GQL-118: Remove 'providerId' as requested
htranho Apr 7, 2026
611ad91
GQL-118: Fix vulnerabilities
htranho Apr 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions src/cmr/concepts/concept.js
Original file line number Diff line number Diff line change
Expand Up @@ -643,14 +643,17 @@ export default class Concept {
fetchUmm(searchParams, requestedKeys, providedHeaders) {
this.logKeyRequest(requestedKeys, 'umm')

const { revisionId } = searchParams
Comment thread
macrouch marked this conversation as resolved.
Outdated

// Construct the promise that will request data from the umm endpoint
return cmrQuery({
conceptType: this.getConceptType(),
params: pick(snakeCaseKeys(searchParams), this.getPermittedUmmSearchParams()),
nonIndexedKeys: this.getNonIndexedKeys(),
headers: providedHeaders,
options: {
format: 'umm_json'
format: 'umm_json',
allRevisions: revisionId ? true : undefined
}
})
}
Expand Down Expand Up @@ -943,6 +946,7 @@ export default class Concept {
* @param {Array} ummKeys Array of the keys requested in the query
*/
async parseUmm(ummResponse, ummKeys) {
const { revisionId } = this.params.params || {}
// Pull out the key mappings so we can retrieve the values below
const { ummKeyMappings } = this.requestInfo

Expand All @@ -961,14 +965,26 @@ export default class Concept {
items.forEach((item, itemIndex) => {
const normalizedItem = this.normalizeUmmItem(item)

// Filter out items that don't match the requested revisionId
// This is necessary because the umm endpoint returns multiple revisions
if (revisionId && normalizedItem.meta['revision-id'].toString() !== revisionId.toString()) {
return
}

// Creates unique item keys regardless of whether or not
// a user calls for data with similar conceptIds (as is the case with revisions)
const itemKey = this.generateUmmItemKey(itemIndex, normalizedItem)

this.setEssentialUmmValues(itemKey, normalizedItem)

this.setUmmItems(item, itemKey, ummKeys, ummKeyMappings,)
this.setUmmItems(item, itemKey, ummKeys, ummKeyMappings)
})

// Update the item count if we filtered out items
if (revisionId) {
const filteredItemCount = Object.keys(this.items).length
this.setUmmItemCount(filteredItemCount)
}
}

/**
Expand Down
95 changes: 95 additions & 0 deletions src/resolvers/__tests__/collection.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1083,6 +1083,101 @@ describe('Collection', () => {
})
})
})

describe('when retrieving by revisionId', () => {
Comment thread
macrouch marked this conversation as resolved.
test('returns the specific revision when revisionId is provided', async () => {
nock(/example-cmr/)
.defaultReplyHeaders({
'CMR-Took': 7,
'CMR-Request-Id': 'abcd-1234-efgh-5678'
})
.get('/search/collections.umm_json?all_revisions=true&concept_id=C100000-EDSC')
.reply(200, {
items: [{
meta: {
'concept-id': 'C100000-EDSC',
'revision-id': 2
},
umm: {}
}]
})

const response = await server.executeOperation({
variables: {
params: {
conceptId: 'C100000-EDSC',
revisionId: '2'
}
},
query: `
query GetCollection($params: CollectionInput!) {
collection(params: $params) {
conceptId
revisionId
}
}
`
}, {
contextValue
})

const { data, errors } = response.body.singleResult

if (errors) {
console.error('GraphQL errors:', errors)
throw new Error('GraphQL query resulted in errors')
}
Comment thread
macrouch marked this conversation as resolved.
Outdated

expect(data).toEqual({
collection: {
conceptId: 'C100000-EDSC',
revisionId: '2'
}
})
})

test('returns null when the specified revision does not exist', async () => {
nock(/example-cmr/)
.defaultReplyHeaders({
'CMR-Took': 7,
'CMR-Request-Id': 'abcd-1234-efgh-5678'
})
.get('/search/collections.umm_json?all_revisions=true&concept_id=C100000-EDSC')
.reply(200, {
items: [{
meta: {
'concept-id': 'C100000-EDSC',
'revision-id': 4
},
umm: {}
}]
})

const response = await server.executeOperation({
query: `query GetCollection($params: CollectionInput!) {
collection(params: $params) {
conceptId
revisionId
}
}`,
variables: {
params: {
conceptId: 'C100000-EDSC',
revisionId: '2'
}
}
}, {
contextValue
})

const { data, errors } = response.body.singleResult

expect(errors).toBeUndefined()
expect(data).toEqual({
collection: null
})
})
})
})
})

Expand Down
3 changes: 3 additions & 0 deletions src/types/collection.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -887,6 +887,9 @@ input CollectionInput {

"If this parameter is set (e.g. include_tags=gov.nasa.earthdata.search.*,gov.nasa.echo.*), the collection results will contain an additional field 'tags' within each collection. The value of the tags field is a list of tag_keys that are associated with the collection. Only the tags with tag_key matching the values of include_tags parameter (with wildcard support) are included in the results."
includeTags: String

"The revision id of the collection."
revisionId: String
}

type Query {
Expand Down
46 changes: 46 additions & 0 deletions src/utils/__tests__/parseRequestedFields.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -696,4 +696,50 @@ describe('parseRequestedFields', () => {
})
})
})

describe('when revisionId is provided', () => {
test('moves all keys to ummKeys when revisionId is present', () => {
const requestInfo = {
name: 'collection',
alias: 'collection',
args: {
params: {
revisionId: '1'
}
},
fieldsByTypeName: {
Collection: {
conceptId: {
name: 'conceptId',
alias: 'conceptId',
args: {},
fieldsByTypeName: {}
},
title: {
name: 'title',
alias: 'title',
args: {},
fieldsByTypeName: {}
},
keyTwo: {
Comment thread
macrouch marked this conversation as resolved.
Outdated
name: 'keyTwo',
alias: 'keyTwo',
args: {},
fieldsByTypeName: {}
}
}
}
}

const requestedFields = parseRequestedFields(requestInfo, keyMap, 'collection')

expect(requestedFields).toEqual({
jsonKeys: [],
metaKeys: [],
ummKeys: ['conceptId', 'keyTwo', 'title'],
ummKeyMappings,
isList: false
})
})
})
})
10 changes: 9 additions & 1 deletion src/utils/cmrQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,16 @@ export const cmrQuery = ({
url: `${process.env.cmrRootUrl}/${path}`
}

// Create a new object with the original params and add all_revisions if needed
const updatedParams = options.allRevisions
Comment thread
htranho marked this conversation as resolved.
Outdated
? {
...params,
all_revisions: true
}
: params

// Append any query arguments based on provided params
const cmrParameters = prepKeysForCmr(snakecaseKeys(params), nonIndexedKeys)
const cmrParameters = prepKeysForCmr(snakecaseKeys(updatedParams), nonIndexedKeys)

const { env } = process
const { maximumQueryPathLength } = env
Expand Down
16 changes: 13 additions & 3 deletions src/utils/parseRequestedFields.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { CONCEPT_TYPES, PSEUDO_FIELDS } from '../constants'
* @param {Array} requestedFields Fields requested
* @param {Object} keyMap Mappings of UMM fields to requestable fields
* @param {String} conceptName Name of the concept () to lookup requested fields in the query
* @param {Object} params Query parameters that may affect key routing
*/
export const parseRequestedFields = (parsedInfo, keyMap, conceptName) => {
let { fieldsByTypeName } = parsedInfo
Expand Down Expand Up @@ -236,7 +237,10 @@ export const parseRequestedFields = (parsedInfo, keyMap, conceptName) => {
))

// If all requested keys are available in json, use json because its all indexed in CMR
if (difference(ummKeys, sharedKeys).length === 0) {
// UNLESS revisionId, in which case JSON endpoint doesn't support it
const { revisionId } = parsedInfo.args.params || {}

if (difference(ummKeys, sharedKeys).length === 0 && !revisionId) {
return {
jsonKeys: requestedFields,
metaKeys,
Expand All @@ -247,12 +251,18 @@ export const parseRequestedFields = (parsedInfo, keyMap, conceptName) => {
}

// Requested keys that are not UMM and not CONCEPT_TYPES keys must be json
const jsonKeys = requestedFields.filter((field) => (
let jsonKeys = requestedFields.filter((field) => (
!ummKeys.includes(field)
&& !CONCEPT_TYPES.includes(field)
&& !PSEUDO_FIELDS.includes(field)
))

// When revisionId, all data must come from UMM endpoint (JSON endpoint doesn't support it)
if (revisionId && jsonKeys.length > 0) {
ummKeys = [...ummKeys, ...jsonKeys]
Comment thread
macrouch marked this conversation as resolved.
Outdated
jsonKeys = []
}

// If we already have to go to the json endpoint get as much info from there as possible
if (jsonKeys.length > 0) {
// Move any requested key that is shared over to the jsonKeys
Expand All @@ -268,7 +278,7 @@ export const parseRequestedFields = (parsedInfo, keyMap, conceptName) => {

// If facets were requested, we need to ensure we have at least 1 json key
// some do because facets are not available from the umm endpoint
if (metaKeys.includes('collectionFacets') && jsonKeys.length === 0) {
if (metaKeys.includes('collectionFacets') && jsonKeys.length === 0 && !revisionId) {
jsonKeys.push('conceptId')

// Remove the concept id from the ummKeys (if it exists) because we just moved it to the jsonKeys
Expand Down
2 changes: 2 additions & 0 deletions src/utils/umm/collectionKeyMap.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"hasTransforms",
"hasVariables",
"processingLevelId",
"provider",
Comment thread
htranho marked this conversation as resolved.
"shortName",
"title",
"versionId"
Expand Down Expand Up @@ -52,6 +53,7 @@
"processingLevel": "umm.ProcessingLevel",
"processingLevelId": "umm.ProcessingLevel.Id",
"projects": "umm.Projects",
"provider": "meta.provider-id",
"providerId": "meta.provider-id",
Comment thread
htranho marked this conversation as resolved.
Outdated
"publicationReferences": "umm.PublicationReferences",
"purpose": "umm.Purpose",
Expand Down
Loading