UNPKG

apollo-link-contentful

Version:

Perform GraphQL queries against Contentful’s Rest API. No more, query size limits! No more, query complexities!!

299 lines (264 loc) • 8.54 kB
import { omit } from 'lomit' import { DefinitionKind, SelectionKind, VariableKind, } from './constants' /** * Return root query object to work against based on if an operationName key * is set in operation. * * @param {Operation} operation * @return {Object} */ const getRootQuery = (operation) => { const { query, operationName } = operation return query && query.hasOwnProperty(operationName) ? query[operationName] : query } /** * Return the root key that will contain the data returned via the GraphQL query. * * @param {Operation} operation * @return {string} */ export const getRootKey = (operation) => { const definition = getRootQuery(operation).definitions.find(definition => definition.operation === 'query') if (definition) { const selection = definition.selectionSet.selections.find(selection => selection.name.kind === 'Name') if (selection) { return selection.name.value } } return null } /** * Contentful API - Search Parameters * @ref https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters */ const contentfulReservedParameters = [ 'access_token', 'id', 'include', 'locale', 'content_type', 'select', 'query', 'links_to_entry', 'links_to_asset', 'order', 'limit', 'skip', 'mimetype_group', 'preview', ] /** * Pass in a GraphQL query variable field and get the equivalent * Contentful REST API query argument. * * @ref https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/search-on-references * * @param {string} variableKey * @return {string} */ const getSearchKey = (variableKey) => { if (variableKey.endsWith('_in')) { return `fields.${variableKey.replace('_in', '')}[in]` } if (variableKey.endsWith('_not')) { return `fields.${variableKey.replace('_not', '')}[ne]` } if (variableKey.endsWith('_exists')) { return `fields.${variableKey.replace('_exists', '')}[exists]` } if (variableKey.endsWith('_not_in')) { return `fields.${variableKey.replace('_not_in', '')}[nin]` } // if (variableKey.endsWith('_contains')) { // return `fields.${variableKey.replace('_contains', '')}[?]` // } // if (variableKey.endsWith('_not_contains')) { // return `fields.${variableKey.replace('_not_contains', '')}[?]` // } // @todo Add support for `x[all]` - Ryan // @todo Add support for `x[match]` - Ryan // @todo Add support for `x[gt]` - Ryan // @todo Add support for `x[gte]` - Ryan // @todo Add support for `x[lt]` - Ryan // @todo Add support for `x[lte]` - Ryan // @todo Add support for `x[near]` - Ryan // @todo Add support for `x[within]` - Ryan return `fields.${variableKey}` } /** * Build a map that associates the variables passed into the query * with the actual fields/arguments that are being requested in the * API request. * * @param {Object} operation * @return {Object} */ const buildVariableMap = (operation) => { const variableMap = {} getRootQuery(operation).definitions .filter(definition => definition.kind === DefinitionKind.OperationDefinition) .forEach(definition => { definition.selectionSet.selections.forEach(selection => { selection.arguments.forEach(argument => { if (argument.value && argument.value.fields) { argument.value.fields.forEach(field => { const validField = field.value && field.value.name && field.value.name.value && field.kind && field.name && field.name.value if (validField) { variableMap[field.value.name.value] = { kind: field.kind, field: field.name.value, } } }) } else { const validArgument = argument.value && argument.value.name && argument.value.name.value && argument.kind && argument.name && argument.name.value if (validArgument) { variableMap[argument.value.name.value] = { kind: argument.kind, field: argument.name.value, } } } }) }) }) return variableMap } /** * GraphQL queries specify the expected shape of the response from the API, * so this method attempts to extract that in a format that we can apply * to the parsed REST API response, in order to make sure the response * fulfills the contract of the request. * * @param {Array.<Object>} selectionSet * @param {Array.<Object>} definitions * @return {Object} */ const extractSelections = (selectionSet, definitions) => { if (!( selectionSet && selectionSet.selections && selectionSet.selections.length )) return null const selections = {} selectionSet.selections.forEach(selection => { if (selection.kind === SelectionKind.Field) { if (selection.name && selection.name.value) { selections[selection.name.value] = extractSelections( selection.selectionSet, definitions ) } } else if (selection.kind === SelectionKind.FragmentSpread) { const fragmentDefinition = definitions.find( definition => definition.kind === DefinitionKind.FramentDefinition && definition.name && definition.name.value && selection.name && selection.name.value && definition.name.value === selection.name.value ) if (fragmentDefinition) { selections[`...${selection.name.value}`] = extractSelections( fragmentDefinition.selectionSet, definitions ) } } }) return selections } /** * * * @param {Object} operation * @return {Object} */ export const buildDefinitionMap = (operation) => { const { query } = operation const operationDefinition = query.definitions.find( definition => definition.kind === DefinitionKind.OperationDefinition ) if (!( operationDefinition && operationDefinition.name && operationDefinition.name.value )) { return {} } return { [operationDefinition.name.value]: extractSelections(operationDefinition.selectionSet, query.definitions) } } /** * Convert the query variables to a format the Contentful API understands * * @param {Object} query * @param {Object} variables * @param {Object} variableMap * @param {string} operationName * @retrun {Object} */ export const parseQueryVariables = (operation) => { const { variables } = operation const variableMap = buildVariableMap(operation) const operationVariables = getRootQuery(operation) .definitions .find(definition => definition.kind === DefinitionKind.OperationDefinition) .variableDefinitions .map(variableDefinition => variableDefinition.variable.name.value) const operationQueries = Object.keys(variableMap) .filter(variableKey => { return variables && variables.hasOwnProperty(variableKey) && variableMap && variableMap.hasOwnProperty(variableKey) }) .map(variableKey => { try { switch (variableMap[variableKey].kind) { case VariableKind.Argument: // If the variable key is known search parameter for the Contentful API, // just pass it through un-parsed const fieldExists = variableMap[variableKey] && variableMap[variableKey].field if (fieldExists && contentfulReservedParameters.includes(variableMap[variableKey].field)) { // Exclude preview variable is not set to true if (variableMap[variableKey].field === 'preview' && !variables[variableKey]) { return null } return { [variableMap[variableKey].field]: variables[variableKey] } } break case VariableKind.ObjectField: // Convert variable into query format supported in Contentful API const queryKey = getSearchKey(variableMap[variableKey].field) return { [queryKey]: variables[variableKey] } default: return null } } catch (err) { console.error(err) return null } }) .filter(variable => !!variable) .reduce((acc, cur) => { return {...acc, ...cur } }, {}); return { ...operationQueries, ...omit(variables, operationVariables) } }