graphql-fields-list
Version:
Extracts and returns list of fields requested from graphql resolver info object
377 lines • 10.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.fieldsMap = fieldsMap;
exports.fieldsList = fieldsList;
exports.fieldsProjection = fieldsProjection;
/**
* Pre-compiled wildcard replacement regexp
*
* @type {RegExp}
*/
const RX_AST = /\*/g;
/**
* Retrieves a list of nodes from a given selection (either fragment or
* selection node)
*
* @param {FragmentDefinitionNode | FieldNode} selection
* @return {ReadonlyArray<FieldNode>}
* @access private
*/
function getNodes(selection) {
var _a;
return ((_a = selection === null || selection === void 0 ? void 0 : selection.selectionSet) === null || _a === void 0 ? void 0 : _a.selections) ||
[];
}
/**
* Checks if a given directive name and value valid to return a field
*
* @param {string} name
* @param {boolean} value
* @return boolean
* @access private
*/
function checkValue(name, value) {
return name === 'skip'
? !value
: name === 'include' ? value : true;
}
/**
* Checks if a given directive arg allows to return field
*
* @param {string} name - directive name
* @param {ArgumentNode} arg
* @param {VariablesValues} vars
* @return {boolean}
* @access private
*/
function verifyDirectiveArg(name, arg, vars) {
switch (arg.value.kind) {
case 'BooleanValue':
return checkValue(name, arg.value.value);
case 'Variable':
return checkValue(name, vars[arg.value.name.value]);
}
return true;
}
/**
* Checks if a given directive allows to return field
*
* @param {DirectiveNode} directive
* @param {VariablesValues} vars
* @return {boolean}
* @access private
*/
function verifyDirective(directive, vars) {
const directiveName = directive.name.value;
if (!~['include', 'skip'].indexOf(directiveName)) {
return true;
}
let args = directive.arguments;
if (!(args && args.length)) {
args = [];
}
for (const arg of args) {
if (!verifyDirectiveArg(directiveName, arg, vars)) {
return false;
}
}
return true;
}
/**
* Checks if a given list of directives allows to return field
*
* @param {ReadonlyArray<DirectiveNode>} directives
* @param {VariablesValues} vars
* @return {boolean}
* @access private
*/
function verifyDirectives(directives, vars) {
if (!directives || !directives.length) {
return true;
}
vars = vars || {};
for (const directive of directives) {
if (!verifyDirective(directive, vars)) {
return false;
}
}
return true;
}
/**
* Checks if a given node is inline fragment and process it,
* otherwise does nothing and returns false.
*
* @param {SelectionNode} node
* @param {MapResult | MapResultKey} root
* @param {*} skip
* @param {TraverseOptions} opts
*/
function verifyInlineFragment(node, root, opts, skip) {
if (node.kind === 'InlineFragment') {
const nodes = getNodes(node);
nodes.length && traverse(nodes, root, opts, skip);
return true;
}
return false;
}
/**
* Builds skip rules tree from a given skip option argument
*
* @param {string[]} skip - skip option arguments
* @return {SkipTree} - skip rules tree
*/
function skipTree(skip) {
const tree = {};
for (const pattern of skip) {
const props = pattern.split('.');
let propTree = tree;
for (let i = 0, s = props.length; i < s; i++) {
const prop = props[i];
const all = props[i + 1] === '*';
if (!propTree[prop]) {
propTree[prop] = i === s - 1 || all ? true : {};
all && i++;
}
propTree = propTree[prop];
}
}
return tree;
}
/**
*
* @param node
* @param skip
*/
function verifySkip(node, skip) {
if (!skip) {
return false;
}
// true['string'] is a valid operation is JS resulting in `undefined`
if (skip[node]) {
return skip[node];
}
// lookup through wildcard patterns
let nodeTree = false;
const patterns = Object.keys(skip).filter(pattern => ~pattern.indexOf('*'));
for (const pattern of patterns) {
const rx = new RegExp(pattern.replace(RX_AST, '.*'));
if (rx.test(node)) {
nodeTree = skip[pattern];
// istanbul ignore else
if (nodeTree === true) {
break;
}
}
}
return nodeTree;
}
/**
* Traverses recursively given nodes and fills-up given root tree with
* a requested field names
*
* @param {ReadonlyArray<FieldNode>} nodes
* @param {MapResult | MapResultKey} root
* @param {TraverseOptions} opts
* @param {SkipValue} skip
* @return {MapResult}
* @access private
*/
function traverse(nodes, root, opts, skip) {
for (const node of nodes) {
if (opts.withVars && !verifyDirectives(node.directives, opts.vars)) {
continue;
}
if (verifyInlineFragment(node, root, opts, skip)) {
continue;
}
const name = node.name.value;
if (opts.fragments[name]) {
traverse(getNodes(opts.fragments[name]), root, opts, skip);
continue;
}
const nodes = getNodes(node);
const nodeSkip = verifySkip(name, skip);
if (nodeSkip !== true) {
root[name] = root[name] || (nodes.length ? {} : false);
nodes.length && traverse(nodes, root[name], opts, nodeSkip);
}
}
return root;
}
/**
* Retrieves and returns a branch from a given tree by a given path
*
* @param {MapResult} tree
* @param {string} [path]
* @return {MapResult}
* @access private
*/
function getBranch(tree, path) {
if (!path) {
return tree;
}
for (const fieldName of path.split('.')) {
const branch = tree[fieldName];
if (!branch) {
return {};
}
tree = branch;
}
return tree;
}
/**
* Verifies if a given info object is valid. If valid - returns
* proper FieldNode object for given resolver node, otherwise returns null.
*
* @param {GraphQLResolveInfo} info
* @return {FieldNode | null}
* @access private
*/
function verifyInfo(info) {
if (!info) {
return null;
}
if (!info.fieldNodes && info.fieldASTs) {
info.fieldNodes = info.fieldASTs;
}
if (!info.fieldNodes) {
return null;
}
return verifyFieldNode(info);
}
/**
* Verifies if a proper fieldNode existing on given info object
*
* @param {GraphQLResolveInfo} info
* @return {FieldNode | null}
* @access private
*/
function verifyFieldNode(info) {
const fieldNode = info.fieldNodes.find((node) => node && node.name && node.name.value === info.fieldName);
if (!(fieldNode && fieldNode.selectionSet)) {
return null;
}
return fieldNode;
}
/**
* Parses input options and returns prepared options object
*
* @param {FieldsListOptions} options
* @return {FieldsListOptions}
* @access private
*/
function parseOptions(options) {
if (!options) {
return {};
}
if (options.withDirectives === undefined) {
options.withDirectives = true;
}
return options;
}
/**
* Extracts and returns requested fields tree.
* May return `false` if path option is pointing to leaf of tree
*
* @param {GraphQLResolveInfo} info
* @param {FieldsListOptions} options
* @return {MapResult}
* @access public
*/
function fieldsMap(info, options) {
const fieldNode = verifyInfo(info);
if (!fieldNode) {
return {};
}
const { path, withDirectives, skip } = parseOptions(options);
const tree = traverse(getNodes(fieldNode), {}, {
fragments: info.fragments,
vars: info.variableValues,
withVars: withDirectives,
}, skipTree(skip || []));
return getBranch(tree, path);
}
/**
* Extracts list of selected fields from a given GraphQL resolver info
* argument and returns them as an array of strings, using the given
* extraction options.
*
* @param {GraphQLResolveInfo} info - GraphQL resolver info object
* @param {FieldsListOptions} [options] - fields list extraction options
* @return {string[]} - array of field names
* @access public
*/
function fieldsList(info, options = {}) {
return Object.keys(fieldsMap(info, options)).map((field) => (options.transform || {})[field] || field);
}
/**
* Combines parent path with child name to fully-qualified dot-notation path
* of a child
*
* @param {string} parent
* @param {string} child
* @return {string}
* @access private
*/
function toDotNotation(parent, child) {
return `${parent ? parent + '.' : ''}${child}`;
}
/**
* Extracts projection of selected fields from a given GraphQL resolver info
* argument and returns flat fields projection object, where keys are object
* paths in dot-notation form.
*
* @param {GraphQLResolveInfo} info - GraphQL resolver info object
* @param {FieldsListOptions} options - fields list extraction options
* @return {FieldsProjection} - fields projection object
* @access public
*/
function fieldsProjection(info, options) {
const tree = fieldsMap(info, options);
const stack = [];
const map = {};
const transform = (options || {}).transform || {};
stack.push({ node: '', tree });
while (stack.length) {
for (const j of Object.keys(stack[0].tree)) {
if (stack[0].tree[j]) {
const nodeDottedName = toDotNotation(stack[0].node, j);
stack.push({
node: nodeDottedName,
tree: stack[0].tree[j],
});
if (options === null || options === void 0 ? void 0 : options.keepParentField) {
map[nodeDottedName] = 1;
}
continue;
}
let dotName = toDotNotation(stack[0].node, j);
if (transform[dotName]) {
dotName = transform[dotName];
}
map[dotName] = 1;
}
stack.shift();
}
return map;
}
// istanbul ignore next
if (process.env['IS_UNIT_TEST']) {
// noinspection JSUnusedGlobalSymbols
Object.assign(module.exports, {
getNodes,
traverse,
getBranch,
verifyDirectives,
verifyDirective,
verifyDirectiveArg,
checkValue,
verifyInfo,
verifyFieldNode,
verifyInlineFragment,
verifySkip,
parseOptions,
toDotNotation,
});
}
//# sourceMappingURL=index.js.map