graphql-anywhere
Version:
Run GraphQL queries with no schema and just one resolver
246 lines (206 loc) • 6.03 kB
text/typescript
import {
DocumentNode,
SelectionSetNode,
FieldNode,
FragmentDefinitionNode,
InlineFragmentNode,
} from 'graphql';
import {
getMainDefinition,
getFragmentDefinitions,
createFragmentMap,
FragmentMap,
DirectiveInfo,
shouldInclude,
getDirectiveInfoFromField,
isField,
isInlineFragment,
resultKeyNameFromField,
argumentsObjectFromField,
} from 'apollo-utilities';
export type Resolver = (
fieldName: string,
rootValue: any,
args: any,
context: any,
info: ExecInfo,
) => any;
export type VariableMap = { [name: string]: any };
export type ResultMapper = (
values: { [fieldName: string]: any },
rootValue: any,
) => any;
export type FragmentMatcher = (
rootValue: any,
typeCondition: string,
context: any,
) => boolean;
export type ExecContext = {
fragmentMap: FragmentMap;
contextValue: any;
variableValues: VariableMap;
resultMapper: ResultMapper;
resolver: Resolver;
fragmentMatcher: FragmentMatcher;
};
export type ExecInfo = {
isLeaf: boolean;
resultKey: string;
directives: DirectiveInfo;
field: FieldNode;
};
export type ExecOptions = {
resultMapper?: ResultMapper;
fragmentMatcher?: FragmentMatcher;
};
/* Based on graphql function from graphql-js:
*
* graphql(
* schema: GraphQLSchema,
* requestString: string,
* rootValue?: ?any,
* contextValue?: ?any,
* variableValues?: ?{[key: string]: any},
* operationName?: ?string
* ): Promise<GraphQLResult>
*
* The default export as of graphql-anywhere is sync as of 4.0,
* but below is an exported alternative that is async.
* In the 5.0 version, this will be the only export again
* and it will be async
*/
export function graphql(
resolver: Resolver,
document: DocumentNode,
rootValue?: any,
contextValue?: any,
variableValues: VariableMap = {},
execOptions: ExecOptions = {},
) {
const mainDefinition = getMainDefinition(document);
const fragments = getFragmentDefinitions(document);
const fragmentMap = createFragmentMap(fragments);
const resultMapper = execOptions.resultMapper;
// Default matcher always matches all fragments
const fragmentMatcher = execOptions.fragmentMatcher || (() => true);
const execContext: ExecContext = {
fragmentMap,
contextValue,
variableValues,
resultMapper,
resolver,
fragmentMatcher,
};
return executeSelectionSet(
mainDefinition.selectionSet as SelectionSetNode,
rootValue,
execContext,
);
}
function executeSelectionSet(
selectionSet: SelectionSetNode,
rootValue: any,
execContext: ExecContext,
) {
const { fragmentMap, contextValue, variableValues: variables } = execContext;
const result = {};
selectionSet.selections.forEach(selection => {
if (variables && !shouldInclude(selection, variables)) {
// Skip selection sets which we're able to determine should not be run
return;
}
if (isField(selection)) {
const fieldResult = executeField(selection, rootValue, execContext);
const resultFieldKey = resultKeyNameFromField(selection);
if (fieldResult !== undefined) {
if (result[resultFieldKey] === undefined) {
result[resultFieldKey] = fieldResult;
} else {
merge(result[resultFieldKey], fieldResult);
}
}
} else {
let fragment: InlineFragmentNode | FragmentDefinitionNode;
if (isInlineFragment(selection)) {
fragment = selection;
} else {
// This is a named fragment
fragment = fragmentMap[selection.name.value] as FragmentDefinitionNode;
if (!fragment) {
throw new Error(`No fragment named ${selection.name.value}`);
}
}
const typeCondition = fragment.typeCondition.name.value;
if (execContext.fragmentMatcher(rootValue, typeCondition, contextValue)) {
const fragmentResult = executeSelectionSet(
fragment.selectionSet,
rootValue,
execContext,
);
merge(result, fragmentResult);
}
}
});
if (execContext.resultMapper) {
return execContext.resultMapper(result, rootValue);
}
return result;
}
function executeField(
field: FieldNode,
rootValue: any,
execContext: ExecContext,
): any {
const { variableValues: variables, contextValue, resolver } = execContext;
const fieldName = field.name.value;
const args = argumentsObjectFromField(field, variables);
const info: ExecInfo = {
isLeaf: !field.selectionSet,
resultKey: resultKeyNameFromField(field),
directives: getDirectiveInfoFromField(field, variables),
field,
};
const result = resolver(fieldName, rootValue, args, contextValue, info);
// Handle all scalar types here
if (!field.selectionSet) {
return result;
}
// From here down, the field has a selection set, which means it's trying to
// query a GraphQLObjectType
if (result == null) {
// Basically any field in a GraphQL response can be null, or missing
return result;
}
if (Array.isArray(result)) {
return executeSubSelectedArray(field, result, execContext);
}
// Returned value is an object, and the query has a sub-selection. Recurse.
return executeSelectionSet(field.selectionSet, result, execContext);
}
function executeSubSelectedArray(field, result, execContext) {
return result.map(item => {
// null value in array
if (item === null) {
return null;
}
// This is a nested array, recurse
if (Array.isArray(item)) {
return executeSubSelectedArray(field, item, execContext);
}
// This is an object, run the selection set on it
return executeSelectionSet(field.selectionSet, item, execContext);
});
}
const hasOwn = Object.prototype.hasOwnProperty;
export function merge(dest, src) {
if (src !== null && typeof src === 'object') {
Object.keys(src).forEach(key => {
const srcVal = src[key];
if (!hasOwn.call(dest, key)) {
dest[key] = srcVal;
} else {
merge(dest[key], srcVal);
}
});
}
}