├── .gitignore ├── examples ├── simple │ ├── schema.graphql │ ├── models.js │ ├── index.js │ └── index.test.js ├── nested │ ├── schema.graphql │ ├── models.js │ ├── index.test.js │ └── index.js ├── reference │ ├── schema.graphql │ ├── models.js │ ├── index.test.js │ └── index.js ├── heterogeneous │ ├── schema.graphql │ ├── models.js │ ├── index.test.js │ └── index.js ├── populate-manual │ ├── schema.graphql │ ├── models.js │ ├── index.test.js │ └── index.js ├── populate-simple │ ├── schema.graphql │ ├── models.js │ ├── index.js │ └── index.test.js ├── populate-virtual │ ├── schema.graphql │ ├── models.js │ ├── index.test.js │ └── index.js ├── interface │ ├── schema.graphql │ ├── models.js │ ├── index.js │ └── index.test.js └── index.js ├── .github └── dependabot.yml ├── .editorconfig ├── tests ├── schema.graphql ├── resolver.test.js ├── prepareConfig.test.js ├── population.test.js ├── schema.test.js └── projection.test.js ├── logger.js ├── appveyor.yml ├── index.js ├── src ├── resolver.js ├── prepareConfig.js ├── population.js ├── core.js ├── projection.js └── schema.js ├── LICENSE.md ├── package.json ├── .eslintrc.json ├── README.md └── docs └── API.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Artifact 2 | node_modules 3 | coverage 4 | 5 | # Cruft 6 | *~ 7 | *.swp 8 | *.swo 9 | .DS_Store 10 | npm-debug.log 11 | yarn-error.log 12 | -------------------------------------------------------------------------------- /examples/simple/schema.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | user(id: ID!): User 3 | } 4 | 5 | type User { 6 | userId: ID 7 | field1: String 8 | field2: String 9 | } 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | versioning-strategy: increase-if-necessary 8 | -------------------------------------------------------------------------------- /examples/nested/schema.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | user(id: ID!): User 3 | } 4 | 5 | type User { 6 | alters: [ExtraInfo] 7 | } 8 | 9 | type ExtraInfo { 10 | field3: Int 11 | } 12 | -------------------------------------------------------------------------------- /examples/reference/schema.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | user(id: ID!): User 3 | } 4 | 5 | type User { 6 | items: [Item] 7 | } 8 | 9 | type Item { 10 | itemId: ID 11 | field4: String 12 | } 13 | -------------------------------------------------------------------------------- /examples/heterogeneous/schema.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | user(id: ID!): User 3 | item(id: ID!): Item 4 | } 5 | 6 | type User { 7 | items: [Item] 8 | } 9 | 10 | type Item { 11 | itemId: ID 12 | field4: String 13 | } 14 | -------------------------------------------------------------------------------- /examples/simple/models.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const UserSchema = new mongoose.Schema({ 4 | _id: String, 5 | mongoA: String, 6 | }); 7 | 8 | module.exports = { 9 | User: mongoose.model('users', UserSchema), 10 | }; 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | indent_style = space 10 | indent_size = 2 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /examples/populate-manual/schema.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | user(id: ID!): User 3 | } 4 | 5 | type User { 6 | items: [Item] 7 | } 8 | 9 | type Item { 10 | itemId: ID 11 | field4: String 12 | subs: [SubItem] 13 | } 14 | 15 | type SubItem { 16 | content: String 17 | } 18 | -------------------------------------------------------------------------------- /examples/populate-simple/schema.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | user(id: ID!): User 3 | } 4 | 5 | type User { 6 | items: [Item] 7 | } 8 | 9 | type Item { 10 | itemId: ID 11 | field4: String 12 | subs: [SubItem] 13 | } 14 | 15 | type SubItem { 16 | content: String 17 | } 18 | -------------------------------------------------------------------------------- /examples/populate-virtual/schema.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | user(id: ID!): User 3 | } 4 | 5 | type User { 6 | items: [Item] 7 | } 8 | 9 | type Item { 10 | itemId: ID 11 | field4: String 12 | subs: [SubItem] 13 | } 14 | 15 | type SubItem { 16 | content: String 17 | } 18 | -------------------------------------------------------------------------------- /examples/interface/schema.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | user(id: ID!): User 3 | } 4 | 5 | interface User { 6 | field1: Int 7 | } 8 | 9 | type AdminUser implements User { 10 | field1: Int 11 | field2: String 12 | } 13 | 14 | type NormalUser implements User { 15 | field1: Int 16 | field3: String 17 | } 18 | -------------------------------------------------------------------------------- /examples/interface/models.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const UserSchema = new mongoose.Schema({ 4 | _id: String, 5 | type: String, // ['admin', 'normal'] 6 | mongoA: Number, 7 | mongoB: String, 8 | mongoC: String, 9 | }); 10 | 11 | module.exports = { 12 | User: mongoose.model('users', UserSchema), 13 | }; 14 | -------------------------------------------------------------------------------- /examples/nested/models.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const NestedSchema = new mongoose.Schema({ 4 | mongoC: Number, 5 | }); 6 | 7 | const UserSchema = new mongoose.Schema({ 8 | _id: String, 9 | mongoA: String, 10 | nested: [NestedSchema], 11 | }); 12 | 13 | module.exports = { 14 | User: mongoose.model('users', UserSchema), 15 | }; 16 | -------------------------------------------------------------------------------- /examples/reference/models.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const ItemSchema = new mongoose.Schema({ 4 | _id: String, 5 | mongoD: String, 6 | }); 7 | 8 | const UserSchema = new mongoose.Schema({ 9 | _id: String, 10 | itemsId: [String], // ItemSchema._id 11 | }); 12 | 13 | module.exports = { 14 | Item: mongoose.model('items', ItemSchema), 15 | User: mongoose.model('users', UserSchema), 16 | }; 17 | -------------------------------------------------------------------------------- /tests/schema.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | obj: [Obj!] 3 | evil: Evil 4 | } 5 | 6 | type Obj { 7 | field1: String 8 | field2: Foo 9 | field3: [Father!]! 10 | evil: Evil 11 | } 12 | 13 | type Foo { 14 | f1: String 15 | } 16 | 17 | interface Father { 18 | g0: Int 19 | } 20 | 21 | type Child implements Father { 22 | g0: Int 23 | g1: String 24 | } 25 | 26 | type Evil { 27 | self: Evil 28 | field: String 29 | } 30 | -------------------------------------------------------------------------------- /logger.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const debug = require('debug')('graphql-advanced-projection'); 3 | 4 | const print = (k) => (message, data) => { 5 | const msg = `${k}: ${message}`; 6 | if (data === undefined) { 7 | debug(msg); 8 | } else { 9 | debug(`${msg} %O`, data); 10 | } 11 | }; 12 | 13 | _.keys({ 14 | fatal: 0, 15 | error: 1, 16 | warn: 2, 17 | info: 3, 18 | debug: 4, 19 | trace: 5, 20 | }).forEach((k) => { 21 | module.exports[k] = print(k); 22 | }); 23 | -------------------------------------------------------------------------------- /examples/heterogeneous/models.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const ItemSchema = new mongoose.Schema({ 4 | _id: String, 5 | mongoE: String, 6 | }); 7 | 8 | const NestedItemSchema = new mongoose.Schema({ 9 | _id: String, 10 | mongoD: String, 11 | }); 12 | 13 | const UserSchema = new mongoose.Schema({ 14 | _id: String, 15 | items: [NestedItemSchema], 16 | }); 17 | 18 | module.exports = { 19 | Item: mongoose.model('items', ItemSchema), 20 | User: mongoose.model('users', UserSchema), 21 | }; 22 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 2.0.0.{build} 2 | 3 | skip_tags: true 4 | 5 | skip_commits: 6 | message: /\[ci skip\]|\[skip ci\]/ 7 | 8 | image: Ubuntu2004 9 | stack: node node, mongodb 10 | 11 | shallow_clone: true 12 | clone_depth: 1 13 | 14 | environment: 15 | COVERALLS_REPO_TOKEN: 16 | secure: DJqo9Kbit+Ndeo872mHNnpvhC3YiOjmSlc7sI01gHeSbxRC2t6VMfY8Fdjh7D3kW 17 | 18 | install: 19 | - npm ci 20 | 21 | build: off 22 | 23 | test_script: 24 | - node --version 25 | - npm --version 26 | - npm test --color 27 | - npm run coveralls 28 | -------------------------------------------------------------------------------- /examples/index.js: -------------------------------------------------------------------------------- 1 | const { graphql } = require('graphql'); 2 | const mongoose = require('mongoose'); 3 | 4 | module.exports.run = (schema, source) => graphql({ schema, source }); 5 | 6 | module.exports.connect = () => new Promise((resolve, reject) => { 7 | const host = process.env.MONGO_HOST || 'localhost'; 8 | const dbName = 'graphql-advanced-projection-example'; 9 | 10 | mongoose.connection.on('connected', () => { 11 | resolve(); 12 | }); 13 | 14 | try { 15 | mongoose.connect(`mongodb://${host}/${dbName}`).then(resolve, reject); 16 | } catch (e) /* istanbul ignore next */ { 17 | reject(e); 18 | } 19 | }); 20 | 21 | module.exports.disconnect = mongoose.disconnect; 22 | -------------------------------------------------------------------------------- /examples/populate-manual/models.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const SubItemSchema = new mongoose.Schema({ 4 | _id: String, 5 | c: String, 6 | }); 7 | 8 | const ItemSchema = new mongoose.Schema({ 9 | _id: String, 10 | mongoD: String, 11 | subsId: [{ 12 | type: String, 13 | ref: 'sub-items', 14 | }], 15 | }); 16 | 17 | const UserSchema = new mongoose.Schema({ 18 | _id: String, 19 | itemsId: [{ 20 | type: String, 21 | ref: 'items', 22 | }], 23 | }); 24 | 25 | module.exports = { 26 | SubItem: mongoose.model('sub-items', SubItemSchema), 27 | Item: mongoose.model('items', ItemSchema), 28 | User: mongoose.model('users', UserSchema), 29 | }; 30 | -------------------------------------------------------------------------------- /examples/populate-simple/models.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const SubItemSchema = new mongoose.Schema({ 4 | _id: String, 5 | c: String, 6 | }); 7 | 8 | const ItemSchema = new mongoose.Schema({ 9 | _id: String, 10 | mongoD: String, 11 | subsId: [{ 12 | type: String, 13 | ref: 'sub-items', 14 | }], 15 | }); 16 | 17 | const UserSchema = new mongoose.Schema({ 18 | _id: String, 19 | itemsId: [{ 20 | type: String, 21 | ref: 'items', 22 | }], 23 | }); 24 | 25 | module.exports = { 26 | SubItem: mongoose.model('sub-items', SubItemSchema), 27 | Item: mongoose.model('items', ItemSchema), 28 | User: mongoose.model('users', UserSchema), 29 | }; 30 | -------------------------------------------------------------------------------- /examples/nested/index.test.js: -------------------------------------------------------------------------------- 1 | const jestMongoose = require('jest-mongoose'); 2 | const models = require('./models'); 3 | const gql = require('.'); 4 | const { run, connect, disconnect } = require('..'); 5 | 6 | const { make } = jestMongoose(models, connect, disconnect); 7 | 8 | it('alters.field4', async () => { 9 | await make.User({ 10 | _id: 'the-id', 11 | nested: [ 12 | { mongoC: 123 }, 13 | { mongoC: 456 }, 14 | ], 15 | }); 16 | const result = await run(gql, ` 17 | query { 18 | user(id: "the-id") { 19 | alters { 20 | field3 21 | } 22 | } 23 | } 24 | `); 25 | expect(result).toEqual({ 26 | data: { 27 | user: { 28 | alters: [ 29 | { field3: 123 }, 30 | { field3: 456 }, 31 | ], 32 | }, 33 | }, 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { prepareConfig } = require('./src/prepareConfig'); 2 | const { genProjection } = require('./src/projection'); 3 | const { genPopulation } = require('./src/population'); 4 | const { genResolvers } = require('./src/resolver'); 5 | 6 | const gqlProjection = (config) => { 7 | const ncfgs = prepareConfig(config); 8 | 9 | return { 10 | project: genProjection(ncfgs), 11 | populator: genPopulation(ncfgs), 12 | resolvers: genResolvers(ncfgs), 13 | }; 14 | }; 15 | 16 | module.exports = gqlProjection; 17 | module.exports.default = gqlProjection; 18 | module.exports.gqlProjection = gqlProjection; 19 | module.exports.prepareConfig = prepareConfig; 20 | module.exports.genProjection = genProjection; 21 | module.exports.genPopulation = genPopulation; 22 | module.exports.genResolvers = genResolvers; 23 | -------------------------------------------------------------------------------- /examples/populate-virtual/models.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const SubItemSchema = new mongoose.Schema({ 4 | _id: String, 5 | c: String, 6 | }); 7 | 8 | const ItemSchema = new mongoose.Schema({ 9 | _id: String, 10 | mongoD: String, 11 | subsId: [String], // For testing purpose only 12 | }); 13 | 14 | ItemSchema.virtual('subs', { 15 | ref: 'sub-items', 16 | localField: 'subsId', 17 | foreignField: '_id', 18 | justOne: false, 19 | }); 20 | 21 | const UserSchema = new mongoose.Schema({ 22 | _id: String, 23 | itemsId: [String], // For testing purpose only 24 | }); 25 | 26 | UserSchema.virtual('items', { 27 | ref: 'items', 28 | localField: 'itemsId', 29 | foreignField: '_id', 30 | justOne: false, 31 | }); 32 | 33 | module.exports = { 34 | SubItem: mongoose.model('sub-items', SubItemSchema), 35 | Item: mongoose.model('items', ItemSchema), 36 | User: mongoose.model('users', UserSchema), 37 | }; 38 | -------------------------------------------------------------------------------- /examples/nested/index.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const { makeExecutableSchema } = require('@graphql-tools/schema'); 5 | const { User } = require('./models'); 6 | const gqlProjection = require('../..'); 7 | 8 | const { project, resolvers } = gqlProjection({ 9 | User: { 10 | proj: { 11 | alters: 'nested.', 12 | }, 13 | }, 14 | ExtraInfo: { 15 | proj: { 16 | field3: 'mongoC', 17 | }, 18 | }, 19 | }); 20 | 21 | module.exports = makeExecutableSchema({ 22 | typeDefs: fs.readFileSync(path.join(__dirname, 'schema.graphql'), 'utf-8'), 23 | resolvers: _.merge(resolvers, { 24 | Query: { 25 | async user(parent, args, context, info) { 26 | const proj = project(info); 27 | const result = await User.findById(args.id, proj); 28 | return result.toObject(); 29 | }, 30 | }, 31 | }), 32 | resolverValidationOptions: { requireResolversForResolveType: false }, 33 | }); 34 | -------------------------------------------------------------------------------- /examples/simple/index.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const { makeExecutableSchema } = require('@graphql-tools/schema'); 5 | const { User } = require('./models'); 6 | const gqlProjection = require('../..'); 7 | 8 | const { project, resolvers } = gqlProjection({ 9 | User: { 10 | proj: { 11 | userId: '_id', 12 | field1: 'mongoA', 13 | field2: null, 14 | }, 15 | }, 16 | }); 17 | 18 | module.exports = makeExecutableSchema({ 19 | typeDefs: fs.readFileSync(path.join(__dirname, 'schema.graphql'), 'utf-8'), 20 | resolvers: _.merge(resolvers, { 21 | Query: { 22 | async user(parent, args, context, info) { 23 | const proj = project(info); 24 | const result = await User.findById(args.id, proj); 25 | return result.toObject(); 26 | }, 27 | }, 28 | User: { 29 | field2: () => 'Hello World', 30 | }, 31 | }), 32 | resolverValidationOptions: { requireResolversForResolveType: false }, 33 | }); 34 | -------------------------------------------------------------------------------- /src/resolver.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash/fp'); 2 | const logger = require('../logger'); 3 | 4 | function makeResolver(configs, pick) { 5 | let fn; 6 | if (!_.isArray(configs)) { 7 | fn = _.compose( 8 | _.mapValues(_.get), 9 | _.pickBy(_.identity), 10 | _.mapValues(_.get('select')), 11 | _.get('proj'), 12 | ); 13 | } else { 14 | fn = _.compose( 15 | _.fromPairs, 16 | _.map((k) => [k, (parent, args, context, info) => { 17 | const cfg = pick(info); 18 | const select = _.get(['proj', k, 'select'])(cfg); 19 | return _.get(select || k)(parent); 20 | }]), 21 | _.uniq, 22 | _.flatMap(_.keys), 23 | _.map('[1].proj'), 24 | ); 25 | } 26 | const res = fn(configs); 27 | return res; 28 | } 29 | 30 | const genResolvers = ({ config, pick }) => { 31 | const result = _.mergeWith(makeResolver)(config, pick); 32 | logger.info('Resolvers', result); 33 | return result; 34 | }; 35 | 36 | module.exports = { 37 | makeResolver, 38 | genResolvers, 39 | }; 40 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 b1f6c1c4 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/heterogeneous/index.test.js: -------------------------------------------------------------------------------- 1 | const jestMongoose = require('jest-mongoose'); 2 | const models = require('./models'); 3 | const gql = require('.'); 4 | const { run, connect, disconnect } = require('..'); 5 | 6 | const { make } = jestMongoose(models, connect, disconnect); 7 | 8 | it('item.field4', async () => { 9 | await make.Item({ 10 | _id: 'item-id', 11 | mongoE: 'ccc', 12 | }); 13 | const result = await run(gql, ` 14 | query { 15 | item(id: "item-id") { 16 | field4 17 | } 18 | } 19 | `); 20 | expect(result).toEqual({ 21 | data: { 22 | item: { 23 | field4: 'ccc', 24 | }, 25 | }, 26 | }); 27 | }); 28 | 29 | it('user.items.field4', async () => { 30 | await make.User({ 31 | _id: 'the-id', 32 | items: [ 33 | { mongoD: 'aaa' }, 34 | { mongoD: 'bbb' }, 35 | ], 36 | }); 37 | const result = await run(gql, ` 38 | query { 39 | user(id: "the-id") { 40 | items { 41 | field4 42 | } 43 | } 44 | } 45 | `); 46 | expect(result).toEqual({ 47 | data: { 48 | user: { 49 | items: [ 50 | { field4: 'aaa' }, 51 | { field4: 'bbb' }, 52 | ], 53 | }, 54 | }, 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-advanced-projection", 3 | "version": "2.0.0", 4 | "description": "Fully customizable Mongoose/MongoDB projection generator.", 5 | "main": "index.js", 6 | "repository": "https://github.com/b1f6c1c4/graphql-advanced-projection", 7 | "keywords": [ 8 | "mongo", 9 | "mongodb", 10 | "projection", 11 | "graphql", 12 | "apollo", 13 | "mongoose" 14 | ], 15 | "scripts": { 16 | "lint": "eslint --ignore-path .gitignore .", 17 | "test": "npm run lint && jest --coverage --runInBand", 18 | "coveralls": "coveralls < ./coverage/lcov.info" 19 | }, 20 | "author": "b1f6c1c4 ", 21 | "license": "MIT", 22 | "dependencies": { 23 | "debug": "^4.3.4", 24 | "lodash": "^4.17.21" 25 | }, 26 | "devDependencies": { 27 | "@graphql-tools/schema": "^10.0.0", 28 | "coveralls": "^3.1.1", 29 | "eslint": "^8.21.0", 30 | "eslint-config-airbnb-base": "^15.0.0", 31 | "eslint-plugin-import": "^2.26.0", 32 | "eslint-plugin-lodash-fp": "^2.2.0-a1", 33 | "graphql": "^16.5.0", 34 | "jest-cli": "^28.1.0", 35 | "jest-mongoose": "^2.0.0", 36 | "mongoose": "^6.5.0" 37 | }, 38 | "jest": { 39 | "testEnvironment": "node" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/populate-simple/index.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const { makeExecutableSchema } = require('@graphql-tools/schema'); 5 | const { User } = require('./models'); 6 | const gqlProjection = require('../..'); 7 | 8 | const { project, populator, resolvers } = gqlProjection({ 9 | User: { 10 | proj: { 11 | items: 'itemsId', 12 | }, 13 | }, 14 | Item: { 15 | proj: { 16 | itemId: '_id', 17 | field4: 'mongoD', 18 | subs: 'subsId', 19 | }, 20 | }, 21 | SubItem: { 22 | proj: { 23 | content: 'c', 24 | }, 25 | }, 26 | }); 27 | 28 | module.exports = makeExecutableSchema({ 29 | typeDefs: fs.readFileSync(path.join(__dirname, 'schema.graphql'), 'utf-8'), 30 | resolvers: _.merge(resolvers, { 31 | Query: { 32 | async user(parent, args, context, info) { 33 | const proj = project(info); 34 | let promise = User.findById(args.id, proj); 35 | const popu = populator(info); 36 | if (popu) { 37 | promise = promise.populate(popu); 38 | } 39 | return promise; 40 | }, 41 | }, 42 | }), 43 | resolverValidationOptions: { requireResolversForResolveType: false }, 44 | }); 45 | -------------------------------------------------------------------------------- /examples/simple/index.test.js: -------------------------------------------------------------------------------- 1 | const jestMongoose = require('jest-mongoose'); 2 | const models = require('./models'); 3 | const gql = require('.'); 4 | const { run, connect, disconnect } = require('..'); 5 | 6 | const { make } = jestMongoose(models, connect, disconnect); 7 | 8 | it('userId', async () => { 9 | await make.User({ _id: 'the-id' }); 10 | const result = await run(gql, ` 11 | query { 12 | user(id: "the-id") { 13 | userId 14 | } 15 | } 16 | `); 17 | expect(result).toEqual({ 18 | data: { 19 | user: { 20 | userId: 'the-id', 21 | }, 22 | }, 23 | }); 24 | }); 25 | 26 | it('field1', async () => { 27 | await make.User({ _id: 'the-id', mongoA: 'value' }); 28 | const result = await run(gql, ` 29 | query { 30 | user(id: "the-id") { 31 | field1 32 | } 33 | } 34 | `); 35 | expect(result).toEqual({ 36 | data: { 37 | user: { 38 | field1: 'value', 39 | }, 40 | }, 41 | }); 42 | }); 43 | 44 | it('field2', async () => { 45 | await make.User({ _id: 'the-id' }); 46 | const result = await run(gql, ` 47 | query { 48 | user(id: "the-id") { 49 | field2 50 | } 51 | } 52 | `); 53 | expect(result).toEqual({ 54 | data: { 55 | user: { 56 | field2: 'Hello World', 57 | }, 58 | }, 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /examples/heterogeneous/index.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const { makeExecutableSchema } = require('@graphql-tools/schema'); 5 | const { User, Item } = require('./models'); 6 | const gqlProjection = require('../..'); 7 | 8 | const { project, resolvers } = gqlProjection({ 9 | User: { 10 | proj: { 11 | items: true, 12 | }, 13 | }, 14 | Item: [ 15 | ['user', { 16 | proj: { 17 | itemId: '_id', 18 | field4: 'mongoD', 19 | }, 20 | }], 21 | ['item', { 22 | proj: { 23 | itemId: '_id', 24 | field4: 'mongoE', 25 | }, 26 | }], 27 | ], 28 | }); 29 | 30 | module.exports = makeExecutableSchema({ 31 | typeDefs: fs.readFileSync(path.join(__dirname, 'schema.graphql'), 'utf-8'), 32 | resolvers: _.merge(resolvers, { 33 | Query: { 34 | async user(parent, args, context, info) { 35 | const proj = project(info); 36 | const result = await User.findById(args.id, proj); 37 | return result.toObject(); 38 | }, 39 | async item(parent, args, context, info) { 40 | const proj = project(info); 41 | const result = await Item.findById(args.id, proj); 42 | return result.toObject(); 43 | }, 44 | }, 45 | }), 46 | resolverValidationOptions: { requireResolversForResolveType: false }, 47 | }); 48 | -------------------------------------------------------------------------------- /examples/reference/index.test.js: -------------------------------------------------------------------------------- 1 | const jestMongoose = require('jest-mongoose'); 2 | const models = require('./models'); 3 | const gql = require('.'); 4 | const { run, connect, disconnect } = require('..'); 5 | 6 | const { make } = jestMongoose(models, connect, disconnect); 7 | 8 | it('itemId', async () => { 9 | await make.Item({ _id: 'item1', mongoD: 'd1' }); 10 | await make.Item({ _id: 'item2', mongoD: 'd2' }); 11 | await make.User({ 12 | _id: 'the-id', 13 | itemsId: ['item1', 'item2'], 14 | }); 15 | const result = await run(gql, ` 16 | query { 17 | user(id: "the-id") { 18 | items { 19 | itemId 20 | } 21 | } 22 | } 23 | `); 24 | expect(result).toEqual({ 25 | data: { 26 | user: { 27 | items: [ 28 | { itemId: 'item1' }, 29 | { itemId: 'item2' }, 30 | ], 31 | }, 32 | }, 33 | }); 34 | }); 35 | 36 | it('field4', async () => { 37 | await make.Item({ _id: 'item1', mongoD: 'd1' }); 38 | await make.Item({ _id: 'item2', mongoD: 'd2' }); 39 | await make.User({ 40 | _id: 'the-id', 41 | itemsId: ['item1', 'item2'], 42 | }); 43 | const result = await run(gql, ` 44 | query { 45 | user(id: "the-id") { 46 | items { 47 | field4 48 | } 49 | } 50 | } 51 | `); 52 | expect(result).toEqual({ 53 | data: { 54 | user: { 55 | items: [ 56 | { field4: 'd1' }, 57 | { field4: 'd2' }, 58 | ], 59 | }, 60 | }, 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /examples/interface/index.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const { makeExecutableSchema } = require('@graphql-tools/schema'); 5 | const { User } = require('./models'); 6 | const gqlProjection = require('../..'); 7 | 8 | const { project, resolvers } = gqlProjection({ 9 | User: { 10 | typeProj: 'type', 11 | proj: { 12 | field1: { query: 'mongoA' }, 13 | }, 14 | }, 15 | AdminUser: { 16 | proj: { 17 | field1: 'mongoA', 18 | field2: 'mongoB', 19 | }, 20 | }, 21 | NormalUser: { 22 | proj: { 23 | field1: 'mongoA', 24 | field3: 'mongoC', 25 | }, 26 | }, 27 | }); 28 | 29 | module.exports = makeExecutableSchema({ 30 | typeDefs: fs.readFileSync(path.join(__dirname, 'schema.graphql'), 'utf-8'), 31 | resolvers: _.merge(resolvers, { 32 | Query: { 33 | async user(parent, args, context, info) { 34 | const proj = project(info); 35 | const result = await User.findById(args.id, proj); 36 | return result.toObject(); 37 | }, 38 | }, 39 | User: { 40 | __resolveType(parent) { 41 | switch (parent.type) { 42 | case 'admin': 43 | return 'AdminUser'; 44 | case 'normal': 45 | return 'NormalUser'; 46 | default: /* istanbul ignore next */ 47 | return null; 48 | } 49 | }, 50 | }, 51 | }), 52 | resolverValidationOptions: { requireResolversForResolveType: false }, 53 | }); 54 | -------------------------------------------------------------------------------- /examples/reference/index.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const { makeExecutableSchema } = require('@graphql-tools/schema'); 5 | const { User, Item } = require('./models'); 6 | const gqlProjection = require('../..'); 7 | 8 | const { project, resolvers } = gqlProjection({ 9 | User: { 10 | proj: { 11 | items: { query: 'itemsId' }, 12 | }, 13 | }, 14 | Item: { 15 | proj: { 16 | itemId: '_id', 17 | field4: 'mongoD', 18 | }, 19 | }, 20 | }); 21 | 22 | module.exports = makeExecutableSchema({ 23 | typeDefs: fs.readFileSync(path.join(__dirname, 'schema.graphql'), 'utf-8'), 24 | resolvers: _.merge(resolvers, { 25 | Query: { 26 | async user(parent, args, context, info) { 27 | const proj = project(info); 28 | const result = await User.findById(args.id, proj); 29 | return result.toObject(); 30 | }, 31 | }, 32 | User: { 33 | async items(parent, args, context, info) { 34 | const proj = project(info); 35 | if (_.keys(proj).length === 1) { 36 | // Only itemId is inquired. 37 | // Save some db queries! 38 | return parent.itemsId.map((id) => ({ _id: id })); 39 | } 40 | const fetchItem = async (id) => { 41 | const result = await Item.findById(id, proj); 42 | return result.toObject(); 43 | }; 44 | const results = await Promise.all(parent.itemsId.map(fetchItem)); 45 | return results; 46 | }, 47 | }, 48 | }), 49 | resolverValidationOptions: { requireResolversForResolveType: false }, 50 | }); 51 | -------------------------------------------------------------------------------- /examples/populate-manual/index.test.js: -------------------------------------------------------------------------------- 1 | const jestMongoose = require('jest-mongoose'); 2 | const models = require('./models'); 3 | const gql = require('.'); 4 | const { run, connect, disconnect } = require('..'); 5 | 6 | const { make } = jestMongoose(models, connect, disconnect); 7 | 8 | it('itemId', async () => { 9 | await make.Item({ _id: 'item1', mongoD: 'd1' }); 10 | await make.Item({ _id: 'item2', mongoD: 'd2' }); 11 | await make.User({ 12 | _id: 'the-id', 13 | itemsId: ['item1', 'item2'], 14 | }); 15 | const result = await run(gql, ` 16 | query { 17 | user(id: "the-id") { 18 | items { 19 | itemId 20 | } 21 | } 22 | } 23 | `); 24 | expect(result).toEqual({ 25 | data: { 26 | user: { 27 | items: [ 28 | { itemId: 'item1' }, 29 | { itemId: 'item2' }, 30 | ], 31 | }, 32 | }, 33 | }); 34 | }); 35 | 36 | it('content', async () => { 37 | await make.SubItem({ _id: 'sub1', c: 'foo1' }); 38 | await make.SubItem({ _id: 'sub2', c: 'foo2' }); 39 | await make.SubItem({ _id: 'sub3', c: 'foo3' }); 40 | await make.Item({ _id: 'item1', mongoD: 'd1', subsId: ['sub1', 'sub2'] }); 41 | await make.Item({ _id: 'item2', mongoD: 'd2', subsId: ['sub1', 'sub3'] }); 42 | await make.User({ 43 | _id: 'the-id', 44 | itemsId: ['item1', 'item2'], 45 | }); 46 | const result = await run(gql, ` 47 | query { 48 | user(id: "the-id") { 49 | items { 50 | field4 51 | subs { 52 | content 53 | } 54 | } 55 | } 56 | } 57 | `); 58 | expect(result).toEqual({ 59 | data: { 60 | user: { 61 | items: [ 62 | { field4: 'd1', subs: [{ content: 'foo1' }, { content: 'foo2' }] }, 63 | { field4: 'd2', subs: [{ content: 'foo1' }, { content: 'foo3' }] }, 64 | ], 65 | }, 66 | }, 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /examples/populate-simple/index.test.js: -------------------------------------------------------------------------------- 1 | const jestMongoose = require('jest-mongoose'); 2 | const models = require('./models'); 3 | const gql = require('.'); 4 | const { run, connect, disconnect } = require('..'); 5 | 6 | const { make } = jestMongoose(models, connect, disconnect); 7 | 8 | it('itemId', async () => { 9 | await make.Item({ _id: 'item1', mongoD: 'd1' }); 10 | await make.Item({ _id: 'item2', mongoD: 'd2' }); 11 | await make.User({ 12 | _id: 'the-id', 13 | itemsId: ['item1', 'item2'], 14 | }); 15 | const result = await run(gql, ` 16 | query { 17 | user(id: "the-id") { 18 | items { 19 | itemId 20 | } 21 | } 22 | } 23 | `); 24 | expect(result).toEqual({ 25 | data: { 26 | user: { 27 | items: [ 28 | { itemId: 'item1' }, 29 | { itemId: 'item2' }, 30 | ], 31 | }, 32 | }, 33 | }); 34 | }); 35 | 36 | it('content', async () => { 37 | await make.SubItem({ _id: 'sub1', c: 'foo1' }); 38 | await make.SubItem({ _id: 'sub2', c: 'foo2' }); 39 | await make.SubItem({ _id: 'sub3', c: 'foo3' }); 40 | await make.Item({ _id: 'item1', mongoD: 'd1', subsId: ['sub1', 'sub2'] }); 41 | await make.Item({ _id: 'item2', mongoD: 'd2', subsId: ['sub1', 'sub3'] }); 42 | await make.User({ 43 | _id: 'the-id', 44 | itemsId: ['item1', 'item2'], 45 | }); 46 | const result = await run(gql, ` 47 | query { 48 | user(id: "the-id") { 49 | items { 50 | field4 51 | subs { 52 | content 53 | } 54 | } 55 | } 56 | } 57 | `); 58 | expect(result).toEqual({ 59 | data: { 60 | user: { 61 | items: [ 62 | { field4: 'd1', subs: [{ content: 'foo1' }, { content: 'foo2' }] }, 63 | { field4: 'd2', subs: [{ content: 'foo1' }, { content: 'foo3' }] }, 64 | ], 65 | }, 66 | }, 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /examples/populate-virtual/index.test.js: -------------------------------------------------------------------------------- 1 | const jestMongoose = require('jest-mongoose'); 2 | const models = require('./models'); 3 | const gql = require('.'); 4 | const { run, connect, disconnect } = require('..'); 5 | 6 | const { make } = jestMongoose(models, connect, disconnect); 7 | 8 | it('itemId', async () => { 9 | await make.Item({ _id: 'item1', mongoD: 'd1' }); 10 | await make.Item({ _id: 'item2', mongoD: 'd2' }); 11 | await make.User({ 12 | _id: 'the-id', 13 | itemsId: ['item1', 'item2'], 14 | }); 15 | const result = await run(gql, ` 16 | query { 17 | user(id: "the-id") { 18 | items { 19 | itemId 20 | } 21 | } 22 | } 23 | `); 24 | expect(result).toEqual({ 25 | data: { 26 | user: { 27 | items: [ 28 | { itemId: 'item1' }, 29 | { itemId: 'item2' }, 30 | ], 31 | }, 32 | }, 33 | }); 34 | }); 35 | 36 | it('content', async () => { 37 | await make.SubItem({ _id: 'sub1', c: 'foo1' }); 38 | await make.SubItem({ _id: 'sub2', c: 'foo2' }); 39 | await make.SubItem({ _id: 'sub3', c: 'foo3' }); 40 | await make.Item({ _id: 'item1', mongoD: 'd1', subsId: ['sub1', 'sub2'] }); 41 | await make.Item({ _id: 'item2', mongoD: 'd2', subsId: ['sub1', 'sub3'] }); 42 | await make.User({ 43 | _id: 'the-id', 44 | itemsId: ['item1', 'item2'], 45 | }); 46 | const result = await run(gql, ` 47 | query { 48 | user(id: "the-id") { 49 | items { 50 | field4 51 | subs { 52 | content 53 | } 54 | } 55 | } 56 | } 57 | `); 58 | expect(result).toEqual({ 59 | data: { 60 | user: { 61 | items: [ 62 | { field4: 'd1', subs: [{ content: 'foo1' }, { content: 'foo2' }] }, 63 | { field4: 'd2', subs: [{ content: 'foo1' }, { content: 'foo3' }] }, 64 | ], 65 | }, 66 | }, 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /examples/populate-virtual/index.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const { makeExecutableSchema } = require('@graphql-tools/schema'); 5 | const { User, Item } = require('./models'); 6 | const gqlProjection = require('../..'); 7 | 8 | const { project, resolvers } = gqlProjection({ 9 | User: { 10 | proj: { 11 | items: { query: 'itemsId' }, 12 | }, 13 | }, 14 | Item: { 15 | proj: { 16 | itemId: '_id', 17 | field4: 'mongoD', 18 | subs: { query: 'subsId' }, 19 | }, 20 | }, 21 | SubItem: { 22 | proj: { 23 | content: 'c', 24 | }, 25 | }, 26 | }); 27 | 28 | module.exports = makeExecutableSchema({ 29 | typeDefs: fs.readFileSync(path.join(__dirname, 'schema.graphql'), 'utf-8'), 30 | resolvers: _.merge(resolvers, { 31 | Query: { 32 | async user(parent, args, context, info) { 33 | const proj = project(info); 34 | const result = await User.findById(args.id, proj); 35 | return result.toObject(); 36 | }, 37 | }, 38 | User: { 39 | async items(parent, args, context, info) { 40 | const proj = project(info); 41 | if (_.keys(proj).length === 1) { 42 | // Only itemId is inquired. 43 | // Save some db queries! 44 | return parent.itemsId.map((id) => ({ _id: id })); 45 | } 46 | await User.populate(parent, { path: 'items', select: proj }); 47 | return parent.items; 48 | }, 49 | }, 50 | Item: { 51 | async subs(parent, args, context, info) { 52 | const proj = project(info); 53 | if (_.keys(proj).length === 1) { 54 | // Only itemId is inquired. 55 | // Save some db queries! 56 | return parent.subsId.map((id) => ({ _id: id })); 57 | } 58 | await Item.populate(parent, { path: 'subs', select: proj }); 59 | return parent.subs; 60 | }, 61 | }, 62 | }), 63 | resolverValidationOptions: { requireResolversForResolveType: false }, 64 | }); 65 | -------------------------------------------------------------------------------- /examples/populate-manual/index.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const { makeExecutableSchema } = require('@graphql-tools/schema'); 5 | const { User, Item } = require('./models'); 6 | const gqlProjection = require('../..'); 7 | 8 | const { project, resolvers } = gqlProjection({ 9 | User: { 10 | proj: { 11 | items: { query: 'itemsId' }, 12 | }, 13 | }, 14 | Item: { 15 | proj: { 16 | itemId: '_id', 17 | field4: 'mongoD', 18 | subs: { query: 'subsId' }, 19 | }, 20 | }, 21 | SubItem: { 22 | proj: { 23 | content: 'c', 24 | }, 25 | }, 26 | }); 27 | 28 | module.exports = makeExecutableSchema({ 29 | typeDefs: fs.readFileSync(path.join(__dirname, 'schema.graphql'), 'utf-8'), 30 | resolvers: _.merge(resolvers, { 31 | Query: { 32 | async user(parent, args, context, info) { 33 | const proj = project(info); 34 | const result = await User.findById(args.id, proj); 35 | return result.toObject(); 36 | }, 37 | }, 38 | User: { 39 | async items(parent, args, context, info) { 40 | const proj = project(info); 41 | if (_.keys(proj).length === 1) { 42 | // Only itemId is inquired. 43 | // Save some db queries! 44 | return parent.itemsId.map((id) => ({ _id: id })); 45 | } 46 | await User.populate(parent, { path: 'itemsId', select: proj }); 47 | return parent.itemsId; 48 | }, 49 | }, 50 | Item: { 51 | async subs(parent, args, context, info) { 52 | const proj = project(info); 53 | if (_.keys(proj).length === 1) { 54 | // Only itemId is inquired. 55 | // Save some db queries! 56 | return parent.subsId.map((id) => ({ _id: id })); 57 | } 58 | await Item.populate(parent, { path: 'subsId', select: proj }); 59 | return parent.subsId; 60 | }, 61 | }, 62 | }), 63 | resolverValidationOptions: { requireResolversForResolveType: false }, 64 | }); 65 | -------------------------------------------------------------------------------- /src/prepareConfig.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash/fp'); 2 | const { pickType } = require('./schema'); 3 | const logger = require('../logger'); 4 | 5 | function prepareProjectionConfig(def, fieldName) { 6 | if (def === undefined) { 7 | return { query: fieldName }; 8 | } 9 | if (def === null) { 10 | return { query: null }; 11 | } 12 | if (def === true) { 13 | return { query: null, recursive: true }; 14 | } 15 | if (_.isString(def)) { 16 | if (def.endsWith('.')) { 17 | return { 18 | query: null, 19 | select: def.substr(0, def.length - 1), 20 | recursive: true, 21 | prefix: def, 22 | }; 23 | } 24 | return { 25 | query: def, 26 | select: def, 27 | }; 28 | } 29 | if (_.isArray(def)) { 30 | return { 31 | query: def, 32 | }; 33 | } 34 | return { 35 | query: def.query === undefined ? fieldName : def.query, 36 | select: def.select, 37 | recursive: def.recursive ? true : undefined, 38 | prefix: def.prefix, 39 | }; 40 | } 41 | 42 | function prepareSchemaConfig(config) { 43 | const norm = (cfg) => { 44 | if (cfg === undefined) { 45 | return [[null]]; 46 | } 47 | if (cfg === null) { 48 | return []; 49 | } 50 | if (_.isString(cfg)) { 51 | return [[cfg, null]]; 52 | } 53 | if (!_.isArray(cfg)) { 54 | throw new Error('Incorrect match config'); 55 | } 56 | if (cfg.length > 0 && !_.isArray(cfg[0])) { 57 | return [cfg]; 58 | } 59 | return cfg; 60 | }; 61 | if (_.isArray(config)) { 62 | return config.map(([m, t]) => [norm(m), _.cloneDeep(t)]); 63 | } 64 | return _.cloneDeep(config); 65 | } 66 | 67 | function prepareConfig(configs = {}) { 68 | const root = configs.root || { _id: 0 }; 69 | const ncfgs = _.compose( 70 | _.mapValues.convert({ cap: false })( 71 | (v) => (_.isArray(v) 72 | ? _.compose( 73 | _.map, 74 | _.update('[1].proj'), 75 | _.mapValues.convert({ cap: false }), 76 | ) 77 | : _.compose( 78 | _.update('proj'), 79 | _.mapValues.convert({ cap: false }), 80 | ) 81 | )(prepareProjectionConfig)(v), 82 | ), 83 | _.mapValues(prepareSchemaConfig), 84 | _.pickBy((v, k) => /^[A-Z]/.test(k)), 85 | )(configs); 86 | logger.info(`Total config: ${JSON.stringify(ncfgs, null, 2)}`); 87 | return { 88 | root, 89 | config: ncfgs, 90 | pick: _.mapValues(pickType)(ncfgs), 91 | }; 92 | } 93 | 94 | module.exports = { 95 | prepareProjectionConfig, 96 | prepareSchemaConfig, 97 | prepareConfig, 98 | }; 99 | -------------------------------------------------------------------------------- /src/population.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const fp = require('lodash/fp'); 3 | const { makeTraverser } = require('./core'); 4 | const { 5 | makePrefix, 6 | typeFunc, 7 | fieldFunc, 8 | } = require('./projection'); 9 | const logger = require('../logger'); 10 | 11 | const makePopulation = makeTraverser({ 12 | typeFunc: (cfg, [, prefix]) => typeFunc(cfg, [prefix]), 13 | fieldFunc({ root, config, field }, [metaPath, prefix], recursion) { 14 | const result = { 15 | path: metaPath, 16 | select: fieldFunc({ config, field }, [prefix]), 17 | }; 18 | const def = _.get(config.proj, field); 19 | if (recursion && def) { 20 | const pf = makePrefix(prefix, config.prefix); 21 | if (def.recursive) { 22 | const newArgs = [metaPath, makePrefix(pf, def.prefix, `${field}.`)]; 23 | const project = recursion(newArgs); 24 | return _.merge(result, project); 25 | } 26 | if (!_.isString(def.query)) { 27 | throw new Error('genPopulation with recursive=false must have query: string'); 28 | } 29 | const populate = _.merge({}, { select: root }, recursion([pf + def.query, ''])); // TODO 30 | return _.merge(result, { populate }); 31 | } 32 | return result; 33 | }, 34 | stepFunc({ config, field, type, next }, [metaPath, prefix], recursion) { 35 | logger.debug('Projecting (inline) fragment', field); 36 | const newPrefix = type.name === next.name 37 | ? prefix 38 | : makePrefix(prefix, config.prefix); 39 | return recursion([metaPath, newPrefix]); 40 | }, 41 | reduceFunc(configs, [metaPath], typeResult, fieldResults) { 42 | const raw = fp.compose( 43 | fp.mapValues(fp.reduce(fp.merge, {})), 44 | fp.groupBy('path'), 45 | fp.compact, 46 | fp.map('populate'), 47 | )(fieldResults); 48 | const select = fp.compose( 49 | fp.merge(typeResult), 50 | fp.reduce(fp.merge, {}), 51 | fp.map('select'), 52 | )(fieldResults); 53 | const populate = fp.values(raw); 54 | if (populate.length) { 55 | return { 56 | path: metaPath, 57 | select, 58 | populate, 59 | }; 60 | } 61 | return { 62 | path: metaPath, 63 | select, 64 | }; 65 | }, 66 | }, ['', '']); 67 | 68 | const genPopulation = ({ root, pick }) => { 69 | const populator = makePopulation({ root, pick }); 70 | return (info) => { 71 | const { populate } = populator(info); 72 | logger.debug('Population result', populate); 73 | return populate; 74 | }; 75 | }; 76 | 77 | module.exports = { 78 | makePopulation, 79 | genPopulation, 80 | }; 81 | -------------------------------------------------------------------------------- /tests/resolver.test.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const { graphql } = require('graphql'); 5 | const { makeExecutableSchema } = require('@graphql-tools/schema'); 6 | const { prepareConfig } = require('../src/prepareConfig'); 7 | const { genResolvers } = require('../src/resolver'); 8 | 9 | describe('genResolvers', () => { 10 | const typeDefs = fs.readFileSync(path.join(__dirname, 'schema.graphql'), 'utf-8'); 11 | 12 | const run = (config, source, { obj, evil }) => graphql({ 13 | schema: makeExecutableSchema({ 14 | typeDefs, 15 | resolvers: _.merge(genResolvers(prepareConfig(config)), { 16 | Query: { 17 | obj: () => obj, 18 | evil: () => evil, 19 | }, 20 | }), 21 | resolverValidationOptions: { requireResolversForResolveType: false }, 22 | }), 23 | source, 24 | }); 25 | 26 | it('should accept simple', async () => { 27 | const result = await run({ 28 | Obj: { 29 | proj: { 30 | field1: 'x', 31 | }, 32 | }, 33 | }, '{ obj { field1 } }', { 34 | obj: [{ x: 'xxx' }], 35 | }); 36 | expect(result).toEqual({ data: { obj: [{ field1: 'xxx' }] } }); 37 | }); 38 | 39 | it('should accept complex 1', async () => { 40 | const result = await run({ 41 | Evil: [ 42 | ['obj', { 43 | proj: { 44 | field: 'x', 45 | }, 46 | }], 47 | ['evil', { 48 | proj: { 49 | field: 'y', 50 | }, 51 | }], 52 | ], 53 | }, '{ obj { evil { field } } }', { 54 | obj: [{ evil: { x: 'xxx' } }], 55 | }); 56 | expect(result).toEqual({ data: { obj: [{ evil: { field: 'xxx' } }] } }); 57 | }); 58 | 59 | it('should accept complex 2', async () => { 60 | const result = await run({ 61 | Evil: [ 62 | ['obj', { 63 | proj: { 64 | field: 'x', 65 | }, 66 | }], 67 | ['evil', { 68 | proj: { 69 | field: 'y', 70 | }, 71 | }], 72 | ], 73 | }, '{ evil { field } }', { 74 | evil: { y: 'xxx' }, 75 | }); 76 | expect(result).toEqual({ data: { evil: { field: 'xxx' } } }); 77 | }); 78 | 79 | it('should accept complex 3', async () => { 80 | const result = await run({ 81 | Evil: [ 82 | ['obj', { 83 | proj: { 84 | field: 'x', 85 | }, 86 | }], 87 | ['evil', { 88 | proj: { 89 | field: null, 90 | }, 91 | }], 92 | ], 93 | }, '{ evil { field } }', { 94 | evil: { field: 'xxx' }, 95 | }); 96 | expect(result).toEqual({ data: { evil: { field: 'xxx' } } }); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /src/core.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | 3 | const stripType = (typeRef) => { 4 | const arr = typeRef.constructor.name === 'GraphQLList' ? 1 : 0; 5 | if (typeRef.ofType) { 6 | const res = stripType(typeRef.ofType); 7 | res.level += arr; 8 | return res; 9 | } 10 | return { name: typeRef.name, level: 0 }; 11 | }; 12 | 13 | const makeTraverser = ({ typeFunc, fieldFunc, stepFunc, reduceFunc }, seed) => (configs) => { 14 | const { root: globalRoot, pick } = configs; 15 | const func = (root, context, type) => (args) => { 16 | const { info } = root; 17 | const config = (pick[type.name] || _.constant({}))(info); 18 | const cfgs = { root: globalRoot, configs, config, type }; 19 | const fieldResults = []; 20 | const typeResult = typeFunc(cfgs, args); 21 | context.selectionSet.selections.forEach((sel) => { 22 | const field = _.get(sel, 'name.value'); 23 | switch (sel.kind) { 24 | case 'Field': { 25 | const typeRef = info.schema.getType(type.name); 26 | const fieldValue = typeRef.getFields()[field]; 27 | if (!fieldValue) { 28 | return; 29 | } 30 | const next = stripType(fieldValue.type); 31 | const recursion = sel.selectionSet ? func(root, sel, next) : undefined; 32 | fieldResults.push(fieldFunc({ 33 | ...cfgs, 34 | field, 35 | next, 36 | }, args, recursion)); 37 | return; 38 | } 39 | case 'InlineFragment': { 40 | const newType = _.get(sel, 'typeCondition.name.value'); 41 | const next = newType ? { name: newType, level: 0 } : type; 42 | const recursion = func(root, sel, next); 43 | fieldResults.push(stepFunc({ 44 | ...cfgs, 45 | field, 46 | next, 47 | }, args, recursion)); 48 | return; 49 | } 50 | case 'FragmentSpread': { 51 | const frag = info.fragments[field]; 52 | const newType = _.get(frag, 'typeCondition.name.value'); 53 | const next = { name: newType, level: 0 }; 54 | const recursion = func(root, frag, next); 55 | fieldResults.push(stepFunc({ 56 | ...cfgs, 57 | field, 58 | next, 59 | }, args, recursion)); 60 | return; 61 | } 62 | /* istanbul ignore next */ 63 | default: 64 | /* istanbul ignore next */ 65 | throw new Error(`sel.kind not supported: ${sel.kind}`); 66 | } 67 | }); 68 | return reduceFunc(cfgs, args, typeResult, fieldResults); 69 | }; 70 | return (info) => { 71 | const context = info.fieldNodes[0]; 72 | const type = stripType(info.returnType); 73 | return func( 74 | { info }, 75 | context, 76 | type, 77 | )(seed); 78 | }; 79 | }; 80 | 81 | module.exports = { 82 | stripType, 83 | makeTraverser, 84 | }; 85 | -------------------------------------------------------------------------------- /examples/interface/index.test.js: -------------------------------------------------------------------------------- 1 | const jestMongoose = require('jest-mongoose'); 2 | const models = require('./models'); 3 | const gql = require('.'); 4 | const { run, connect, disconnect } = require('..'); 5 | 6 | const { make } = jestMongoose(models, connect, disconnect); 7 | 8 | it('User.field1', async () => { 9 | await make.User({ 10 | _id: 'the-id', 11 | type: 'admin', 12 | mongoA: 123, 13 | }); 14 | const result = await run(gql, ` 15 | query { 16 | user(id: "the-id") { 17 | field1 18 | } 19 | } 20 | `); 21 | expect(result).toEqual({ 22 | data: { 23 | user: { 24 | field1: 123, 25 | }, 26 | }, 27 | }); 28 | }); 29 | 30 | it('User.field1 with typename', async () => { 31 | await make.User({ 32 | _id: 'the-id', 33 | type: 'admin', 34 | mongoA: 123, 35 | }); 36 | const result = await run(gql, ` 37 | query { 38 | user(id: "the-id") { 39 | __typename 40 | field1 41 | } 42 | } 43 | `); 44 | expect(result).toEqual({ 45 | data: { 46 | user: { 47 | __typename: 'AdminUser', 48 | field1: 123, 49 | }, 50 | }, 51 | }); 52 | }); 53 | 54 | it('AdminUser.field1', async () => { 55 | await make.User({ 56 | _id: 'the-id', 57 | type: 'admin', 58 | mongoA: 123, 59 | }); 60 | const result = await run(gql, ` 61 | query { 62 | user(id: "the-id") { 63 | ... on AdminUser { 64 | field1 65 | } 66 | } 67 | } 68 | `); 69 | expect(result).toEqual({ 70 | data: { 71 | user: { 72 | field1: 123, 73 | }, 74 | }, 75 | }); 76 | }); 77 | 78 | it('NormalUser.field1', async () => { 79 | await make.User({ 80 | _id: 'the-id', 81 | type: 'normal', 82 | mongoA: 123, 83 | }); 84 | const result = await run(gql, ` 85 | query { 86 | user(id: "the-id") { 87 | ... on NormalUser { 88 | field1 89 | } 90 | } 91 | } 92 | `); 93 | expect(result).toEqual({ 94 | data: { 95 | user: { 96 | field1: 123, 97 | }, 98 | }, 99 | }); 100 | }); 101 | 102 | it('field2', async () => { 103 | await make.User({ 104 | _id: 'the-id', 105 | type: 'admin', 106 | mongoB: 'value', 107 | }); 108 | const result = await run(gql, ` 109 | query { 110 | user(id: "the-id") { 111 | ... on AdminUser { 112 | field2 113 | } 114 | } 115 | } 116 | `); 117 | expect(result).toEqual({ 118 | data: { 119 | user: { 120 | field2: 'value', 121 | }, 122 | }, 123 | }); 124 | }); 125 | 126 | it('field3', async () => { 127 | await make.User({ 128 | _id: 'the-id', 129 | type: 'normal', 130 | mongoC: 'value', 131 | }); 132 | const result = await run(gql, ` 133 | query { 134 | user(id: "the-id") { 135 | ...frag 136 | } 137 | } 138 | 139 | fragment frag on NormalUser { 140 | field3 141 | } 142 | `); 143 | expect(result).toEqual({ 144 | data: { 145 | user: { 146 | field3: 'value', 147 | }, 148 | }, 149 | }); 150 | }); 151 | -------------------------------------------------------------------------------- /src/projection.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const { makeTraverser } = require('./core'); 3 | const logger = require('../logger'); 4 | 5 | const makePrefix = (prev, cur, subs) => { 6 | let curr = cur; 7 | if (curr === undefined) { 8 | curr = subs; 9 | } 10 | if (!curr) { 11 | return prev; 12 | } 13 | if (curr.startsWith('.')) { 14 | return curr.substr(1); 15 | } 16 | return prev + curr; 17 | }; 18 | 19 | const proj = (reason, pf, k) => { 20 | const result = {}; 21 | if (_.isArray(k)) { 22 | k.forEach((v) => { 23 | logger.trace(`>${reason}`, pf + v); 24 | result[pf + v] = 1; 25 | }); 26 | return result; 27 | } 28 | /* istanbul ignore else */ 29 | if (_.isString(k)) { 30 | logger.trace(`>${reason}`, pf + k); 31 | result[pf + k] = 1; 32 | return result; 33 | } 34 | /* istanbul ignore next */ 35 | throw new Error(`Proj not supported: ${k}`); 36 | }; 37 | 38 | const typeFunc = ({ config }, [prefix]) => { 39 | if (config.typeProj) { 40 | const pf = makePrefix(prefix, config.prefix); 41 | return proj('TypeProj', pf, config.typeProj); 42 | } 43 | return {}; 44 | }; 45 | 46 | const fieldFunc = ({ config, field }, [prefix]) => { 47 | const def = _.get(config.proj, field); 48 | const query = def === undefined ? field : def.query; 49 | if (query === null) { 50 | logger.trace('>Ignored'); 51 | return {}; 52 | } 53 | const pf = makePrefix(prefix, config.prefix); 54 | return proj('Simple', pf, query); 55 | }; 56 | 57 | const makeProjection = makeTraverser({ 58 | typeFunc, 59 | fieldFunc({ config, field }, [prefix], recursion) { 60 | const result = fieldFunc({ config, field }, [prefix]); 61 | const def = _.get(config.proj, field); 62 | if (recursion && def && def.recursive) { 63 | const pf = makePrefix(prefix, config.prefix); 64 | return _.assign(result, recursion([makePrefix(pf, def.prefix, `${field}.`)])); 65 | } 66 | return result; 67 | }, 68 | stepFunc({ config, field, type, next }, [prefix], recursion) { 69 | logger.debug('Projecting (inline) fragment', field); 70 | const newPrefix = type.name === next.name 71 | ? prefix 72 | : makePrefix(prefix, config.prefix); 73 | return recursion([newPrefix]); 74 | }, 75 | reduceFunc(configs, args, typeResult, fieldResults) { 76 | return _.assign({}, typeResult, ...fieldResults); 77 | }, 78 | }, ['']); 79 | 80 | const genProjection = ({ root, pick }) => { 81 | const projector = makeProjection({ pick }); 82 | return (info) => { 83 | try { 84 | const result = _.assign({}, root, projector(info)); 85 | logger.debug('Project result', result); 86 | return result; 87 | } catch (e) { 88 | /* istanbul ignore next */ 89 | logger.error('Projecting', e); 90 | /* istanbul ignore next */ 91 | return undefined; 92 | } 93 | }; 94 | }; 95 | 96 | module.exports = { 97 | makePrefix, 98 | typeFunc, 99 | fieldFunc, 100 | makeProjection, 101 | genProjection, 102 | }; 103 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "env": { 4 | "node": true, 5 | "jest": true, 6 | "es6": true 7 | }, 8 | "parserOptions": { 9 | "ecmaVersion": 2018, 10 | "sourceType": "module" 11 | }, 12 | "plugins": [ 13 | "lodash-fp" 14 | ], 15 | "rules": { 16 | "arrow-parens": [ 17 | "error", 18 | "always" 19 | ], 20 | "arrow-body-style": [ 21 | "error", 22 | "as-needed" 23 | ], 24 | "class-methods-use-this": "error", 25 | "comma-dangle": [ 26 | "error", 27 | { 28 | "arrays": "always-multiline", 29 | "objects": "always-multiline", 30 | "imports": "always-multiline", 31 | "exports": "always-multiline", 32 | "functions": "always-multiline" 33 | } 34 | ], 35 | "function-paren-newline": [ 36 | "error", 37 | "consistent" 38 | ], 39 | "import/newline-after-import": "error", 40 | "import/no-dynamic-require": "error", 41 | "import/no-extraneous-dependencies": [ 42 | "error", 43 | { 44 | "devDependencies": true, 45 | "optionalDependencies": true, 46 | "peerDependencies": true 47 | } 48 | ], 49 | "import/no-named-as-default": "off", 50 | "import/no-unresolved": "error", 51 | "import/no-webpack-loader-syntax": "error", 52 | "import/prefer-default-export": "off", 53 | "indent": [ 54 | "error", 55 | 2, 56 | { 57 | "SwitchCase": 1 58 | } 59 | ], 60 | "lodash-fp/consistent-compose": "off", 61 | "lodash-fp/consistent-name": [ 62 | "warn", 63 | "_" 64 | ], 65 | "lodash-fp/no-argumentless-calls": "error", 66 | "lodash-fp/no-chain": "warn", 67 | "lodash-fp/no-extraneous-args": "error", 68 | "lodash-fp/no-extraneous-function-wrapping": "error", 69 | "lodash-fp/no-extraneous-iteratee-args": "error", 70 | "lodash-fp/no-for-each": "off", 71 | "lodash-fp/no-partial-of-curried": "error", 72 | "lodash-fp/no-single-composition": "error", 73 | "lodash-fp/no-submodule-destructuring": "error", 74 | "lodash-fp/no-unused-result": "error", 75 | "lodash-fp/prefer-compact": "error", 76 | "lodash-fp/prefer-composition-grouping": "error", 77 | "lodash-fp/prefer-constant": [ 78 | "error", 79 | { 80 | "arrowFunctions": false 81 | } 82 | ], 83 | "lodash-fp/prefer-flat-map": "error", 84 | "lodash-fp/prefer-get": "error", 85 | "lodash-fp/prefer-identity": [ 86 | "error", 87 | { 88 | "arrowFunctions": false 89 | } 90 | ], 91 | "lodash-fp/preferred-alias": "off", 92 | "lodash-fp/use-fp": "off", 93 | "max-len": [ 94 | "warn", 95 | { 96 | "code": 120, 97 | "tabWidth": 2, 98 | "ignoreComments": false, 99 | "ignoreTrailingComments": false, 100 | "ignoreUrls": true, 101 | "ignoreStrings": true, 102 | "ignoreTemplateLiterals": true, 103 | "ignoreRegExpLiterals": true 104 | } 105 | ], 106 | "newline-per-chained-call": [ 107 | "warn", 108 | { 109 | "ignoreChainWithDepth": 3 110 | } 111 | ], 112 | "no-confusing-arrow": "off", 113 | "no-console": "warn", 114 | "no-underscore-dangle": "off", 115 | "no-use-before-define": "error", 116 | "object-curly-newline": [ 117 | "error", 118 | { 119 | "multiline": true, 120 | "consistent": true, 121 | "minProperties": 0 122 | } 123 | ], 124 | "prefer-template": "error", 125 | "require-yield": "warn" 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/schema.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash/fp'); 2 | const logger = require('../logger'); 3 | 4 | function unwindPath(path) { 5 | const result = []; 6 | let p = path; 7 | while (p) { 8 | result.unshift(p.key); 9 | p = p.prev; 10 | } 11 | return result; 12 | } 13 | 14 | /* 15 | * NFA 16 | * 17 | * [0]: Initial 18 | * 19 | * [ 20 | * { 21 | * [EPSILON]: [Number], 22 | * [ANY]: [Number], 23 | * [NOTNUMBER]: [Number], 24 | * [NUMBER]: [Number], 25 | * key: [Number], 26 | * [ACCEPT]: true | false, 27 | * }, 28 | * ] 29 | * 30 | */ 31 | 32 | const EPSILON = Symbol('epsilon'); 33 | const ANY = Symbol('any'); 34 | const NOTNUMBER = Symbol('not-number'); 35 | const NUMBER = Symbol('number'); 36 | const ACCEPT = Symbol('accept'); 37 | 38 | const extend = (NFA) => (states) => { 39 | const extended = new Set(); 40 | const queue = [...states]; 41 | while (queue.length) { 42 | const state = queue.shift(); 43 | extended.add(state); 44 | const cfg = NFA[state]; 45 | if (cfg[EPSILON]) { 46 | queue.push(...cfg[EPSILON]); 47 | } 48 | } 49 | return [...extended]; 50 | }; 51 | 52 | const run = (NFA) => _.compose( 53 | _.some((state) => NFA[state][ACCEPT]), 54 | _.compose( 55 | _.reduce((states, char) => _.compose( 56 | extend(NFA), 57 | _.reduce((next, state) => { 58 | const cfg = NFA[state]; 59 | const mer = (k) => { 60 | if (cfg[k]) { 61 | next.add(...cfg[k]); 62 | } 63 | }; 64 | mer(ANY); 65 | if (_.isNumber(char)) { 66 | mer(NUMBER); 67 | } else { 68 | mer(NOTNUMBER); 69 | } 70 | mer(char); 71 | return next; 72 | })(new Set()), 73 | )(states)), 74 | extend(NFA), 75 | )(new Set([0])), 76 | ); 77 | 78 | const append = (obj, key, val) => { 79 | if (obj[key]) { 80 | obj[key].push(val); 81 | } else { 82 | // eslint-disable-next-line no-param-reassign 83 | obj[key] = [val]; 84 | } 85 | }; 86 | 87 | const appendAny = (NFA) => { 88 | const len = NFA.length; 89 | append(NFA[len - 1], EPSILON, len); 90 | NFA.push({ [ANY]: [len] }); 91 | }; 92 | 93 | const appendExact = (NFA, str) => { 94 | const len = NFA.length; 95 | append(NFA[len - 1], str, len); 96 | NFA.push({ [NUMBER]: [len] }); 97 | }; 98 | 99 | const appendExactOrNothing = (NFA, str) => { 100 | const len = NFA.length; 101 | append(NFA[len - 1], str, len); 102 | append(NFA[len - 1], EPSILON, len + 1); 103 | NFA.push({ [NUMBER]: [len], [EPSILON]: [len + 1] }); 104 | NFA.push({}); 105 | }; 106 | 107 | const matchSchema = (cfg) => { 108 | const NFA = _.reduce((n, c) => { 109 | if (c === null) { 110 | appendAny(n); 111 | } else if (c === '') { 112 | appendExact(n, NOTNUMBER); 113 | } else if (c === '?') { 114 | appendExactOrNothing(n, NOTNUMBER); 115 | } else if (c.endsWith('?')) { 116 | appendExactOrNothing(n, c.substr(0, c.length - 1)); 117 | } else { 118 | appendExact(n, c); 119 | } 120 | return n; 121 | })([{}])(cfg); 122 | NFA[NFA.length - 1][ACCEPT] = true; 123 | logger.info('NFA', NFA); 124 | return run(NFA); 125 | }; 126 | 127 | const matchSchemas = _.compose( 128 | _.overSome, 129 | _.map(matchSchema), 130 | ); 131 | 132 | const pickType = (config) => { 133 | if (!_.isArray(config)) { 134 | return _.constant(config); 135 | } 136 | const matchers = _.compose( 137 | _.over, 138 | _.map(_.compose( 139 | matchSchemas, 140 | _.get('0'), 141 | )), 142 | )(config); 143 | return (info) => { 144 | const id = _.compose( 145 | _.findIndex(_.identity), 146 | matchers, 147 | unwindPath, 148 | _.get('path'), 149 | )(info); 150 | if (id === -1) { 151 | return {}; 152 | } 153 | return config[id][1]; 154 | }; 155 | }; 156 | 157 | module.exports = { 158 | unwindPath, 159 | append, 160 | matchSchema, 161 | pickType, 162 | }; 163 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graphql-advanced-projection 2 | 3 | [![npm](https://img.shields.io/npm/v/graphql-advanced-projection.svg?style=flat-square)](https://www.npmjs.com/package/graphql-advanced-projection) 4 | [![npm](https://img.shields.io/npm/dt/graphql-advanced-projection.svg?style=flat-square)](https://www.npmjs.com/package/graphql-advanced-projection) 5 | [![GitHub last commit](https://img.shields.io/github/last-commit/b1f6c1c4/graphql-advanced-projection.svg?style=flat-square)](https://github.com/b1f6c1c4/graphql-advanced-projection) 6 | [![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/b1f6c1c4/graphql-advanced-projection.svg?style=flat-square)](https://github.com/b1f6c1c4/graphql-advanced-projection) 7 | [![license](https://img.shields.io/github/license/b1f6c1c4/graphql-advanced-projection.svg?style=flat-square)](https://github.com/b1f6c1c4/graphql-advanced-projection/blob/master/LICENSE.md) 8 | 9 | [![Appveyor Build](https://img.shields.io/appveyor/build/b1f6c1c4/graphql-advanced-projection?style=flat-square)](https://ci.appveyor.com/project/b1f6c1c4/graphql-advanced-projection) 10 | [![Coveralls](https://img.shields.io/coveralls/github/b1f6c1c4/graphql-advanced-projection.svg?style=flat-square)](https://coveralls.io/github/b1f6c1c4/graphql-advanced-projection) 11 | 12 | > Fully customizable Mongoose/MongoDB projection generator. 13 | 14 | ## Why 15 | 16 | We already have [graphql-projection](https://github.com/bharley/graphql-projection), [graphql-mongodb-projection](https://github.com/du5rte/graphql-mongodb-projection), [graphql-db-projection](https://github.com/markshapiro/graphql-db-projection), and [graphql-fields-projection](https://github.com/Impact-Technical-Resources/graphql-fields-projection). 17 | But `graphql-advanced-projection` is different from all of them above in the following ways: 18 | * **Separete graphql schema and mongodb projection config.** This helps you decouple schema and mongodb into two parts, each of them may change independently. Write graphql in `.graphql`, write config in javascript or `.json`. 19 | * **Easy customization.** No more `gqlField: { type: new GraphQLNonNull(GraphQLInt), projection: 'mongoField' }`. Simply `gqlField: 'mongoField'`. 20 | * **We create resolvers.** `gqlField: (parent) => parent.mongoField` can be automatically generated, even complicated ones like `first: (parent) => parent.items.data[0].value`. 21 | * **Fully supports interfaces, fragments, and inline fragments.** Write `typeProj: 'type'` and `switch (parent.type)` in `__resolveType`. 22 | 23 | ## Installation 24 | 25 | ```sh 26 | $ npm i graphql-advanced-projection 27 | ``` 28 | ## Usage 29 | 30 | > For a complete working demo, see the `examples` folder. 31 | 32 | ### Setup `mongoose` 33 | 34 | ```js 35 | const UserSchema = new mongoose.Schema({ 36 | _id: String, 37 | mongoA: String, 38 | }); 39 | const User = mongoose.model('users', UserSchema); 40 | ``` 41 | 42 | ### Setup `graphql` 43 | 44 | ```graphql 45 | type Query { 46 | user(id: ID!): User 47 | } 48 | type User { 49 | userId: ID 50 | field1: String 51 | field2: String 52 | } 53 | ``` 54 | 55 | ### Setup `graphql-advanced-projection` 56 | 57 | ```js 58 | const { project, resolvers } = gqlProjection({ 59 | User: { 60 | proj: { 61 | userId: '_id', 62 | field1: 'mongoA', 63 | field2: null, 64 | }, 65 | }, 66 | }); 67 | ``` 68 | 69 | ### Combine everything together 70 | 71 | ```js 72 | const { makeExecutableSchema } = require('@graphql-tools/schema'); 73 | 74 | /* ... */ 75 | 76 | module.exports = makeExecutableSchema({ 77 | typeDefs, 78 | resolvers: _.merge(resolvers, { 79 | Query: { 80 | async user(parent, args, context, info) { 81 | const proj = project(info); 82 | const result = await User.findById(args.id, proj); 83 | return result.toObject(); 84 | }, 85 | }, 86 | User: { 87 | field2: () => 'Hello World', 88 | }, 89 | }), 90 | resolverValidationOptions: { requireResolversForResolveType: false }, 91 | }); 92 | ``` 93 | 94 | ### Run 95 | 96 | ```graphql 97 | query { 98 | user(id: $id) { 99 | field1 100 | field2 101 | } 102 | } 103 | ``` 104 | ```js 105 | proj = { 106 | _id: 0, 107 | mongoA: 1, 108 | } 109 | ``` 110 | 111 | ## License 112 | 113 | MIT 114 | -------------------------------------------------------------------------------- /tests/prepareConfig.test.js: -------------------------------------------------------------------------------- 1 | const { 2 | prepareProjectionConfig, 3 | prepareSchemaConfig, 4 | prepareConfig, 5 | } = require('../src/prepareConfig'); 6 | 7 | describe('prepareProjectionConfig', () => { 8 | it('should accept undefined', () => { 9 | const result = prepareProjectionConfig(undefined, 'fn'); 10 | expect(result.query).toEqual('fn'); 11 | expect(result.select).toEqual(undefined); 12 | expect(result.recursive).toBeFalsy(); 13 | expect(result.prefix).toBeUndefined(); 14 | }); 15 | 16 | it('should accept null', () => { 17 | const result = prepareProjectionConfig(null, 'fn'); 18 | expect(result.query).toEqual(null); 19 | expect(result.select).toEqual(undefined); 20 | expect(result.recursive).toBeFalsy(); 21 | expect(result.prefix).toBeUndefined(); 22 | }); 23 | 24 | it('should accept true', () => { 25 | const result = prepareProjectionConfig(true, 'fn'); 26 | expect(result.query).toEqual(null); 27 | expect(result.select).toEqual(undefined); 28 | expect(result.recursive).toBeTruthy(); 29 | expect(result.prefix).toBeUndefined(); 30 | }); 31 | 32 | it('should accept string', () => { 33 | const result = prepareProjectionConfig('str', 'fn'); 34 | expect(result.query).toEqual('str'); 35 | expect(result.select).toEqual('str'); 36 | expect(result.recursive).toBeFalsy(); 37 | expect(result.prefix).toBeUndefined(); 38 | }); 39 | 40 | it('should accept recursive string', () => { 41 | const result = prepareProjectionConfig('str.', 'fn'); 42 | expect(result.query).toEqual(null); 43 | expect(result.select).toEqual('str'); 44 | expect(result.recursive).toBeTruthy(); 45 | expect(result.prefix).toEqual('str.'); 46 | }); 47 | 48 | it('should accept recursive string', () => { 49 | const result = prepareProjectionConfig('str', 'fn'); 50 | expect(result.query).toEqual('str'); 51 | expect(result.select).toEqual('str'); 52 | expect(result.recursive).toBeFalsy(); 53 | expect(result.prefix).toBeUndefined(); 54 | }); 55 | 56 | it('should accept array', () => { 57 | const result = prepareProjectionConfig(['a', 'b'], 'fn'); 58 | expect(result.query).toEqual(['a', 'b']); 59 | expect(result.select).toEqual(undefined); 60 | expect(result.recursive).toBeFalsy(); 61 | expect(result.prefix).toBeUndefined(); 62 | }); 63 | 64 | it('should accept object 1', () => { 65 | const result = prepareProjectionConfig({ 66 | query: 'q', 67 | select: 's', 68 | recursive: 0, 69 | }, 'fn'); 70 | expect(result.query).toEqual('q'); 71 | expect(result.select).toEqual('s'); 72 | expect(result.recursive).toBeFalsy(); 73 | expect(result.prefix).toBeUndefined(); 74 | }); 75 | 76 | it('should accept object 2', () => { 77 | const result = prepareProjectionConfig({ 78 | query: ['a', 'b'], 79 | recursive: true, 80 | prefix: 'xxx', 81 | }, 'fn'); 82 | expect(result.query).toEqual(['a', 'b']); 83 | expect(result.select).toEqual(undefined); 84 | expect(result.recursive).toBeTruthy(); 85 | expect(result.prefix).toEqual('xxx'); 86 | }); 87 | 88 | it('should accept object 3', () => { 89 | const result = prepareProjectionConfig({ 90 | recursive: true, 91 | }, 'fn'); 92 | expect(result.query).toEqual('fn'); 93 | expect(result.select).toEqual(undefined); 94 | expect(result.recursive).toBeTruthy(); 95 | expect(result.prefix).toBeUndefined(); 96 | }); 97 | }); 98 | 99 | describe('prepareSchemaConfig', () => { 100 | it('should accept object', () => { 101 | expect(prepareSchemaConfig({ obj: true })).toEqual({ obj: true }); 102 | }); 103 | 104 | it('should accept missing', () => { 105 | // eslint-disable-next-line no-sparse-arrays 106 | expect(prepareSchemaConfig([[, { k: 1 }]])).toEqual([ 107 | [[[null]], { k: 1 }], 108 | ]); 109 | }); 110 | 111 | it('should accept undefined', () => { 112 | expect(prepareSchemaConfig([[undefined, { k: 1 }]])).toEqual([ 113 | [[[null]], { k: 1 }], 114 | ]); 115 | }); 116 | 117 | it('should accept null', () => { 118 | expect(prepareSchemaConfig([[null, { k: 1 }]])).toEqual([ 119 | [[], { k: 1 }], 120 | ]); 121 | }); 122 | 123 | it('should accept string', () => { 124 | expect(prepareSchemaConfig([['aa', { k: 1 }]])).toEqual([ 125 | [[['aa', null]], { k: 1 }], 126 | ]); 127 | }); 128 | 129 | it('should accept array', () => { 130 | expect(prepareSchemaConfig([[['a', 'b'], { k: 1 }]])).toEqual([ 131 | [[['a', 'b']], { k: 1 }], 132 | ]); 133 | }); 134 | 135 | it('should accept regular', () => { 136 | expect(prepareSchemaConfig([[[['c']], { k: 1 }]])).toEqual([ 137 | [[['c']], { k: 1 }], 138 | ]); 139 | }); 140 | 141 | it('should throw wrong', () => { 142 | expect(() => prepareSchemaConfig([[{}, {}]])).toThrow(); 143 | }); 144 | }); 145 | 146 | describe('prepareConfig', () => { 147 | it('should handle undefined', () => { 148 | const { root, config, pick } = prepareConfig(); 149 | expect(root).toEqual({ _id: 0 }); 150 | expect(config).toEqual({}); 151 | expect(pick).toEqual({}); 152 | }); 153 | 154 | it('should handle simple', () => { 155 | const { root, config, pick } = prepareConfig({ 156 | root: {}, 157 | Obj: { 158 | prefix: 'x', 159 | proj: { 160 | a: 'b', 161 | c: {}, 162 | }, 163 | }, 164 | }); 165 | expect(root).toEqual({}); 166 | expect(config).toEqual({ 167 | Obj: { 168 | prefix: 'x', 169 | proj: { 170 | a: { 171 | query: 'b', 172 | select: 'b', 173 | }, 174 | c: { 175 | query: 'c', 176 | }, 177 | }, 178 | }, 179 | }); 180 | expect(pick.Obj({})).toEqual({ 181 | prefix: 'x', 182 | proj: { 183 | a: { query: 'b', select: 'b' }, 184 | c: { query: 'c' }, 185 | }, 186 | }); 187 | }); 188 | }); 189 | -------------------------------------------------------------------------------- /tests/population.test.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash/fp'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const { graphql } = require('graphql'); 5 | const { makeExecutableSchema } = require('@graphql-tools/schema'); 6 | const { prepareConfig } = require('../src/prepareConfig'); 7 | const { makePopulation, genPopulation } = require('../src/population'); 8 | 9 | describe('makePopulation', () => { 10 | const typeDefs = fs.readFileSync(path.join(__dirname, 'schema.graphql'), 'utf-8'); 11 | 12 | const run = (config, source) => new Promise((resolve, reject) => { 13 | const pick = _.mapValues(_.constant)(config); 14 | const go = (info) => { 15 | try { 16 | const proj = makePopulation({ root: { _id: 0 }, pick })(info); 17 | resolve(proj); 18 | } catch (e) { 19 | reject(e); 20 | } 21 | }; 22 | graphql({ 23 | schema: makeExecutableSchema({ 24 | typeDefs, 25 | resolvers: { 26 | Query: { 27 | obj: (parent, args, context, info) => { 28 | go(info); 29 | }, 30 | evil: (parent, args, context, info) => { 31 | go(info); 32 | }, 33 | }, 34 | }, 35 | resolverValidationOptions: { requireResolversForResolveType: false }, 36 | }), 37 | source, 38 | }).then((res) => { 39 | if (res.errors) { 40 | throw res.errors; 41 | } 42 | }); 43 | }); 44 | 45 | it('should project default when not configured', async () => { 46 | expect(await run({}, '{ obj { field1 } }')).toEqual({ 47 | path: '', 48 | select: { field1: 1 }, 49 | }); 50 | }); 51 | 52 | it('should project query simple', () => { 53 | expect.hasAssertions(); 54 | return expect(run({ 55 | Obj: { 56 | prefix: 'wrap.', 57 | proj: { 58 | field1: { 59 | query: 'value', 60 | }, 61 | }, 62 | }, 63 | }, '{ obj { field1 } }')).resolves.toEqual({ 64 | path: '', 65 | select: { 'wrap.value': 1 }, 66 | }); 67 | }); 68 | 69 | it('should populate not recursive', () => { 70 | expect.hasAssertions(); 71 | return expect(run({ 72 | Obj: { 73 | prefix: 'wrap.', 74 | proj: { 75 | field2: { query: 'field2' }, 76 | }, 77 | }, 78 | Foo: { 79 | prefix: 'wrap2.', 80 | proj: { 81 | f1: { query: 'foo' }, 82 | }, 83 | }, 84 | }, '{ obj { field2 { f1 } } }')).resolves.toEqual({ 85 | path: '', 86 | select: { 87 | 'wrap.field2': 1, 88 | }, 89 | populate: [{ 90 | path: 'wrap.field2', 91 | select: { _id: 0, 'wrap2.foo': 1 }, 92 | }], 93 | }); 94 | }); 95 | 96 | it('should project recursive', () => { 97 | expect.hasAssertions(); 98 | return expect(run({ 99 | Obj: { 100 | prefix: 'wrap.', 101 | proj: { 102 | field2: { 103 | query: ['evil', 'evils'], 104 | recursive: true, 105 | }, 106 | }, 107 | }, 108 | }, '{ obj { field2 { f1 } } }')).resolves.toEqual({ 109 | path: '', 110 | select: { 111 | 'wrap.evil': 1, 112 | 'wrap.evils': 1, 113 | 'wrap.field2.f1': 1, 114 | }, 115 | }); 116 | }); 117 | 118 | it('should throw not recursive', () => { 119 | expect.hasAssertions(); 120 | return expect(run({ 121 | Obj: { 122 | prefix: 'wrap.', 123 | proj: { 124 | field2: { 125 | query: ['evil', 'evils'], 126 | recursive: false, 127 | }, 128 | }, 129 | }, 130 | }, '{ obj { field2 { f1 } } }')).rejects.toBeInstanceOf(Error); 131 | }); 132 | 133 | it('should project deep inline fragment with typeCondition', () => { 134 | expect.hasAssertions(); 135 | return expect(run({ 136 | Obj: { 137 | proj: { 138 | field3: { 139 | query: null, 140 | recursive: true, 141 | prefix: 'evil.', 142 | }, 143 | }, 144 | }, 145 | Father: { 146 | prefix: 'wrap.', 147 | typeProj: 'type', 148 | proj: { 149 | g0: { query: 'value' }, 150 | }, 151 | }, 152 | Child: { 153 | prefix: 'wrap2.', 154 | proj: { 155 | g1: { query: 'value2' }, 156 | }, 157 | }, 158 | }, `{ 159 | obj { 160 | field3 { 161 | g0 162 | ... on Child { 163 | ... { 164 | ... on Child { 165 | g1 166 | } 167 | } 168 | } 169 | } 170 | } 171 | }`)).resolves.toEqual({ 172 | path: '', 173 | select: { 174 | 'evil.wrap.type': 1, 175 | 'evil.wrap.value': 1, 176 | 'evil.wrap.wrap2.value2': 1, 177 | }, 178 | }); 179 | }); 180 | 181 | it('should lookup simple', () => { 182 | expect.hasAssertions(); 183 | return expect(run({ 184 | Obj: { 185 | prefix: 'wrap.', 186 | proj: { 187 | field2: { 188 | query: 'q', 189 | }, 190 | field3: { 191 | query: 'p', 192 | }, 193 | }, 194 | }, 195 | Father: { 196 | prefix: 'fthr.', 197 | proj: { 198 | g0: { 199 | query: 'tt', 200 | }, 201 | }, 202 | }, 203 | }, '{ obj { field1 field2 { f1 } field3 { g0 } } }')).resolves.toEqual({ 204 | path: '', 205 | select: { 206 | 'wrap.field1': 1, 207 | 'wrap.q': 1, 208 | 'wrap.p': 1, 209 | }, 210 | populate: [{ 211 | path: 'wrap.q', 212 | select: { _id: 0, f1: 1 }, 213 | }, { 214 | path: 'wrap.p', 215 | select: { _id: 0, 'fthr.tt': 1 }, 216 | }], 217 | }); 218 | }); 219 | 220 | it('should lookup evil', () => { 221 | expect.hasAssertions(); 222 | return expect(run({ 223 | Evil: { 224 | prefix: 'wrap.', 225 | proj: { 226 | self: { 227 | query: 'q', 228 | }, 229 | }, 230 | }, 231 | }, '{ evil { self { self { field } } } }')).resolves.toEqual({ 232 | path: '', 233 | select: { 234 | 'wrap.q': 1, 235 | }, 236 | populate: [{ 237 | path: 'wrap.q', 238 | select: { _id: 0, 'wrap.q': 1 }, 239 | populate: [{ 240 | path: 'wrap.q', 241 | select: { _id: 0, 'wrap.field': 1 }, 242 | }], 243 | }], 244 | }); 245 | }); 246 | }); 247 | 248 | describe('genPopulation', () => { 249 | const typeDefs = fs.readFileSync(path.join(__dirname, 'schema.graphql'), 'utf-8'); 250 | 251 | const run = (config, source) => new Promise((resolve, reject) => { 252 | const go = (info) => { 253 | try { 254 | const proj = genPopulation(prepareConfig(config)); 255 | resolve(proj(info)); 256 | } catch (e) { 257 | reject(e); 258 | } 259 | }; 260 | graphql({ 261 | schema: makeExecutableSchema({ 262 | typeDefs, 263 | resolvers: { 264 | Query: { 265 | obj: (parent, args, context, info) => { 266 | go(info); 267 | }, 268 | evil: (parent, args, context, info) => { 269 | go(info); 270 | }, 271 | }, 272 | }, 273 | resolverValidationOptions: { requireResolversForResolveType: false }, 274 | }), 275 | source, 276 | }).then((res) => { 277 | if (res.errors) { 278 | throw res.errors; 279 | } 280 | }); 281 | }); 282 | 283 | it('should project simple', () => { 284 | expect.hasAssertions(); 285 | return expect(run({ root: { _id: 0 }, Obj: { proj: { field1: 'a' } } }, '{ obj { field1 } }')).resolves.toBeUndefined(); 286 | }); 287 | 288 | it('should lookup simple', () => { 289 | expect.hasAssertions(); 290 | return expect(run({ 291 | root: { _id: 0 }, 292 | Obj: { 293 | proj: { 294 | field2: { query: 'xx' }, 295 | }, 296 | }, 297 | }, '{ obj { field2 { f1 } } }')).resolves.toEqual([{ 298 | path: 'xx', 299 | select: { _id: 0, f1: 1 }, 300 | }]); 301 | }); 302 | }); 303 | -------------------------------------------------------------------------------- /docs/API.md: -------------------------------------------------------------------------------- 1 | # Terms 2 | 3 | ## Path 4 | A dot-separated string that is to be projected. Used by [Lodash](https://lodash.com/docs/4.17.21#get) and [MongoDB](https://docs.mongodb.com/manual/core/document/#document-dot-notation). 5 | 6 | Example: 7 | - `'_id'` - the most common path. 8 | - `'items.0.value'` - get a value in an array. 9 | 10 | # Exported functions 11 | 12 | The default exported function is `gqlProjection: (config) => { project, populator, resolvers }`: 13 | 14 | - `project: (info) => proj` 15 | - `info` is the 4th argument of a resolver function. 16 | - `proj` SHOULD be used as [Mongoose projection option](https://mongoosejs.com/docs/api.html#query_Query-select). 17 | - `proj` is `undefined` if error occured. 18 | - `populator: (info) => popu` 19 | - `popu` SHOULD be used as [Mongoose population option](http://mongoosejs.com/docs/populate.html#query-conditions). 20 | - `resolvers` is of valid GraphQL resolver format. SHOULD be used with [`graphql-tools/makeExecutableSchema`](https://github.com/apollographql/graphql-tools). 21 | 22 | # The config object 23 | 24 | All capitalized keys are [Schema config](#schema-config). 25 | Others are global settings, including: 26 | 27 | - `root: Object` - Base projection. 28 | - If undefined, `{ _id: 0 }` will be used. 29 | 30 | Example: 31 | ```js 32 | { 33 | root: {}, // global setting 34 | User: {}, // schema config for GraphQL type User 35 | TypeA: {}, // schema config for GraphQL type TypeA 36 | } 37 | ``` 38 | 39 | # Schema config 40 | 41 | Each GraphQL type corresponds to a schema config, as specified above. 42 | During GraphQL resolution phase, each time such GraphQL type resolves, 43 | a type config corresponds to that GraphQL type is selected. 44 | The type config is further used to create projection, population, and resolvers. 45 | 46 | The rule for deciding type config from schema config is as follow. 47 | 48 | - If the schema config is an object, then itself is treated as the type config for the GraphQL type in all invocations. 49 | - If the schema config is an array of pairs (each is of a [Match config](#match-config) and a [Type config](#type-config)), 50 | then, for a particular invocation, the first matching pair's type config is used. 51 | - If none of them matches, type config `{}` is used. 52 | - Otherwise, the schema config is illegal. 53 | 54 | ## Example for deciding type config out of a schema config 55 | 56 | ```js 57 | { 58 | // Use obj as type config for all invocation of GraphQL type User 59 | // This is the common case and handles 99% use cases. 60 | User: obj, 61 | 62 | // Use different type configs for different invocation of GraphQL type Item 63 | // Rarely do we need to distinguish different invocation of the same GraphQL type 64 | Item: [ 65 | ['x', obj0], // Use obj0 when the GraphQL invocation tree starts with x (see below) 66 | [[['x', null]], obj1], // Use obj1 when the GraphQL invocation tree starts with x 67 | [[[null, 'y']], obj2], // Use obj2 when the GraphQL invocation tree ends with y 68 | [[[null, 'z', null]], obj3], // Use obj3 when the GraphQL invocation tree contains z 69 | [[['']], obj4], // Use obj4 when the GraphQL invocation tree of length 1 70 | [[['', '?']], obj4], // Use obj4 when the GraphQL invocation tree of length <= 2 71 | [[['x?']], obj1], // Use obj1 when the GraphQL invocation tree with exactly x or nothing 72 | // Use {} by default 73 | ], 74 | } 75 | ``` 76 | 77 | ## Match config 78 | 79 | - If it's undefined or missing, then it's equivalent to `[[null]]`. 80 | - If it's `null`, then it's equivalent to `[]`. 81 | - If it's a string `str`, then it's equivalent to `[[str, null]]`. 82 | - If it's an array of string `arr`, then it's equivalent to `[arr]`. 83 | - Otherwise, it MUST be of `[[null | String]]`: 84 | - The whole config matches if and only if at least one `[null | String]` matches the path. 85 | - `null` can match zero, one, or more path items. 86 | - `''` can match one path item (not numeric) and following numeric keys. 87 | - `'?'` can match nothing or (one path item (not numeric) and following numeric keys). 88 | - A string with suffix `'?'` can match nothing or (one path item (exactly match its key) and following numeric keys). 89 | - Otherwise, a string can match one path item (exactly match its key) and following numeric keys. 90 | 91 | # Type config 92 | 93 | A type config is an object with the following keys (all optional): 94 | 95 | - `prefix: Path` - all `Path` projected by the type config object will be _literally_ prefixed with it. 96 | - If you expect a path separator, add **manually** in `prefix`. 97 | - If it starts with `'.'`, then previous prefixes are cleared. 98 | - `typeProj: Path | [Path]` - always project Path(s) when the type is inquired. This SHOULD be used to retrieve the type information. 99 | - `proj: Object` - project Path(s) based on what the client asks for. 100 | - Each key MUST match a GraphQL field of the type. 101 | - The corresponded value MUST be a [Projection config](#projection-config). 102 | 103 | Example: 104 | ```js 105 | { 106 | prefix: '', // don't prefix mongodb fields 107 | typeProj: 'type', // mongodb field(s) that stores type information 108 | proj: { 109 | // GraphQL field id corresponds to mongodb field _id, we project and resolve for you 110 | id: '_id', 111 | 112 | // GraphQL field f1 corresponds to mongodb field f1, GraphQL handles such case very well 113 | // f1: undefined, 114 | 115 | // GraphQL field f2 (which may contain subfields) 116 | // corresponds to mongodb field f2 (which stores an array of documents) 117 | // We project and resolve recursively into sub-documents for you 118 | f2: true, 119 | 120 | // GraphQL field f3 (which may contain subfields) 121 | // corresponds to mongodb field sub (which stores an array of documents) 122 | // We project and resolve recursively into sub-documents for you 123 | f3: 'sub.', 124 | 125 | // GraphQL field f4 is not stored in mongodb at all 126 | // You SHOULD write your own resolver for this 127 | f4: null, 128 | 129 | // GraphQL field f5 corresponds to mongodb field cus 130 | // You want customize the resolver part but leave the projection part untouched. 131 | // You SHOULD write your own resolver for this 132 | // Note: this is useful for you to make additional db queries to get referenced documents! 133 | f5: { query: 'cus' }, 134 | }, 135 | } 136 | ``` 137 | 138 | ## Projection config 139 | 140 | - If it's `undefined`, then it's equivalent to `{}`. 141 | - If it's `null`, then it's equivalent to `{ query: null }`. 142 | - If it's `true`, then it's equivalent to `{ query: null, recursive: true }`. 143 | - If it's a string: 144 | - If it matches `/^(?.*)\.$/`, then it's equivalent to `{ query: null, select: str, recursive: true, prefix: str + '.' }`. 145 | - Otherwise, it's equivalent to `{ query: str, select: str }`. 146 | - If it's an array `arr`, then it's equivalent to `{ query: arr }`. 147 | - Otherwise, it MUST be an object and MAY contain the following keys: 148 | - `query: null | Path | [Path]` - Path(s) to project when the field is inquired. 149 | - If undefined, project the field name. 150 | - If `null`, project nothing. 151 | - `select: Path` - Generate a resolver for the type that maps the Path to the field. 152 | - If undefined, don't generate. 153 | - Note: GraphQL will natively resolve `fieldName: (parent) => parent.fieldName`. Thus if `select` exactly matches the field name, leave it undefined. 154 | - `recursive: Boolean` - (default `false`) Project the fields of the return type altogether. For `genProjection`, SHOULD be used with `query: null`; for `genPopulation`, MUST be used with `query: string`. 155 | - It SHOULD be `true` if a single MongoDB query can get all the information. 156 | - It SHOULD be `false` if a separate query is needed to obtain extra information. 157 | - `prefix: null | Path` - (ignored except `recursive: true`) Each `Path` projected by the return type is _literally_ prefixed by it. 158 | - If undefined, prefix the field name and `'.'`. 159 | - If `null`, don't prefix. 160 | - If it starts with `'.'`, then previous prefixes (include `prefix` in the type config) are cleared. 161 | - Note: path separator `'.'` will _not_ be appended automatically. Append it manually if you need. 162 | 163 | -------------------------------------------------------------------------------- /tests/schema.test.js: -------------------------------------------------------------------------------- 1 | const { prepareSchemaConfig } = require('../src/prepareConfig'); 2 | const { 3 | unwindPath, 4 | append, 5 | matchSchema, 6 | pickType, 7 | } = require('../src/schema'); 8 | 9 | describe('unwindPath', () => { 10 | it('should unwind', () => { 11 | expect(unwindPath({ 12 | prev: { 13 | prev: undefined, 14 | key: 'k', 15 | }, 16 | key: 'k2', 17 | })).toEqual(['k', 'k2']); 18 | }); 19 | }); 20 | 21 | describe('append', () => { 22 | it('should append exist', () => { 23 | const obj = { a: [1, 2] }; 24 | append(obj, 'a', 3); 25 | expect(obj.a).toEqual([1, 2, 3]); 26 | }); 27 | 28 | it('should append non-exist', () => { 29 | const obj = {}; 30 | append(obj, 'a', 3); 31 | expect(obj.a).toEqual([3]); 32 | }); 33 | }); 34 | 35 | describe('matchSchema', () => { 36 | it('should match empty', () => { 37 | const func = (...args) => matchSchema([])(args); 38 | expect(func()).toEqual(true); 39 | expect(func('a')).toEqual(false); 40 | expect(func('a', 'b')).toEqual(false); 41 | expect(func('a', 0, 1)).toEqual(false); 42 | expect(func('a', 'b', 0, 1)).toEqual(false); 43 | expect(func('a', 'a')).toEqual(false); 44 | expect(func('a', 0, 'a', 1)).toEqual(false); 45 | expect(func(2)).toEqual(false); 46 | expect(func(2, 'a')).toEqual(false); 47 | }); 48 | 49 | it('should match simple', () => { 50 | const func = (...args) => matchSchema(['a'])(args); 51 | expect(func()).toEqual(false); 52 | expect(func('a')).toEqual(true); 53 | expect(func('a', 'b')).toEqual(false); 54 | expect(func('a', 0, 1)).toEqual(true); 55 | expect(func('a', 'b', 0, 1)).toEqual(false); 56 | expect(func('a', 'a')).toEqual(false); 57 | expect(func('a', 0, 'a', 1)).toEqual(false); 58 | expect(func(2)).toEqual(false); 59 | expect(func(2, 'a')).toEqual(false); 60 | }); 61 | 62 | it('should match null', () => { 63 | const func = (...args) => matchSchema([null])(args); 64 | expect(func()).toEqual(true); 65 | expect(func('a')).toEqual(true); 66 | expect(func('a', 'b')).toEqual(true); 67 | expect(func('a', 0, 1)).toEqual(true); 68 | expect(func('a', 'b', 0, 1)).toEqual(true); 69 | expect(func('a', 'a')).toEqual(true); 70 | expect(func('a', 0, 'a', 1)).toEqual(true); 71 | expect(func(2)).toEqual(true); 72 | expect(func(2, 'a')).toEqual(true); 73 | }); 74 | 75 | it('should match null simple', () => { 76 | const func = (...args) => matchSchema([null, 'b'])(args); 77 | expect(func()).toEqual(false); 78 | expect(func('a')).toEqual(false); 79 | expect(func('a', 'b')).toEqual(true); 80 | expect(func('a', 0, 1)).toEqual(false); 81 | expect(func('a', 'b', 0, 1)).toEqual(true); 82 | expect(func('a', 'a')).toEqual(false); 83 | expect(func('a', 0, 'a', 1)).toEqual(false); 84 | expect(func(2)).toEqual(false); 85 | expect(func(2, 'a')).toEqual(false); 86 | }); 87 | 88 | it('should match simple null', () => { 89 | const func = (...args) => matchSchema(['a', null])(args); 90 | expect(func()).toEqual(false); 91 | expect(func('a')).toEqual(true); 92 | expect(func('a', 'b')).toEqual(true); 93 | expect(func('a', 0, 1)).toEqual(true); 94 | expect(func('a', 'b', 0, 1)).toEqual(true); 95 | expect(func('a', 'a')).toEqual(true); 96 | expect(func('a', 0, 'a', 1)).toEqual(true); 97 | expect(func(2)).toEqual(false); 98 | expect(func(2, 'a')).toEqual(false); 99 | }); 100 | 101 | it('should match simple dup', () => { 102 | const func = (...args) => matchSchema(['a', 'a'])(args); 103 | expect(func()).toEqual(false); 104 | expect(func('a')).toEqual(false); 105 | expect(func('a', 'b')).toEqual(false); 106 | expect(func('a', 0, 1)).toEqual(false); 107 | expect(func('a', 'b', 0, 1)).toEqual(false); 108 | expect(func('a', 'a')).toEqual(true); 109 | expect(func('a', 0, 'a', 1)).toEqual(true); 110 | expect(func(2)).toEqual(false); 111 | expect(func(2, 'a')).toEqual(false); 112 | }); 113 | 114 | it('should match simple simple', () => { 115 | const func = (...args) => matchSchema(['a', 'b'])(args); 116 | expect(func()).toEqual(false); 117 | expect(func('a')).toEqual(false); 118 | expect(func('a', 'b')).toEqual(true); 119 | expect(func('a', 0, 1)).toEqual(false); 120 | expect(func('a', 'b', 0, 1)).toEqual(true); 121 | expect(func('a', 'a')).toEqual(false); 122 | expect(func('a', 0, 'a', 1)).toEqual(false); 123 | expect(func(2)).toEqual(false); 124 | expect(func(2, 'a')).toEqual(false); 125 | }); 126 | 127 | it('should match null simple simple', () => { 128 | const func = (...args) => matchSchema([null, 'a', 'b'])(args); 129 | expect(func()).toEqual(false); 130 | expect(func('a')).toEqual(false); 131 | expect(func('a', 'b')).toEqual(true); 132 | expect(func('a', 0, 1)).toEqual(false); 133 | expect(func('a', 'b', 0, 1)).toEqual(true); 134 | expect(func('a', 'a')).toEqual(false); 135 | expect(func('a', 0, 'a', 1)).toEqual(false); 136 | expect(func(2)).toEqual(false); 137 | expect(func(2, 'a')).toEqual(false); 138 | }); 139 | 140 | it('should match simple null simple', () => { 141 | const func = (...args) => matchSchema(['a', null, 'b'])(args); 142 | expect(func()).toEqual(false); 143 | expect(func('a')).toEqual(false); 144 | expect(func('a', 'b')).toEqual(true); 145 | expect(func('a', 0, 1)).toEqual(false); 146 | expect(func('a', 'b', 0, 1)).toEqual(true); 147 | expect(func('a', 'a')).toEqual(false); 148 | expect(func('a', 0, 'a', 1)).toEqual(false); 149 | expect(func(2)).toEqual(false); 150 | expect(func(2, 'a')).toEqual(false); 151 | }); 152 | 153 | it('should match null null', () => { 154 | const func = (...args) => matchSchema([null, null])(args); 155 | expect(func()).toEqual(true); 156 | expect(func('a')).toEqual(true); 157 | expect(func('a', 'b')).toEqual(true); 158 | expect(func('a', 0, 1)).toEqual(true); 159 | expect(func('a', 'b', 0, 1)).toEqual(true); 160 | expect(func('a', 'a')).toEqual(true); 161 | expect(func('a', 0, 'a', 1)).toEqual(true); 162 | expect(func(2)).toEqual(true); 163 | expect(func(2, 'a')).toEqual(true); 164 | }); 165 | 166 | it('should match wrong simple', () => { 167 | const func = (...args) => matchSchema(['b'])(args); 168 | expect(func()).toEqual(false); 169 | expect(func('a')).toEqual(false); 170 | expect(func('a', 'b')).toEqual(false); 171 | expect(func('a', 0, 1)).toEqual(false); 172 | expect(func('a', 'b', 0, 1)).toEqual(false); 173 | expect(func('a', 'a')).toEqual(false); 174 | expect(func('a', 0, 'a', 1)).toEqual(false); 175 | expect(func(2)).toEqual(false); 176 | expect(func(2, 'a')).toEqual(false); 177 | }); 178 | 179 | it('should match empty', () => { 180 | const func = (...args) => matchSchema([''])(args); 181 | expect(func()).toEqual(false); 182 | expect(func('a')).toEqual(true); 183 | expect(func('a', 'b')).toEqual(false); 184 | expect(func('a', 0, 1)).toEqual(true); 185 | expect(func('a', 'b', 0, 1)).toEqual(false); 186 | expect(func('a', 'a')).toEqual(false); 187 | expect(func('a', 0, 'a', 1)).toEqual(false); 188 | expect(func(2)).toEqual(false); 189 | expect(func(2, 'a')).toEqual(false); 190 | }); 191 | 192 | it('should match empty empty', () => { 193 | const func = (...args) => matchSchema(['', ''])(args); 194 | expect(func()).toEqual(false); 195 | expect(func('a')).toEqual(false); 196 | expect(func('a', 'b')).toEqual(true); 197 | expect(func('a', 0, 1)).toEqual(false); 198 | expect(func('a', 'b', 0, 1)).toEqual(true); 199 | expect(func('a', 'a')).toEqual(true); 200 | expect(func('a', 0, 'a', 1)).toEqual(true); 201 | expect(func(2)).toEqual(false); 202 | expect(func(2, 'a')).toEqual(false); 203 | }); 204 | 205 | it('should match opt empty', () => { 206 | const func = (...args) => matchSchema(['?'])(args); 207 | expect(func()).toEqual(true); 208 | expect(func('a')).toEqual(true); 209 | expect(func('a', 'b')).toEqual(false); 210 | expect(func('a', 0, 1)).toEqual(true); 211 | expect(func('a', 'b', 0, 1)).toEqual(false); 212 | expect(func('a', 'a')).toEqual(false); 213 | expect(func('a', 0, 'a', 1)).toEqual(false); 214 | expect(func(2)).toEqual(false); 215 | expect(func(2, 'a')).toEqual(false); 216 | }); 217 | 218 | it('should match opt', () => { 219 | const func = (...args) => matchSchema(['a?', 'a'])(args); 220 | expect(func()).toEqual(false); 221 | expect(func('a')).toEqual(true); 222 | expect(func('a', 'b')).toEqual(false); 223 | expect(func('a', 0, 1)).toEqual(true); 224 | expect(func('a', 'b', 0, 1)).toEqual(false); 225 | expect(func('a', 'a')).toEqual(true); 226 | expect(func('a', 0, 'a', 1)).toEqual(true); 227 | expect(func(2)).toEqual(false); 228 | expect(func(2, 'a')).toEqual(false); 229 | }); 230 | }); 231 | 232 | describe('pickType', () => { 233 | it('should pick object', () => { 234 | const config = prepareSchemaConfig({ k: 1 }); 235 | expect(pickType(config)()).toEqual(config); 236 | }); 237 | 238 | it('should pick simple', () => { 239 | const config = prepareSchemaConfig([ 240 | ['a', { k: 1 }], 241 | ['b', { k: 2 }], 242 | ]); 243 | expect(pickType(config)({ path: { key: 'a' } })).toEqual(config[0][1]); 244 | }); 245 | 246 | it('should pick dup', () => { 247 | const config = prepareSchemaConfig([ 248 | ['a', { k: 1 }], 249 | ['a', { k: 2 }], 250 | ]); 251 | expect(pickType(config)({ path: { key: 'a' } })).toEqual(config[0][1]); 252 | }); 253 | 254 | it('should pick default', () => { 255 | const config = prepareSchemaConfig([ 256 | ['a', { k: 1 }], 257 | ['b', { k: 2 }], 258 | ]); 259 | expect(pickType(config)({ path: { key: 'c' } })).toEqual({}); 260 | }); 261 | 262 | it('should pick either', () => { 263 | const config = prepareSchemaConfig([ 264 | [[['c'], ['a']], { k: 1 }], 265 | ['a', { k: 2 }], 266 | ]); 267 | expect(pickType(config)({ path: { key: 'a' } })).toEqual(config[0][1]); 268 | }); 269 | 270 | it('should pick empty', () => { 271 | const config = prepareSchemaConfig([]); 272 | expect(pickType(config)({ path: { key: 'a' } })).toEqual({}); 273 | }); 274 | }); 275 | -------------------------------------------------------------------------------- /tests/projection.test.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash/fp'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const { graphql } = require('graphql'); 5 | const { makeExecutableSchema } = require('@graphql-tools/schema'); 6 | const { prepareConfig } = require('../src/prepareConfig'); 7 | const { makeProjection, genProjection } = require('../src/projection'); 8 | 9 | describe('makeProjection', () => { 10 | const typeDefs = fs.readFileSync(path.join(__dirname, 'schema.graphql'), 'utf-8'); 11 | 12 | const run = (config, source) => new Promise((resolve, reject) => { 13 | const pick = _.mapValues(_.constant)(config); 14 | const go = (info) => { 15 | try { 16 | const proj = makeProjection({ pick })(info); 17 | resolve(proj); 18 | } catch (e) { 19 | reject(e); 20 | } 21 | }; 22 | graphql({ 23 | schema: makeExecutableSchema({ 24 | typeDefs, 25 | resolvers: { 26 | Query: { 27 | obj: (parent, args, context, info) => { 28 | go(info); 29 | }, 30 | evil: (parent, args, context, info) => { 31 | go(info); 32 | }, 33 | }, 34 | }, 35 | resolverValidationOptions: { requireResolversForResolveType: false }, 36 | }), 37 | source, 38 | }).then((res) => { 39 | if (res.errors) { 40 | throw res.errors; 41 | } 42 | }); 43 | }); 44 | 45 | it('should project default when not configured', () => { 46 | expect.hasAssertions(); 47 | return expect(run({}, '{ obj { field1 } }')).resolves.toEqual({ 48 | field1: 1, 49 | }); 50 | }); 51 | 52 | it('should project query null', () => { 53 | expect.hasAssertions(); 54 | return expect(run({ 55 | Obj: { 56 | proj: { 57 | field1: { 58 | query: null, 59 | }, 60 | }, 61 | }, 62 | }, '{ obj { __typename field1 } }')).resolves.toEqual({ 63 | }); 64 | }); 65 | 66 | it('should project query simple', () => { 67 | expect.hasAssertions(); 68 | return expect(run({ 69 | Obj: { 70 | prefix: 'wrap.', 71 | proj: { 72 | field1: { 73 | query: 'value', 74 | }, 75 | }, 76 | }, 77 | }, '{ obj { field1 } }')).resolves.toEqual({ 78 | 'wrap.value': 1, 79 | }); 80 | }); 81 | 82 | it('should project query multiple', () => { 83 | expect.hasAssertions(); 84 | return expect(run({ 85 | Obj: { 86 | prefix: 'wrap.', 87 | proj: { 88 | field1: { 89 | query: ['value', 'value2'], 90 | }, 91 | }, 92 | }, 93 | }, '{ obj { field1 } }')).resolves.toEqual({ 94 | 'wrap.value': 1, 95 | 'wrap.value2': 1, 96 | }); 97 | }); 98 | 99 | it('should not project recursive if false', () => { 100 | expect.hasAssertions(); 101 | return expect(run({ 102 | Obj: { 103 | prefix: 'wrap.', 104 | proj: { }, 105 | }, 106 | Foo: { 107 | prefix: 'wrap2.', 108 | proj: { 109 | f1: { query: 'foo' }, 110 | }, 111 | }, 112 | }, '{ obj { field2 { f1 } } }')).resolves.toEqual({ 113 | 'wrap.field2': 1, 114 | }); 115 | }); 116 | 117 | it('should project recursive', () => { 118 | expect.hasAssertions(); 119 | return expect(run({ 120 | Obj: { 121 | prefix: 'wrap.', 122 | proj: { 123 | field2: { 124 | query: ['evil', 'evils'], 125 | recursive: true, 126 | }, 127 | }, 128 | }, 129 | }, '{ obj { field2 { f1 } } }')).resolves.toEqual({ 130 | 'wrap.evil': 1, 131 | 'wrap.evils': 1, 132 | 'wrap.field2.f1': 1, 133 | }); 134 | }); 135 | 136 | it('should project recursive prefix null', () => { 137 | expect.hasAssertions(); 138 | return expect(run({ 139 | Obj: { 140 | prefix: 'wrap.', 141 | proj: { 142 | field2: { 143 | query: ['evil', 'evils'], 144 | recursive: true, 145 | prefix: null, 146 | }, 147 | }, 148 | }, 149 | }, '{ obj { field2 { f1 } } }')).resolves.toEqual({ 150 | 'wrap.evil': 1, 151 | 'wrap.evils': 1, 152 | 'wrap.f1': 1, 153 | }); 154 | }); 155 | 156 | it('should project recursive prefix', () => { 157 | expect.hasAssertions(); 158 | return expect(run({ 159 | Obj: { 160 | prefix: 'wrap.', 161 | proj: { 162 | field2: { 163 | query: ['evil', 'evils'], 164 | recursive: true, 165 | prefix: 'xxx.', 166 | }, 167 | }, 168 | }, 169 | }, '{ obj { field2 { f1 } } }')).resolves.toEqual({ 170 | 'wrap.evil': 1, 171 | 'wrap.evils': 1, 172 | 'wrap.xxx.f1': 1, 173 | }); 174 | }); 175 | 176 | it('should project recursive prefix absolute', () => { 177 | expect.hasAssertions(); 178 | return expect(run({ 179 | Obj: { 180 | prefix: 'wrap.', 181 | proj: { 182 | field2: { 183 | query: null, 184 | recursive: true, 185 | prefix: '.xxx.', 186 | }, 187 | }, 188 | }, 189 | }, '{ obj { field2 { f1 } } }')).resolves.toEqual({ 190 | 'xxx.f1': 1, 191 | }); 192 | }); 193 | 194 | it('should project recursive relative', () => { 195 | expect.hasAssertions(); 196 | return expect(run({ 197 | Obj: { 198 | prefix: 'wrap.', 199 | proj: { 200 | field2: { 201 | query: null, 202 | recursive: true, 203 | prefix: 'xxx.', 204 | }, 205 | }, 206 | }, 207 | Foo: { 208 | prefix: 'wrap2.', 209 | proj: { 210 | f1: { query: 'foo' }, 211 | }, 212 | }, 213 | }, '{ obj { field2 { f1 } } }')).resolves.toEqual({ 214 | 'wrap.xxx.wrap2.foo': 1, 215 | }); 216 | }); 217 | 218 | it('should project recursive absolute', () => { 219 | expect.hasAssertions(); 220 | return expect(run({ 221 | Obj: { 222 | prefix: 'wrap.', 223 | proj: { 224 | field2: { 225 | query: null, 226 | recursive: true, 227 | prefix: 'xxx.', 228 | }, 229 | }, 230 | }, 231 | Foo: { 232 | prefix: '.wrap2.', 233 | proj: { 234 | f1: { query: 'foo' }, 235 | }, 236 | }, 237 | }, '{ obj { field2 { f1 } } }')).resolves.toEqual({ 238 | 'wrap2.foo': 1, 239 | }); 240 | }); 241 | 242 | it('should project inline fragment', () => { 243 | expect.hasAssertions(); 244 | return expect(run({ 245 | Obj: { 246 | prefix: 'wrap.', 247 | proj: { 248 | field1: { query: 'value' }, 249 | }, 250 | }, 251 | }, `{ 252 | obj { 253 | ... { 254 | field1 255 | } 256 | } 257 | }`)).resolves.toEqual({ 258 | 'wrap.value': 1, 259 | }); 260 | }); 261 | 262 | it('should project deep inline fragment', () => { 263 | expect.hasAssertions(); 264 | return expect(run({ 265 | Obj: { 266 | prefix: 'wrap.', 267 | proj: { 268 | field1: { query: 'value' }, 269 | }, 270 | }, 271 | }, `{ 272 | obj { 273 | ... { 274 | ... { 275 | ... { 276 | field1 277 | } 278 | } 279 | } 280 | } 281 | }`)).resolves.toEqual({ 282 | 'wrap.value': 1, 283 | }); 284 | }); 285 | 286 | it('should project fragment', () => { 287 | expect.hasAssertions(); 288 | return expect(run({ 289 | Obj: { 290 | prefix: 'wrap.', 291 | proj: { 292 | field1: { query: 'value' }, 293 | }, 294 | }, 295 | }, `{ 296 | obj { 297 | ...f 298 | } 299 | } 300 | fragment f on Obj { 301 | field1 302 | } 303 | `)).resolves.toEqual({ 304 | 'wrap.value': 1, 305 | }); 306 | }); 307 | 308 | it('should project deep fragment', () => { 309 | expect.hasAssertions(); 310 | return expect(run({ 311 | Obj: { 312 | prefix: 'wrap.', 313 | proj: { 314 | field1: { query: 'value' }, 315 | }, 316 | }, 317 | }, `{ 318 | obj { 319 | ...f 320 | } 321 | } 322 | fragment f on Obj { 323 | ...g 324 | } 325 | fragment g on Obj { 326 | ...h 327 | } 328 | fragment h on Obj { 329 | field1 330 | } 331 | `)).resolves.toEqual({ 332 | 'wrap.value': 1, 333 | }); 334 | }); 335 | 336 | it('should project typeProj', () => { 337 | expect.hasAssertions(); 338 | return expect(run({ 339 | Obj: { 340 | proj: { 341 | field3: { 342 | query: null, 343 | recursive: true, 344 | prefix: 'evil.', 345 | }, 346 | }, 347 | }, 348 | Father: { 349 | prefix: 'wrap.', 350 | typeProj: 'type', 351 | proj: { 352 | g0: { query: 'value' }, 353 | }, 354 | }, 355 | Child: { 356 | prefix: 'wrap2.', 357 | proj: { 358 | g1: { query: 'value2' }, 359 | }, 360 | }, 361 | }, `{ 362 | obj { 363 | field3 { 364 | g0 365 | } 366 | } 367 | }`)).resolves.toEqual({ 368 | 'evil.wrap.type': 1, 369 | 'evil.wrap.value': 1, 370 | }); 371 | }); 372 | 373 | it('should project inline fragment with typeCondition', () => { 374 | expect.hasAssertions(); 375 | return expect(run({ 376 | Obj: { 377 | proj: { 378 | field3: { 379 | query: null, 380 | recursive: true, 381 | prefix: 'evil.', 382 | }, 383 | }, 384 | }, 385 | Father: { 386 | prefix: 'wrap.', 387 | typeProj: 'type', 388 | proj: { 389 | g0: { query: 'value' }, 390 | }, 391 | }, 392 | Child: { 393 | prefix: 'wrap2.', 394 | proj: { 395 | g1: { query: 'value2' }, 396 | }, 397 | }, 398 | }, `{ 399 | obj { 400 | field3 { 401 | g0 402 | ... on Child { 403 | g1 404 | } 405 | } 406 | } 407 | }`)).resolves.toEqual({ 408 | 'evil.wrap.type': 1, 409 | 'evil.wrap.value': 1, 410 | 'evil.wrap.wrap2.value2': 1, 411 | }); 412 | }); 413 | 414 | it('should project deep inline fragment with typeCondition', () => { 415 | expect.hasAssertions(); 416 | return expect(run({ 417 | Obj: { 418 | proj: { 419 | field3: { 420 | query: null, 421 | recursive: true, 422 | prefix: 'evil.', 423 | }, 424 | }, 425 | }, 426 | Father: { 427 | prefix: 'wrap.', 428 | typeProj: 'type', 429 | proj: { 430 | g0: { query: 'value' }, 431 | }, 432 | }, 433 | Child: { 434 | prefix: 'wrap2.', 435 | proj: { 436 | g1: { query: 'value2' }, 437 | }, 438 | }, 439 | }, `{ 440 | obj { 441 | field3 { 442 | g0 443 | ... on Child { 444 | ... { 445 | ... on Child { 446 | g1 447 | } 448 | } 449 | } 450 | } 451 | } 452 | }`)).resolves.toEqual({ 453 | 'evil.wrap.type': 1, 454 | 'evil.wrap.value': 1, 455 | 'evil.wrap.wrap2.value2': 1, 456 | }); 457 | }); 458 | 459 | it('should project inline fragment with typeCondition partial', () => { 460 | expect.hasAssertions(); 461 | return expect(run({ 462 | Obj: { 463 | proj: { 464 | field3: { 465 | query: null, 466 | recursive: true, 467 | prefix: 'evil.', 468 | }, 469 | }, 470 | }, 471 | Father: { 472 | prefix: 'wrap.', 473 | typeProj: 'type', 474 | proj: { 475 | g0: { query: 'value' }, 476 | }, 477 | }, 478 | Child: { 479 | prefix: 'wrap2.', 480 | proj: { 481 | g1: { query: 'value2' }, 482 | }, 483 | }, 484 | }, `{ 485 | obj { 486 | field3 { 487 | ... on Child { 488 | g0 489 | g1 490 | } 491 | } 492 | } 493 | }`)).resolves.toEqual({ 494 | 'evil.wrap.type': 1, 495 | 'evil.wrap.wrap2.g0': 1, 496 | 'evil.wrap.wrap2.value2': 1, 497 | }); 498 | }); 499 | 500 | it('should project fragment with typeCondition', () => { 501 | expect.hasAssertions(); 502 | return expect(run({ 503 | Obj: { 504 | proj: { 505 | field3: { 506 | query: null, 507 | recursive: true, 508 | prefix: null, 509 | }, 510 | }, 511 | }, 512 | Father: { 513 | prefix: 'wrap.', 514 | typeProj: 'type', 515 | proj: { 516 | g0: { query: 'value' }, 517 | }, 518 | }, 519 | Child: { 520 | prefix: 'wrap2.', 521 | proj: { 522 | g1: { query: 'value2' }, 523 | }, 524 | }, 525 | }, `{ 526 | obj { 527 | field3 { 528 | g0 529 | ...f 530 | } 531 | } 532 | } 533 | fragment f on Child { 534 | g1 535 | } 536 | `)).resolves.toEqual({ 537 | 'wrap.type': 1, 538 | 'wrap.value': 1, 539 | 'wrap.wrap2.value2': 1, 540 | }); 541 | }); 542 | 543 | it('should handle deep nested', () => { 544 | expect.hasAssertions(); 545 | return expect(run({ 546 | Evil: { 547 | proj: { 548 | self: { 549 | query: null, 550 | recursive: true, 551 | prefix: null, 552 | }, 553 | }, 554 | }, 555 | }, `{ 556 | evil { 557 | field 558 | self { 559 | field 560 | self { 561 | field 562 | } 563 | } 564 | } 565 | } 566 | `)).resolves.toEqual({ 567 | field: 1, 568 | }); 569 | }); 570 | 571 | it('should handle deep nested prefix', () => { 572 | expect.hasAssertions(); 573 | return expect(run({ 574 | Evil: { 575 | prefix: '.x.', 576 | proj: { 577 | self: { 578 | query: null, 579 | recursive: true, 580 | prefix: null, 581 | }, 582 | }, 583 | }, 584 | }, `{ 585 | evil { 586 | field 587 | self { 588 | field 589 | self { 590 | field 591 | } 592 | } 593 | } 594 | } 595 | `)).resolves.toEqual({ 596 | 'x.field': 1, 597 | }); 598 | }); 599 | 600 | it('should handle deep nested prefix relative', () => { 601 | expect.hasAssertions(); 602 | return expect(run({ 603 | Evil: { 604 | prefix: 'x.', 605 | proj: { 606 | self: { 607 | query: null, 608 | recursive: true, 609 | prefix: null, 610 | }, 611 | }, 612 | }, 613 | }, `{ 614 | evil { 615 | field 616 | self { 617 | field 618 | self { 619 | field 620 | } 621 | } 622 | } 623 | } 624 | `)).resolves.toEqual({ 625 | 'x.field': 1, 626 | 'x.x.field': 1, 627 | 'x.x.x.field': 1, 628 | }); 629 | }); 630 | 631 | it('should handle deep nested proj prefix', () => { 632 | expect.hasAssertions(); 633 | return expect(run({ 634 | Evil: { 635 | proj: { 636 | self: { 637 | query: null, 638 | recursive: true, 639 | prefix: 'y.', 640 | }, 641 | }, 642 | }, 643 | }, `{ 644 | evil { 645 | field 646 | self { 647 | field 648 | self { 649 | field 650 | } 651 | } 652 | } 653 | } 654 | `)).resolves.toEqual({ 655 | field: 1, 656 | 'y.field': 1, 657 | 'y.y.field': 1, 658 | }); 659 | }); 660 | 661 | it('should handle deep nested proj prefix prefix', () => { 662 | expect.hasAssertions(); 663 | return expect(run({ 664 | Evil: { 665 | prefix: '.x.', 666 | proj: { 667 | self: { 668 | query: null, 669 | recursive: true, 670 | prefix: 'y.', 671 | }, 672 | }, 673 | }, 674 | }, `{ 675 | evil { 676 | field 677 | self { 678 | field 679 | self { 680 | field 681 | } 682 | } 683 | } 684 | } 685 | `)).resolves.toEqual({ 686 | 'x.field': 1, 687 | }); 688 | }); 689 | 690 | it('should handle deep nested proj prefix prefix relative', () => { 691 | expect.hasAssertions(); 692 | return expect(run({ 693 | Evil: { 694 | prefix: 'x.', 695 | proj: { 696 | self: { 697 | query: null, 698 | recursive: true, 699 | prefix: 'y.', 700 | }, 701 | }, 702 | }, 703 | }, `{ 704 | evil { 705 | field 706 | self { 707 | field 708 | self { 709 | field 710 | } 711 | } 712 | } 713 | } 714 | `)).resolves.toEqual({ 715 | 'x.field': 1, 716 | 'x.y.x.field': 1, 717 | 'x.y.x.y.x.field': 1, 718 | }); 719 | }); 720 | 721 | it('should handle deep nested proj prefix abs', () => { 722 | expect.hasAssertions(); 723 | return expect(run({ 724 | Evil: { 725 | proj: { 726 | self: { 727 | query: null, 728 | recursive: true, 729 | prefix: '.y.', 730 | }, 731 | }, 732 | }, 733 | }, `{ 734 | evil { 735 | field 736 | self { 737 | field 738 | self { 739 | field 740 | } 741 | } 742 | } 743 | } 744 | `)).resolves.toEqual({ 745 | field: 1, 746 | 'y.field': 1, 747 | }); 748 | }); 749 | 750 | it('should handle deep nested proj prefix abs prefix', () => { 751 | expect.hasAssertions(); 752 | return expect(run({ 753 | Evil: { 754 | prefix: '.x.', 755 | proj: { 756 | self: { 757 | query: null, 758 | recursive: true, 759 | prefix: '.y.', 760 | }, 761 | }, 762 | }, 763 | }, `{ 764 | evil { 765 | field 766 | self { 767 | field 768 | self { 769 | field 770 | } 771 | } 772 | } 773 | } 774 | `)).resolves.toEqual({ 775 | 'x.field': 1, 776 | }); 777 | }); 778 | 779 | it('should handle deep nested proj prefix abs prefix relative', () => { 780 | expect.hasAssertions(); 781 | return expect(run({ 782 | Evil: { 783 | prefix: 'x.', 784 | proj: { 785 | self: { 786 | query: null, 787 | recursive: true, 788 | prefix: '.y.', 789 | }, 790 | }, 791 | }, 792 | }, `{ 793 | evil { 794 | field 795 | self { 796 | field 797 | self { 798 | field 799 | } 800 | } 801 | } 802 | } 803 | `)).resolves.toEqual({ 804 | 'x.field': 1, 805 | 'y.x.field': 1, 806 | }); 807 | }); 808 | }); 809 | 810 | describe('genProjection', () => { 811 | const typeDefs = fs.readFileSync(path.join(__dirname, 'schema.graphql'), 'utf-8'); 812 | 813 | const run = (config, source) => new Promise((resolve, reject) => { 814 | const go = (info) => { 815 | try { 816 | const proj = genProjection(prepareConfig(config)); 817 | resolve(proj(info)); 818 | } catch (e) { 819 | reject(e); 820 | } 821 | }; 822 | graphql({ 823 | schema: makeExecutableSchema({ 824 | typeDefs, 825 | resolvers: { 826 | Query: { 827 | obj: (parent, args, context, info) => { 828 | go(info); 829 | }, 830 | evil: (parent, args, context, info) => { 831 | go(info); 832 | }, 833 | }, 834 | }, 835 | resolverValidationOptions: { requireResolversForResolveType: false }, 836 | }), 837 | source, 838 | }).then((res) => { 839 | if (res.errors) { 840 | throw res.errors; 841 | } 842 | }); 843 | }); 844 | 845 | it('should project root', () => { 846 | expect.hasAssertions(); 847 | return expect(run({ root: { itst: 1 } }, '{ obj { field1 } }')).resolves.toEqual({ 848 | itst: 1, 849 | field1: 1, 850 | }); 851 | }); 852 | 853 | it('should project simple', () => { 854 | expect.hasAssertions(); 855 | return expect(run({ Obj: { proj: { field1: 'a' } } }, '{ obj { field1 } }')).resolves.toEqual({ 856 | _id: 0, 857 | a: 1, 858 | }); 859 | }); 860 | 861 | it('should project complex', () => { 862 | expect.hasAssertions(); 863 | return expect(run({ 864 | Obj: [ 865 | ['othr', { proj: { field1: 'a' } }], 866 | ['obj', { proj: { field1: 'b' } }], 867 | ], 868 | }, '{ obj { field1 } }')).resolves.toEqual({ 869 | _id: 0, 870 | b: 1, 871 | }); 872 | }); 873 | 874 | it('should project more complex 1', () => { 875 | expect.hasAssertions(); 876 | return expect(run({ 877 | Obj: { proj: { evil: true } }, 878 | Evil: [ 879 | ['obj', { proj: { field: 'a' } }], 880 | ['evil', { proj: { field: 'b' } }], 881 | ], 882 | }, '{ obj { evil { field } } }')).resolves.toEqual({ 883 | _id: 0, 884 | 'evil.a': 1, 885 | }); 886 | }); 887 | 888 | it('should project more complex 2', () => { 889 | expect.hasAssertions(); 890 | return expect(run({ 891 | Obj: { proj: { evil: true } }, 892 | Evil: [ 893 | ['obj', { proj: { field: 'a' } }], 894 | ['evil', { proj: { field: 'b' } }], 895 | ], 896 | }, '{ evil { field } }')).resolves.toEqual({ 897 | _id: 0, 898 | b: 1, 899 | }); 900 | }); 901 | }); 902 | --------------------------------------------------------------------------------