graphql-advanced-projection
Advanced tools
Comparing version 1.0.0-beta.1 to 1.0.0
@@ -11,2 +11,5 @@ # Terms | ||
# Exported functions | ||
- `gqlProjection (default): (config) => { project, resolvers }` | ||
- Shorthand for calling the the following functions. | ||
- `prepareConfig: (config) => config` | ||
- `genProjection: (config) => (info) => result` | ||
@@ -16,3 +19,2 @@ - `info` is the 4th argument of a resolver function. | ||
- `result` is an object with Path as keys and `1` or `0` as value. | ||
- `result._id` always exists. | ||
- `genResolvers: (config) => resolvers` | ||
@@ -53,3 +55,6 @@ - `resolvers` is of valid GraphQL resolver format. SHOULD be used with [`graphql-tools/makeExecutableSchema`](https://github.com/apollographql/graphql-tools). | ||
- `null` can match zero, one, or more path items. | ||
- A string can match one path item (exactly match its key) and following numeric keys. | ||
- `''` can match one path item (not numeric) and following numeric keys. | ||
- `'?'` can match nothing or (one path item (not numeric) and following numeric keys). | ||
- A string with suffix `'?'` can match nothing or (one path item (exactly match its key) and following numeric keys). | ||
- Otherwise, a string can match one path item (exactly match its key) and following numeric keys. | ||
@@ -56,0 +61,0 @@ # Type config object |
@@ -6,9 +6,5 @@ const _ = require('lodash'); | ||
const { User, Item } = require('./models'); | ||
const { | ||
prepareConfig, | ||
genProjection, | ||
genResolvers, | ||
} = require('../../'); | ||
const gqlProjection = require('../../'); | ||
const config = prepareConfig({ | ||
const { project, resolvers } = gqlProjection({ | ||
User: { | ||
@@ -34,4 +30,2 @@ proj: { | ||
}); | ||
const project = genProjection(config); | ||
const resolvers = genResolvers(config); | ||
@@ -38,0 +32,0 @@ module.exports = makeExecutableSchema({ |
@@ -6,9 +6,5 @@ const _ = require('lodash'); | ||
const { User } = require('./models'); | ||
const { | ||
prepareConfig, | ||
genProjection, | ||
genResolvers, | ||
} = require('../../'); | ||
const gqlProjection = require('../../'); | ||
const config = prepareConfig({ | ||
const { project, resolvers } = gqlProjection({ | ||
User: { | ||
@@ -33,4 +29,2 @@ typeProj: 'type', | ||
}); | ||
const project = genProjection(config); | ||
const resolvers = genResolvers(config); | ||
@@ -37,0 +31,0 @@ module.exports = makeExecutableSchema({ |
@@ -6,9 +6,5 @@ const _ = require('lodash'); | ||
const { User } = require('./models'); | ||
const { | ||
prepareConfig, | ||
genProjection, | ||
genResolvers, | ||
} = require('../../'); | ||
const gqlProjection = require('../../'); | ||
const config = prepareConfig({ | ||
const { project, resolvers } = gqlProjection({ | ||
User: { | ||
@@ -25,4 +21,2 @@ proj: { | ||
}); | ||
const project = genProjection(config); | ||
const resolvers = genResolvers(config); | ||
@@ -29,0 +23,0 @@ module.exports = makeExecutableSchema({ |
@@ -6,9 +6,5 @@ const _ = require('lodash'); | ||
const { User } = require('./models'); | ||
const { | ||
prepareConfig, | ||
genProjection, | ||
genResolvers, | ||
} = require('../../'); | ||
const gqlProjection = require('../../'); | ||
const config = prepareConfig({ | ||
const { project, resolvers } = gqlProjection({ | ||
User: { | ||
@@ -26,4 +22,2 @@ proj: { | ||
}); | ||
const project = genProjection(config); | ||
const resolvers = genResolvers(config); | ||
@@ -30,0 +24,0 @@ module.exports = makeExecutableSchema({ |
@@ -6,9 +6,5 @@ const _ = require('lodash'); | ||
const { User, Item } = require('./models'); | ||
const { | ||
prepareConfig, | ||
genProjection, | ||
genResolvers, | ||
} = require('../../'); | ||
const gqlProjection = require('../../'); | ||
const config = prepareConfig({ | ||
const { project, resolvers } = gqlProjection({ | ||
User: { | ||
@@ -26,4 +22,2 @@ proj: { | ||
}); | ||
const project = genProjection(config); | ||
const resolvers = genResolvers(config); | ||
@@ -30,0 +24,0 @@ module.exports = makeExecutableSchema({ |
@@ -6,9 +6,5 @@ const _ = require('lodash'); | ||
const { User } = require('./models'); | ||
const { | ||
prepareConfig, | ||
genProjection, | ||
genResolvers, | ||
} = require('../../'); | ||
const gqlProjection = require('../../'); | ||
const config = prepareConfig({ | ||
const { project, resolvers } = gqlProjection({ | ||
User: { | ||
@@ -22,4 +18,2 @@ proj: { | ||
}); | ||
const project = genProjection(config); | ||
const resolvers = genResolvers(config); | ||
@@ -26,0 +20,0 @@ module.exports = makeExecutableSchema({ |
18
index.js
@@ -5,7 +5,15 @@ const { prepareConfig } = require('./src/prepareConfig'); | ||
module.exports = { | ||
default: prepareConfig, | ||
prepareConfig, | ||
genProjection, | ||
genResolvers, | ||
const gqlProjection = (config) => { | ||
const ncfgs = prepareConfig(config); | ||
return { | ||
project: genProjection(ncfgs), | ||
resolvers: genResolvers(ncfgs), | ||
}; | ||
}; | ||
module.exports = gqlProjection; | ||
module.exports.default = gqlProjection; | ||
module.exports.gqlProjection = gqlProjection; | ||
module.exports.prepareConfig = prepareConfig; | ||
module.exports.genProjection = genProjection; | ||
module.exports.genResolvers = genResolvers; |
{ | ||
"name": "graphql-advanced-projection", | ||
"version": "1.0.0-beta.1", | ||
"version": "1.0.0", | ||
"description": "Fully customizable Mongoose/MongoDB projection generator.", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -36,2 +36,3 @@ # graphql-advanced-projection | ||
### Setup `mongoose` | ||
```js | ||
@@ -46,2 +47,3 @@ const UserSchema = new mongoose.Schema({ | ||
### Setup `graphql` | ||
```graphql | ||
@@ -59,11 +61,5 @@ type Query { | ||
### Setup `graphql-advanced-projection` | ||
```js | ||
const { | ||
prepareConfig, | ||
genProjection, | ||
genResolvers, | ||
} = require('graphql-advanced-projection'); | ||
// Projection config | ||
const config = prepareConfig({ | ||
const { project, resolvers } = gqlProjection({ | ||
User: { | ||
@@ -77,4 +73,2 @@ proj: { | ||
}); | ||
const project = genProjection(config); | ||
const resolvers = genResolvers(config); | ||
``` | ||
@@ -85,5 +79,2 @@ | ||
```js | ||
const _ = require('lodash'); | ||
const { makeExecutableSchema } = require('graphql-tools'); | ||
module.exports = makeExecutableSchema({ | ||
@@ -90,0 +81,0 @@ typeDefs, |
@@ -62,5 +62,5 @@ const _ = require('lodash/fp'); | ||
if (_.isArray(config)) { | ||
return config.map(([m, t]) => [norm(m), t]); | ||
return config.map(([m, t]) => [norm(m), _.cloneDeep(t)]); | ||
} | ||
return [[[[null]], config]]; | ||
return _.cloneDeep(config); | ||
} | ||
@@ -72,13 +72,18 @@ | ||
_.mapValues( | ||
_.compose( | ||
_.map, | ||
_.update('[1].proj'), | ||
_.mapValues, | ||
)(prepareProjectionConfig), | ||
(v) => (_.isArray(v) | ||
? _.compose( | ||
_.map, | ||
_.update('[1].proj'), | ||
_.mapValues, | ||
) | ||
: _.compose( | ||
_.update('proj'), | ||
_.mapValues, | ||
) | ||
)(prepareProjectionConfig)(v), | ||
), | ||
_.cloneDeep, | ||
_.mapValues(prepareSchemaConfig), | ||
_.pickBy((v, k) => /^[A-Z]/.test(k)), | ||
)(configs); | ||
logger.info('Total config', ncfgs); | ||
logger.info(`Total config: ${JSON.stringify(ncfgs, null, 2)}`); | ||
return { | ||
@@ -85,0 +90,0 @@ root, |
@@ -5,23 +5,20 @@ const _ = require('lodash/fp'); | ||
module.exports.genProjection = ({ root, pick }) => { | ||
logger.trace('genProjection'); | ||
return (info) => { | ||
module.exports.genProjection = ({ root, pick }) => (info) => { | ||
try { | ||
const context = info.fieldNodes[0]; | ||
logger.trace('returnType', info.returnType); | ||
const type = stripType(info.returnType); | ||
logger.trace('Stripped returnType', type); | ||
try { | ||
return _.reduce(_.assign, {})([root, makeProjection( | ||
{ pick, info }, | ||
context, | ||
'', | ||
type, | ||
)]); | ||
} catch (e) { | ||
/* istanbul ignore next */ | ||
logger.error('Projecting', e); | ||
/* istanbul ignore next */ | ||
return undefined; | ||
} | ||
}; | ||
const result = _.reduce(_.assign, {})([root, makeProjection( | ||
{ pick, info }, | ||
context, | ||
'', | ||
type, | ||
)]); | ||
logger.debug('Project result', result); | ||
return result; | ||
} catch (e) { | ||
/* istanbul ignore next */ | ||
logger.error('Projecting', e); | ||
/* istanbul ignore next */ | ||
return undefined; | ||
} | ||
}; |
@@ -5,19 +5,32 @@ const _ = require('lodash/fp'); | ||
function makeResolver(configs, pick) { | ||
logger.trace('makeResolver', configs); | ||
const res = _.compose( | ||
_.fromPairs, | ||
_.map((k) => [k, (parent, args, context, info) => { | ||
const cfg = pick(info); | ||
const select = _.get(['proj', k, 'select'])(cfg); | ||
return _.get(select || k)(parent); | ||
}]), | ||
_.uniq, | ||
_.flatMap(_.keys), | ||
_.map('[1].proj'), | ||
)(configs); | ||
logger.trace('Generated resolver', _.keys(res)); | ||
let fn; | ||
if (!_.isArray(configs)) { | ||
fn = _.compose( | ||
_.mapValues(_.get), | ||
_.pickBy(_.identity), | ||
_.mapValues(_.get('select')), | ||
_.get('proj'), | ||
); | ||
} else { | ||
fn = _.compose( | ||
_.fromPairs, | ||
_.map((k) => [k, (parent, args, context, info) => { | ||
const cfg = pick(info); | ||
const select = _.get(['proj', k, 'select'])(cfg); | ||
return _.get(select || k)(parent); | ||
}]), | ||
_.uniq, | ||
_.flatMap(_.keys), | ||
_.map('[1].proj'), | ||
); | ||
} | ||
const res = fn(configs); | ||
return res; | ||
} | ||
const genResolvers = ({ config, pick }) => _.mergeWith(makeResolver)(config, pick); | ||
const genResolvers = ({ config, pick }) => { | ||
const result = _.mergeWith(makeResolver)(config, pick); | ||
logger.info('Resolvers', result); | ||
return result; | ||
}; | ||
@@ -24,0 +37,0 @@ module.exports = { |
@@ -1,2 +0,3 @@ | ||
const _ = require('lodash'); | ||
const _ = require('lodash/fp'); | ||
const logger = require('../logger'); | ||
@@ -22,2 +23,3 @@ function unwindPath(path) { | ||
* [ANY]: [Number], | ||
* [NOTNUMBER]: [Number], | ||
* [NUMBER]: [Number], | ||
@@ -33,6 +35,7 @@ * key: [Number], | ||
const ANY = Symbol('any'); | ||
const NOTNUMBER = Symbol('not-number'); | ||
const NUMBER = Symbol('number'); | ||
const ACCEPT = Symbol('accept'); | ||
const extend = (NFA, states) => { | ||
const extend = (NFA) => (states) => { | ||
const extended = new Set(); | ||
@@ -48,31 +51,30 @@ const queue = [...states]; | ||
} | ||
return extended; | ||
return [...extended]; | ||
}; | ||
const run = (NFA, input) => { | ||
let states = extend(NFA, new Set([0])); | ||
for (let id = 0; id < input.length; id += 1) { | ||
const char = input[id]; | ||
const next = new Set(); | ||
states.forEach((state) => { | ||
const cfg = NFA[state]; | ||
if (cfg[ANY]) { | ||
next.add(...cfg[ANY]); | ||
} | ||
if (_.isNumber(char) && cfg[NUMBER]) { | ||
next.add(...cfg[NUMBER]); | ||
} | ||
if (cfg[char]) { | ||
next.add(...cfg[char]); | ||
} | ||
}); | ||
if (!next.size) { | ||
return false; | ||
} | ||
states = extend(NFA, next); | ||
} | ||
let accept = false; | ||
states.forEach((state) => { accept = accept || NFA[state][ACCEPT]; }); | ||
return !!accept; | ||
}; | ||
const run = (NFA) => _.compose( | ||
_.some((state) => NFA[state][ACCEPT]), | ||
_.compose( | ||
_.reduce((states, char) => _.compose( | ||
extend(NFA), | ||
_.reduce((next, state) => { | ||
const cfg = NFA[state]; | ||
const mer = (k) => { | ||
if (cfg[k]) { | ||
next.add(...cfg[k]); | ||
} | ||
}; | ||
mer(ANY); | ||
if (_.isNumber(char)) { | ||
mer(NUMBER); | ||
} else { | ||
mer(NOTNUMBER); | ||
} | ||
mer(char); | ||
return next; | ||
})(new Set()), | ||
)(states)), | ||
extend(NFA), | ||
)(new Set([0])), | ||
); | ||
@@ -100,25 +102,53 @@ const append = (obj, key, val) => { | ||
const appendExactOrNothing = (NFA, str) => { | ||
const len = NFA.length; | ||
append(NFA[len - 1], str, len); | ||
append(NFA[len - 1], EPSILON, len + 1); | ||
NFA.push({ [NUMBER]: [len], [EPSILON]: [len + 1] }); | ||
NFA.push({}); | ||
}; | ||
const matchSchema = (cfg) => { | ||
const NFA = [{}]; | ||
cfg.forEach((c) => { | ||
const NFA = _.reduce((n, c) => { | ||
if (c === null) { | ||
appendAny(NFA); | ||
appendAny(n); | ||
} else if (c === '') { | ||
appendExact(n, NOTNUMBER); | ||
} else if (c === '?') { | ||
appendExactOrNothing(n, NOTNUMBER); | ||
} else if (c.endsWith('?')) { | ||
appendExactOrNothing(n, c.substr(0, c.length - 1)); | ||
} else { | ||
appendExact(NFA, c); | ||
appendExact(n, c); | ||
} | ||
}); | ||
return n; | ||
})([{}])(cfg); | ||
NFA[NFA.length - 1][ACCEPT] = true; | ||
return (path) => run(NFA, path); | ||
logger.info('NFA', NFA); | ||
return run(NFA); | ||
}; | ||
const matchSchemas = (cfgs) => { | ||
const ms = cfgs.map(matchSchema); | ||
return (path) => ms.some((m) => m(path)); | ||
}; | ||
const matchSchemas = _.compose( | ||
_.overSome, | ||
_.map(matchSchema), | ||
); | ||
const pickType = (config) => { | ||
const matchers = config.map(([cfgs]) => matchSchemas(cfgs)); | ||
if (!_.isArray(config)) { | ||
return _.constant(config); | ||
} | ||
const matchers = _.compose( | ||
_.over, | ||
_.map(_.compose( | ||
matchSchemas, | ||
_.get('0'), | ||
)), | ||
)(config); | ||
return (info) => { | ||
const path = unwindPath(info.path); | ||
const id = matchers.findIndex((m) => m(path)); | ||
const id = _.compose( | ||
_.findIndex(_.identity), | ||
matchers, | ||
unwindPath, | ||
_.get('path'), | ||
)(info); | ||
if (id === -1) { | ||
@@ -125,0 +155,0 @@ return {}; |
@@ -91,3 +91,3 @@ const { | ||
it('should accept object', () => { | ||
expect(prepareSchemaConfig({ obj: true })).toEqual([[[[null]], { obj: true }]]); | ||
expect(prepareSchemaConfig({ obj: true })).toEqual({ obj: true }); | ||
}); | ||
@@ -157,13 +157,11 @@ | ||
expect(config).toEqual({ | ||
Obj: [ | ||
[[[null]], { | ||
prefix: 'x', | ||
proj: { | ||
a: { | ||
query: 'b', | ||
select: 'b', | ||
}, | ||
Obj: { | ||
prefix: 'x', | ||
proj: { | ||
a: { | ||
query: 'b', | ||
select: 'b', | ||
}, | ||
}], | ||
], | ||
}, | ||
}, | ||
}); | ||
@@ -170,0 +168,0 @@ expect(pick.Obj({})).toEqual({ |
@@ -77,2 +77,23 @@ const _ = require('lodash'); | ||
}); | ||
it('should accept complex 3', async (done) => { | ||
const result = await run({ | ||
Evil: [ | ||
['obj', { | ||
proj: { | ||
field: 'x', | ||
}, | ||
}], | ||
['evil', { | ||
proj: { | ||
field: null, | ||
}, | ||
}], | ||
], | ||
}, '{ evil { field } }', { | ||
evil: { field: 'xxx' }, | ||
}); | ||
expect(result).toEqual({ data: { evil: { field: 'xxx' } } }); | ||
done(); | ||
}); | ||
}); |
@@ -45,2 +45,4 @@ const { prepareSchemaConfig } = require('../src/prepareConfig'); | ||
expect(func('a', 0, 'a', 1)).toEqual(false); | ||
expect(func(2)).toEqual(false); | ||
expect(func(2, 'a')).toEqual(false); | ||
}); | ||
@@ -57,2 +59,4 @@ | ||
expect(func('a', 0, 'a', 1)).toEqual(false); | ||
expect(func(2)).toEqual(false); | ||
expect(func(2, 'a')).toEqual(false); | ||
}); | ||
@@ -69,2 +73,4 @@ | ||
expect(func('a', 0, 'a', 1)).toEqual(true); | ||
expect(func(2)).toEqual(true); | ||
expect(func(2, 'a')).toEqual(true); | ||
}); | ||
@@ -81,2 +87,4 @@ | ||
expect(func('a', 0, 'a', 1)).toEqual(false); | ||
expect(func(2)).toEqual(false); | ||
expect(func(2, 'a')).toEqual(false); | ||
}); | ||
@@ -93,2 +101,4 @@ | ||
expect(func('a', 0, 'a', 1)).toEqual(true); | ||
expect(func(2)).toEqual(false); | ||
expect(func(2, 'a')).toEqual(false); | ||
}); | ||
@@ -105,2 +115,4 @@ | ||
expect(func('a', 0, 'a', 1)).toEqual(true); | ||
expect(func(2)).toEqual(false); | ||
expect(func(2, 'a')).toEqual(false); | ||
}); | ||
@@ -117,2 +129,4 @@ | ||
expect(func('a', 0, 'a', 1)).toEqual(false); | ||
expect(func(2)).toEqual(false); | ||
expect(func(2, 'a')).toEqual(false); | ||
}); | ||
@@ -129,2 +143,4 @@ | ||
expect(func('a', 0, 'a', 1)).toEqual(false); | ||
expect(func(2)).toEqual(false); | ||
expect(func(2, 'a')).toEqual(false); | ||
}); | ||
@@ -141,2 +157,4 @@ | ||
expect(func('a', 0, 'a', 1)).toEqual(false); | ||
expect(func(2)).toEqual(false); | ||
expect(func(2, 'a')).toEqual(false); | ||
}); | ||
@@ -153,2 +171,4 @@ | ||
expect(func('a', 0, 'a', 1)).toEqual(true); | ||
expect(func(2)).toEqual(true); | ||
expect(func(2, 'a')).toEqual(true); | ||
}); | ||
@@ -165,6 +185,65 @@ | ||
expect(func('a', 0, 'a', 1)).toEqual(false); | ||
expect(func(2)).toEqual(false); | ||
expect(func(2, 'a')).toEqual(false); | ||
}); | ||
it('should match empty', () => { | ||
const func = (...args) => matchSchema([''])(args); | ||
expect(func()).toEqual(false); | ||
expect(func('a')).toEqual(true); | ||
expect(func('a', 'b')).toEqual(false); | ||
expect(func('a', 0, 1)).toEqual(true); | ||
expect(func('a', 'b', 0, 1)).toEqual(false); | ||
expect(func('a', 'a')).toEqual(false); | ||
expect(func('a', 0, 'a', 1)).toEqual(false); | ||
expect(func(2)).toEqual(false); | ||
expect(func(2, 'a')).toEqual(false); | ||
}); | ||
it('should match empty empty', () => { | ||
const func = (...args) => matchSchema(['', ''])(args); | ||
expect(func()).toEqual(false); | ||
expect(func('a')).toEqual(false); | ||
expect(func('a', 'b')).toEqual(true); | ||
expect(func('a', 0, 1)).toEqual(false); | ||
expect(func('a', 'b', 0, 1)).toEqual(true); | ||
expect(func('a', 'a')).toEqual(true); | ||
expect(func('a', 0, 'a', 1)).toEqual(true); | ||
expect(func(2)).toEqual(false); | ||
expect(func(2, 'a')).toEqual(false); | ||
}); | ||
it('should match opt empty', () => { | ||
const func = (...args) => matchSchema(['?'])(args); | ||
expect(func()).toEqual(true); | ||
expect(func('a')).toEqual(true); | ||
expect(func('a', 'b')).toEqual(false); | ||
expect(func('a', 0, 1)).toEqual(true); | ||
expect(func('a', 'b', 0, 1)).toEqual(false); | ||
expect(func('a', 'a')).toEqual(false); | ||
expect(func('a', 0, 'a', 1)).toEqual(false); | ||
expect(func(2)).toEqual(false); | ||
expect(func(2, 'a')).toEqual(false); | ||
}); | ||
it('should match opt', () => { | ||
const func = (...args) => matchSchema(['a?', 'a'])(args); | ||
expect(func()).toEqual(false); | ||
expect(func('a')).toEqual(true); | ||
expect(func('a', 'b')).toEqual(false); | ||
expect(func('a', 0, 1)).toEqual(true); | ||
expect(func('a', 'b', 0, 1)).toEqual(false); | ||
expect(func('a', 'a')).toEqual(true); | ||
expect(func('a', 0, 'a', 1)).toEqual(true); | ||
expect(func(2)).toEqual(false); | ||
expect(func(2, 'a')).toEqual(false); | ||
}); | ||
}); | ||
describe('pickType', () => { | ||
it('should pick object', () => { | ||
const config = prepareSchemaConfig({ k: 1 }); | ||
expect(pickType(config)()).toEqual(config); | ||
}); | ||
it('should pick simple', () => { | ||
@@ -175,3 +254,3 @@ const config = prepareSchemaConfig([ | ||
]); | ||
expect(pickType(config)({ path: { key: 'a' } })).toBe(config[0][1]); | ||
expect(pickType(config)({ path: { key: 'a' } })).toEqual(config[0][1]); | ||
}); | ||
@@ -184,3 +263,3 @@ | ||
]); | ||
expect(pickType(config)({ path: { key: 'a' } })).toBe(config[0][1]); | ||
expect(pickType(config)({ path: { key: 'a' } })).toEqual(config[0][1]); | ||
}); | ||
@@ -201,3 +280,3 @@ | ||
]); | ||
expect(pickType(config)({ path: { key: 'a' } })).toBe(config[0][1]); | ||
expect(pickType(config)({ path: { key: 'a' } })).toEqual(config[0][1]); | ||
}); | ||
@@ -204,0 +283,0 @@ |
Sorry, the diff of this file is not supported yet
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
216631
2704
0
112