graphql-fields-list
Advanced tools
Comparing version 2.0.0 to 2.1.1
@@ -41,5 +41,37 @@ /*! | ||
export interface FieldsListOptions { | ||
/** | ||
* Path to a tree branch which should be mapped during fields extraction | ||
* @type {string} | ||
*/ | ||
path?: string; | ||
/** | ||
* Transformation rules which should be used to re-name field names | ||
* @type {FieldNamesMap} | ||
*/ | ||
transform?: FieldNamesMap; | ||
/** | ||
* Flag which turns on/off GraphQL directives checks on a fields | ||
* and take them into account during fields analysis | ||
* @type {boolean} | ||
*/ | ||
withDirectives?: boolean; | ||
/** | ||
* Fields skip rule patterns. Usually used to ignore part of request field | ||
* subtree. For example if query looks like: | ||
* profiles { | ||
* id | ||
* users { | ||
* name | ||
* password | ||
* } | ||
* } | ||
* and you doo n not care about users, it can be done like: | ||
* fieldsList(info, { skip: ['users'] }); // or | ||
* fieldsProjection(info, { skip: ['users.*'] }); // more obvious notation | ||
* | ||
* If you want to skip only exact fields, it can be done as: | ||
* fieldsMap(info, { skip: ['users.email', 'users.password'] }) | ||
*/ | ||
skip?: string[]; | ||
} | ||
@@ -46,0 +78,0 @@ /** |
79
index.js
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
/** | ||
* Pre-compiled wildcard replacement regexp | ||
* | ||
* @type {RegExp} | ||
*/ | ||
const RX_AST = /\*/g; | ||
/** | ||
* Retrieves a list of nodes from a given selection (either fragment or | ||
@@ -96,8 +102,9 @@ * selection node) | ||
* @param {*} root | ||
* @param {*} skip | ||
* @param {TraverseOptions} opts | ||
*/ | ||
function verifyInlineFragment(node, root, opts) { | ||
function verifyInlineFragment(node, root, opts, skip) { | ||
if (node.kind === 'InlineFragment') { | ||
const nodes = getNodes(node); | ||
nodes.length && traverse(nodes, root, opts); | ||
nodes.length && traverse(nodes, root, opts, skip); | ||
return true; | ||
@@ -108,2 +115,51 @@ } | ||
/** | ||
* Builds skip rules tree from a given skip option argument | ||
* | ||
* @param {string[]} skip - skip option arguments | ||
* @return {any} - 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; | ||
} | ||
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]; | ||
if (nodeTree === true) { | ||
break; | ||
} | ||
} | ||
} | ||
return nodeTree; | ||
} | ||
/** | ||
* Traverses recursively given nodes and fills-up given root tree with | ||
@@ -115,6 +171,7 @@ * a requested field names | ||
* @param {TraverseOptions} opts | ||
* @param {*} skip | ||
* @return {*} | ||
* @access private | ||
*/ | ||
function traverse(nodes, root, opts) { | ||
function traverse(nodes, root, opts, skip) { | ||
for (const node of nodes) { | ||
@@ -124,3 +181,3 @@ if (opts.withVars && !verifyDirectives(node.directives, opts.vars)) { | ||
} | ||
if (verifyInlineFragment(node, root, opts)) { | ||
if (verifyInlineFragment(node, root, opts, skip)) { | ||
continue; | ||
@@ -130,8 +187,11 @@ } | ||
if (opts.fragments[name]) { | ||
traverse(getNodes(opts.fragments[name]), root, opts); | ||
traverse(getNodes(opts.fragments[name]), root, opts, skip); | ||
continue; | ||
} | ||
const nodes = getNodes(node); | ||
root[name] = root[name] || (nodes.length ? {} : false); | ||
nodes.length && traverse(nodes, root[name], opts); | ||
const nodeSkip = verifySkip(name, skip); | ||
if (nodeSkip !== true) { | ||
root[name] = root[name] || (nodes.length ? {} : false); | ||
nodes.length && traverse(nodes, root[name], opts, nodeSkip); | ||
} | ||
} | ||
@@ -222,3 +282,3 @@ return root; | ||
} | ||
const { path, withDirectives } = parseOptions(options); | ||
const { path, withDirectives, skip } = parseOptions(options); | ||
const tree = traverse(getNodes(fieldNode), {}, { | ||
@@ -228,3 +288,3 @@ fragments: info.fragments, | ||
withVars: withDirectives, | ||
}); | ||
}, skipTree(skip || [])); | ||
return getBranch(tree, path); | ||
@@ -310,2 +370,3 @@ } | ||
verifyInlineFragment, | ||
verifySkip, | ||
parseOptions, | ||
@@ -312,0 +373,0 @@ toDotNotation, |
{ | ||
"name": "graphql-fields-list", | ||
"version": "2.0.0", | ||
"version": "2.1.1", | ||
"description": "Extracts and returns list of fields requested from graphql resolver info object", | ||
@@ -42,21 +42,21 @@ "scripts": { | ||
"devDependencies": { | ||
"@types/chai": "^4.1.6", | ||
"@types/graphql": "^14.0.3", | ||
"@types/graphql-relay": "^0.4.8", | ||
"@types/chai": "^4.1.7", | ||
"@types/graphql": "^14.0.5", | ||
"@types/graphql-relay": "^0.4.9", | ||
"@types/mocha": "^5.2.5", | ||
"@types/node": "^10.12.0", | ||
"@types/sinon": "^5.0.5", | ||
"@types/node": "^11.9.0", | ||
"@types/sinon": "^7.0.5", | ||
"@types/uuid": "^3.4.4", | ||
"chai": "^4.2.0", | ||
"coveralls": "^3.0.2", | ||
"graphql": "^14.0.2", | ||
"graphql-relay": "^0.5.5", | ||
"graphql": "^14.1.1", | ||
"graphql-relay": "^0.6.0", | ||
"mocha": "^5.2.0", | ||
"npm-scripts-help": "^0.8.0", | ||
"nyc": "^13.1.0", | ||
"nyc": "^13.2.0", | ||
"opn": "^5.4.0", | ||
"sinon": "^7.0.0", | ||
"ts-node": "^7.0.1", | ||
"typedoc": "^0.13.0", | ||
"typescript": "^3.1.3", | ||
"sinon": "^7.2.3", | ||
"ts-node": "^8.0.2", | ||
"typedoc": "^0.14.2", | ||
"typescript": "^3.3.3", | ||
"uuid": "^3.3.2" | ||
@@ -63,0 +63,0 @@ }, |
@@ -286,4 +286,58 @@ # graphql-fields-list | ||
**Since version 2.1.0** | ||
It supports `skip` option to filter output of `fieldsList()`, `fieldsMap()` and | ||
`fieldsProjection()` functions. | ||
[See motivation](https://github.com/Mikhus/graphql-fields-list/issues/4) | ||
Skip option accepts an array of field projections to skip. It allows usage | ||
of wildcard symbol `*` within field names. Please, note, that skip occurs | ||
before transformations, so it should reflect original field names, | ||
transformations would be applied after skip is done. | ||
Typical usage as: | ||
```javascript | ||
const map = fieldsMap(info, { skip: [ | ||
'users.pageInfo.*', | ||
'users.edges.node.email', | ||
'users.edges.node.address', | ||
'users.edges.node.*Name', | ||
]}); | ||
/* | ||
RESULT: | ||
map = { | ||
users: { | ||
edges: { | ||
node: { | ||
id: false, | ||
phoneNumber: false, | ||
}, | ||
}, | ||
}, | ||
} | ||
*/ | ||
const projection = fieldsProjection(info, { | ||
skip: [ | ||
'users.pageInfo.*', | ||
'users.edges.node.email', | ||
'users.edges.node.address', | ||
'users.edges.node.*Name', | ||
], | ||
transform: { | ||
'users.edges.node.id': 'users.edges.node._id', | ||
}, | ||
}); | ||
/* | ||
RESULT: | ||
projection = { | ||
'users.edges.node._id': 1, | ||
'users.edges.node.phoneNumber': 1, | ||
}; | ||
*/ | ||
``` | ||
## License | ||
[ISC Licence](LICENSE) | ||
[ISC Licence](LICENSE) |
26471
6
490
343