UNPKG

apongo

Version:

Create Mongo aggregation pipelines with recursive joins for Apollo queries.

231 lines (202 loc) 6.64 kB
const { getNamedType, isCompositeType, GraphQLObjectType, GraphQLUnionType, } = require('graphql'); const { getArgumentValues } = require('graphql/execution/values'); function getArgVal(resolveInfo, argument) { if (argument.kind === 'Variable') { return resolveInfo.variableValues[argument.name.value]; } if (argument.kind === 'BooleanValue') { return argument.value; } return null; } function firstKey(obj) { for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { return key; } } return null; } function getType(resolveInfo, typeCondition) { const { schema } = resolveInfo; const { kind, name } = typeCondition; if (kind === 'NamedType') { const typeName = name.value; return schema.getType(typeName); } return null; } function skipField(resolveInfo, { directives = [] }) { let skip = false; directives.forEach((directive) => { const directiveName = directive.name.value; if (Array.isArray(directive.arguments)) { const ifArgumentAst = directive.arguments.find((arg) => arg.name && arg.name.value === 'if'); if (ifArgumentAst) { const argumentValueAst = ifArgumentAst.value; if (directiveName === 'skip') { skip = skip || getArgVal(resolveInfo, argumentValueAst); } else if (directiveName === 'include') { skip = skip || !getArgVal(resolveInfo, argumentValueAst); } } } }); return skip; } function getFieldFromAST(ast, parentType) { if (ast.kind === 'Field') { const fieldNode = ast; const fieldName = fieldNode.name.value; if (!(parentType instanceof GraphQLUnionType)) { const type = parentType; return type.getFields()[fieldName]; } // XXX: TODO: Handle GraphQLUnionType } return undefined; } function fieldTreeFromAST(inASTs, resolveInfo, initTree = {}, options = {}, parentType) { const { variableValues } = resolveInfo; const fragments = resolveInfo.fragments || {}; const asts = Array.isArray(inASTs) ? inASTs : [inASTs]; initTree[parentType.name] = initTree[parentType.name] || {}; return asts.reduce((tree, selectionVal) => { if (!skipField(resolveInfo, selectionVal)) { if (selectionVal.kind === 'Field') { const val = selectionVal; const name = val.name && val.name.value; const isReserved = name && name !== '__id' && name.substr(0, 2) === '__'; if (!isReserved) { const alias = val.alias && val.alias.value ? val.alias.value : val.name.value; const field = getFieldFromAST(val, parentType); if (!field) { return tree; } const fieldGqlTypeOrUndefined = getNamedType(field.type); if (!fieldGqlTypeOrUndefined) { return tree; } const fieldGqlType = fieldGqlTypeOrUndefined; const args = getArgumentValues(field, val, variableValues) || {}; if (parentType.name && !tree[parentType.name][alias]) { const apongo = { ...(field.astNode.apongo || {}) }; const newTreeRoot = { name, alias, args, apongo, fieldsByTypeName: isCompositeType(fieldGqlType) ? { [fieldGqlType.name]: {} } : {}, }; tree[parentType.name][alias] = newTreeRoot; } const { selectionSet } = val; if (selectionSet != null && options.deep && isCompositeType(fieldGqlType)) { const newParentType = fieldGqlType; fieldTreeFromAST( selectionSet.selections, resolveInfo, tree[parentType.name][alias].fieldsByTypeName, options, newParentType, ); } } } else if (selectionVal.kind === 'FragmentSpread' && options.deep) { const val = selectionVal; const name = val.name && val.name.value; const fragment = fragments[name]; let fragmentType = parentType; if (fragment.typeCondition) { fragmentType = getType(resolveInfo, fragment.typeCondition); } if (fragmentType && isCompositeType(fragmentType)) { const newParentType = fragmentType; fieldTreeFromAST( fragment.selectionSet.selections, resolveInfo, tree, options, newParentType, ); } } else if (selectionVal.kind === 'InlineFragment' && options.deep) { const val = selectionVal; const fragment = val; let fragmentType = parentType; if (fragment.typeCondition) { fragmentType = getType(resolveInfo, fragment.typeCondition); } if (fragmentType && isCompositeType(fragmentType)) { const newParentType = fragmentType; fieldTreeFromAST( fragment.selectionSet.selections, resolveInfo, tree, options, newParentType, ); } } } return tree; }, initTree); } function parseResolveInfo(resolveInfo, options = {}) { const fieldNodes = resolveInfo.fieldNodes || resolveInfo.fieldASTs; const { parentType } = resolveInfo; if (!fieldNodes) { throw new Error('No fieldNodes provided!'); } if (options.keepRoot == null) { options.keepRoot = false; } if (options.deep == null) { options.deep = true; } const tree = fieldTreeFromAST( fieldNodes, resolveInfo, undefined, options, parentType, ); if (!options.keepRoot) { const typeKey = firstKey(tree); if (!typeKey) { return null; } const fields = tree[typeKey]; const fieldKey = firstKey(fields); if (!fieldKey) { return null; } return fields[fieldKey]; } return tree; } function simplifyParsedResolveInfoFragmentWithType(parsedResolveInfoFragment, type) { const { fieldsByTypeName } = parsedResolveInfoFragment; const fields = {}; const strippedType = getNamedType(type); if (isCompositeType(strippedType)) { Object.assign(fields, fieldsByTypeName[strippedType.name]); if (strippedType instanceof GraphQLObjectType) { const objectType = strippedType; // GraphQL ensures that the subfields cannot clash, so it's safe to simply overwrite them objectType.getInterfaces().forEach((anInterface) => { Object.assign(fields, fieldsByTypeName[anInterface.name]); }); } } return { ...parsedResolveInfoFragment, fields, }; } module.exports = { parseResolveInfo, simplifyParsedResolveInfoFragmentWithType, };