Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
56 changes: 55 additions & 1 deletion src/datasources/collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,61 @@ import { findPreviousRevision } from '../utils/findPreviousRevision'
export const fetchCollections = async (params, context, parsedInfo) => {
const { headers } = context

const requestInfo = parseRequestedFields(parsedInfo, collectionKeyMap, 'collection')
const { revisionId, conceptId } = params

// If a specific revisionId is requested, fetch all revisions and filter
if (revisionId) {
Comment thread
macrouch marked this conversation as resolved.
Outdated
const allRevisionsParams = {
Comment thread
htranho marked this conversation as resolved.
Outdated
...params,
allRevisions: true
}

// Parse requested fields - this determines which fields to fetch
const requestInfo = parseRequestedFields(parsedInfo, collectionKeyMap, 'collection', allRevisionsParams)

// Ensure revisionId is included in the requested fields so we can filter by it
const { ummKeys } = requestInfo
if (!ummKeys.includes('revisionId')) {
Comment thread
htranho marked this conversation as resolved.
Outdated
ummKeys.push('revisionId')
}

const collection = new Collection(headers, requestInfo, allRevisionsParams)

// Query CMR for all revisions
collection.fetch(allRevisionsParams)

// Parse the response from CMR
await collection.parse(requestInfo)

// Get all items and filter for the specific revision
const allItems = collection.getFormattedResponse()

// Convert revisionId to string for comparison since CMR returns it as a string
const requestedRevisionIdStr = String(revisionId)
Comment thread
macrouch marked this conversation as resolved.
Outdated

// For a single collection query with revisionId, return just that revision
const specificRevision = allItems.find((item) => (
String(item.revisionId) === requestedRevisionIdStr
))

if (!specificRevision) {
// Check if the revision exists but is too old (CMR only keeps last 10)
const latestRevisionId = allItems.length > 0 ? parseInt(allItems[0].revisionId, 10) : 0
const oldestAvailableRevision = Math.max(1, latestRevisionId - 9)
const requestedRevisionId = parseInt(revisionId, 10)

if (requestedRevisionId < oldestAvailableRevision) {
throw new Error(`Revision ${revisionId} is no longer stored. Please select an available revision from ${oldestAvailableRevision} to ${latestRevisionId}.`)
}

throw new Error(`Revision ${revisionId} not found for collection ${conceptId}. Available revisions: ${allItems.map((item) => item.revisionId).join(', ')}`)
}

return [specificRevision]
}
Comment thread
macrouch marked this conversation as resolved.
Outdated

// Normal case: no revisionId specified, return latest revision
const requestInfo = parseRequestedFields(parsedInfo, collectionKeyMap, 'collection', params)

const collection = new Collection(headers, requestInfo, params)

Expand Down
53 changes: 52 additions & 1 deletion src/resolvers/collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,58 @@ export default {
collection: async (source, args, context, info) => {
const { dataSources } = context

const result = await dataSources.collectionSourceFetch(args, context, parseResolveInfo(info))
// Support both old deprecated syntax (conceptId directly) and new syntax (params object)
// Old: collection(conceptId: "C123")
Comment thread
htranho marked this conversation as resolved.
Outdated
// New: collection(params: { conceptId: "C123" })
const params = args.params || args
const { revisionId } = params

// Fields that are only available from JSON endpoint and cannot be retrieved for old revisions
const jsonOnlyFields = [
Comment thread
htranho marked this conversation as resolved.
Outdated
Comment thread
macrouch marked this conversation as resolved.
Outdated
'archiveCenter',
'boxes',
'browseFlag',
'cloudHosted',
'consortiums',
'coordinateSystem',
'datasetId',
'hasGranules',
'lines',
'onlineAccessFlag',
'organizations',
'points',
'polygons',
'revisions',
'tagDefinitions',
'tags',
'timeEnd',
'timeStart'
]

// If querying a specific revision, check if any JSON-only fields are requested
if (revisionId) {
const parsedInfo = parseResolveInfo(info)
const { fieldsByTypeName } = parsedInfo
const { Collection: collectionFields = {} } = fieldsByTypeName

const requestedFields = Object.keys(collectionFields)
const requestedJsonOnlyFields = requestedFields.filter(
(field) => jsonOnlyFields.includes(field)
)

if (requestedJsonOnlyFields.length > 0) {
throw new Error(
Comment thread
macrouch marked this conversation as resolved.
Outdated
`The following fields are not available when querying a specific revision: ${requestedJsonOnlyFields.join(', ')}. `
Comment thread
macrouch marked this conversation as resolved.
Outdated
+ 'These fields are only available from the JSON endpoint which does not support historical revisions.'
)
}
}

const result = await dataSources.collectionSourceFetch(
params,
context,
parseResolveInfo(info)
)

const [firstResult] = result

Expand Down
3 changes: 3 additions & 0 deletions src/types/collection.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -882,6 +882,9 @@ input CollectionInput {
"The unique concept id assigned to the collection."
conceptId: String!

"The revision id of the Collection."
revisionId: String
Comment thread
mandyparson marked this conversation as resolved.
Outdated

"If this parameter is set to 'true' this will include a flag indicating true or false if the collection has any granules at all."
includeHasGranules: Boolean

Expand Down
37 changes: 33 additions & 4 deletions src/utils/parseRequestedFields.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ 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) => {
export const parseRequestedFields = (parsedInfo, keyMap, conceptName, params = {}) => {
let { fieldsByTypeName } = parsedInfo

const { name } = 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 allRevisions=true, in which case JSON endpoint doesn't support it
const { allRevisions } = params

if (difference(ummKeys, sharedKeys).length === 0 && !allRevisions) {
return {
jsonKeys: requestedFields,
metaKeys,
Expand All @@ -247,12 +251,29 @@ 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 allRevisions=true, ALL data must come from UMM endpoint (JSON endpoint doesn't support it)
// Check if any JSON-only fields were requested and throw an error
if (allRevisions && jsonKeys.length > 0) {
const jsonOnlyFields = jsonKeys.filter((field) => !sharedKeys.includes(field))

if (jsonOnlyFields.length > 0) {
throw new Error(
`The following fields are not available when querying with allRevisions: ${jsonOnlyFields.join(', ')}. `
+ 'These fields are only available from the JSON endpoint which does not support historical revisions.'
Comment thread
macrouch marked this conversation as resolved.
Outdated
)
}

// If only shared keys are in jsonKeys, move them to ummKeys for allRevisions queries
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 @@ -267,8 +288,16 @@ 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
// because facets are not available from the umm endpoint
// However, if allRevisions=true, we cannot use the JSON endpoint at all
if (metaKeys.includes('collectionFacets') && jsonKeys.length === 0) {
if (allRevisions) {
throw new Error(
'Facets are not available when querying with allRevisions. '
+ 'Facets are only available from the JSON endpoint which does not support historical revisions.'
Comment thread
macrouch marked this conversation as resolved.
Outdated
)
}

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