├── src ├── index.js ├── withAssertions.js └── __tests__ │ └── withAssertions-test.js ├── .gitignore ├── .travis.yml ├── .mailmap ├── .babelrc ├── .eslintrc ├── scripts └── release.sh ├── README.md └── package.json /src/index.js: -------------------------------------------------------------------------------- 1 | export withAssertions from './withAssertions'; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .*.swp 3 | lib 4 | *.log 5 | .idea 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4.3" 4 | script: 5 | - npm run test 6 | - npm run lint 7 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | ironhee ironhee 2 | ironhee Ironhee 3 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | ["transform-runtime", { 4 | "polyfill": false, 5 | "regenerator": true 6 | }], 7 | "add-module-exports" 8 | ], 9 | "presets": ["es2015", "stage-0"] 10 | } 11 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb/base", 4 | "env": { 5 | "node": true, 6 | "mocha": true, 7 | }, 8 | "rules": { 9 | "id-length": 0, 10 | "func-names": 0, 11 | "no-use-before-define": 0, 12 | "no-unused-expressions": 0, 13 | "space-before-function-paren": 0, 14 | "brace-style": 0, 15 | "max-len": 0, 16 | "newline-per-chained-call": 0, 17 | "no-underscore-dangle": 0, 18 | "arrow-body-style": 0, 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | export RELEASE=1 3 | 4 | if ! [ -e scripts/release.sh ]; then 5 | echo >&2 "Please run scripts/release.sh from the repo root" 6 | exit 1 7 | fi 8 | 9 | update_version() { 10 | echo "$(node -p "p=require('./${1}');p.version='${2}';JSON.stringify(p,null,2)")" > $1 11 | echo "Updated ${1} version to ${2}" 12 | } 13 | 14 | validate_semver() { 15 | if ! [[ $1 =~ ^[0-9]\.[0-9]+\.[0-9](-.+)? ]]; then 16 | echo >&2 "Version $1 is not valid! It must be a valid semver string like 1.0.2 or 2.3.0-beta.1" 17 | exit 1 18 | fi 19 | } 20 | 21 | current_version=$(node -p "require('./package').version") 22 | 23 | printf "Next version (current is $current_version)? " 24 | read next_version 25 | 26 | validate_semver $next_version 27 | 28 | next_ref="v$next_version" 29 | 30 | npm test 31 | 32 | update_version 'package.json' $next_version 33 | 34 | git commit -am "Version $next_version" 35 | 36 | # push first to make sure we're up-to-date 37 | git push origin master 38 | 39 | git tag $next_ref 40 | git tag latest -f 41 | 42 | git push origin $next_ref 43 | git push origin latest -f 44 | 45 | npm run build 46 | 47 | npm publish 48 | -------------------------------------------------------------------------------- /src/withAssertions.js: -------------------------------------------------------------------------------- 1 | /* eslint no-param-reassign: 0 */ 2 | import _ from 'lodash'; 3 | 4 | 5 | function withAssertions(asserts, fields) { 6 | return _.mapValues(fields, (field, key) => { 7 | const { 8 | assertions, 9 | resolve = (parent) => parent[key], 10 | } = field; 11 | 12 | if (_.isEmpty(assertions)) return field; 13 | 14 | return { 15 | ...field, 16 | resolve: async (data, args, context, info) => { 17 | const assertionResults = await _.chain(asserts) 18 | .pick(assertions) 19 | .reduce(async (memo, assert, assertKey) => { 20 | let result = _.get(data, `_assertionResults.${assertKey}`); 21 | 22 | if (!result) { 23 | try { 24 | await assert(data, args, context, info); 25 | result = true; 26 | } catch (err) { 27 | result = err; 28 | } 29 | _.set(data, `_assertionResults.${assertKey}`, result); 30 | } 31 | 32 | return { 33 | ...(await memo), 34 | [assertKey]: result, 35 | }; 36 | }, Promise.resolve({})) 37 | .value(); 38 | 39 | if (_.every(assertionResults, assertionResult => assertionResult !== true)) { 40 | throw assertionResults[0]; 41 | } 42 | 43 | return resolve(data, args, context, info); 44 | }, 45 | }; 46 | }); 47 | } 48 | 49 | 50 | export default withAssertions; 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Install 2 | 3 | ```bash 4 | npm install assertql -S 5 | ``` 6 | 7 | # Usage 8 | 9 | ```js 10 | import _ from 'lodash'; 11 | import { 12 | GraphQLObjectType, 13 | GraphQLString, 14 | GraphQLInt, 15 | } from 'graphql'; 16 | import { withAssertions } from 'assertql'; 17 | 18 | 19 | const loggedIn = (data, args, context, info) => { 20 | if (!_.get(info, 'rootValue.currentUser.id')) { 21 | throw new Error('User dose not have enough permission!'); 22 | } 23 | }; 24 | 25 | const hasRole = (role) => (data, args, context, info) => { 26 | const roles = _.get(info, 'rootValue.currentUser.roles', []); 27 | if (!_.includes(roles, role)) { 28 | throw new Error('User dose not have enough permission!'); 29 | } 30 | }; 31 | 32 | const isAdmin = hasRole('admin'); 33 | 34 | const isMe = (data, args, context, info) => { 35 | if (data.id !== _.get(info, 'rootValue.currentUser.id')) 36 | throw new Error('User dose not have enough permission!'); 37 | } 38 | }; 39 | 40 | const userType = new GraphQLObjectType({ 41 | name: 'User', 42 | fields: withAssertions({ 43 | loggedIn, 44 | isAdmin, 45 | isMe, 46 | }, { 47 | id: { 48 | type: GraphQLString, 49 | // if no assertion, field is public 50 | }, 51 | email: { 52 | type: GraphQLString, 53 | // logged in user, self, admin can read this field 54 | assertions: ['loggedIn', 'isMe', 'isAdmin'], 55 | }, 56 | point: { 57 | type: GraphQLInt, 58 | // self, admin can read this field 59 | assertions: ['isMe', 'isAdmin'], 60 | }, 61 | }), 62 | }); 63 | ``` 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "assertql", 3 | "version": "0.2.0", 4 | "description": "Assert graphql field for check permission, etc", 5 | "main": "lib", 6 | "files": [ 7 | "lib" 8 | ], 9 | "scripts": { 10 | "release": "sh ./scripts/release.sh", 11 | "test": "mocha --compilers js:babel-register --timeout 20000 --recursive src/", 12 | "build": "babel --ignore *-test.js -d lib src", 13 | "clean": "rimraf lib", 14 | "lint": "eslint src", 15 | "prepublish": "npm run clean && npm run build" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+ssh://git@github.com/ediket/assertql.git" 20 | }, 21 | "keywords": [ 22 | "graphql", 23 | "permission", 24 | "assert", 25 | "assertql" 26 | ], 27 | "author": "ironhee ", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/ediket/assertql/issues" 31 | }, 32 | "homepage": "https://github.com/ediket/assertql#readme", 33 | "devDependencies": { 34 | "babel-cli": "^6.9.0", 35 | "babel-eslint": "^6.0.4", 36 | "babel-plugin-add-module-exports": "^0.2.1", 37 | "babel-plugin-transform-runtime": "^6.9.0", 38 | "babel-preset-es2015": "^6.9.0", 39 | "babel-preset-stage-0": "^6.5.0", 40 | "babel-register": "^6.9.0", 41 | "chai": "^3.4.1", 42 | "eslint": "^2.10.2", 43 | "eslint-config-airbnb": "^9.0.1", 44 | "eslint-plugin-import": "^1.8.0", 45 | "graphql": ">=0.8.2", 46 | "mocha": "^2.5.3", 47 | "rimraf": "^2.5.1" 48 | }, 49 | "dependencies": { 50 | "babel-runtime": "^6.9.0", 51 | "lodash": "^4.5.1" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/__tests__/withAssertions-test.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import _ from 'lodash'; 3 | import { 4 | graphql, 5 | GraphQLSchema, 6 | GraphQLObjectType, 7 | GraphQLString, 8 | } from 'graphql'; 9 | import withAssertions from '../withAssertions'; 10 | 11 | 12 | describe('withAssertions', () => { 13 | const SAMPLE_USER = { id: '1', email: 'foo@test.com', name: 'foo' }; 14 | 15 | const loggedIn = (data, args, context, info) => { 16 | if (!_.get(info, 'rootValue.currentUser.id')) { 17 | throw new Error('User dose not have enough permission!'); 18 | } 19 | }; 20 | 21 | const isMe = (data, args, context, info) => { 22 | loggedIn(data, args, context, info); 23 | const userId = _.get(info, 'rootValue.currentUser.id'); 24 | if (userId !== data.id) { 25 | throw new Error('User dose not have enough permission!'); 26 | } 27 | }; 28 | 29 | const isAdmin = (data, args, context, info) => { 30 | loggedIn(data, args, context, info); 31 | const roles = _.get(info, 'rootValue.currentUser.roles'); 32 | if (!_.includes(roles, 'admin')) { 33 | throw new Error('User dose not have enough permission!'); 34 | } 35 | }; 36 | 37 | it('should work', async () => { 38 | const userType = new GraphQLObjectType({ 39 | name: 'User', 40 | fields: withAssertions({ isMe, isAdmin }, { 41 | id: { 42 | type: GraphQLString, 43 | }, 44 | email: { 45 | type: GraphQLString, 46 | assertions: ['isMe', 'isAdmin'], 47 | }, 48 | }), 49 | }); 50 | 51 | const schema = new GraphQLSchema({ 52 | query: new GraphQLObjectType({ 53 | name: 'Query', 54 | fields: () => ({ 55 | user: { 56 | type: userType, 57 | resolve: () => _.clone(SAMPLE_USER), 58 | }, 59 | }), 60 | }), 61 | }); 62 | 63 | const query = ` 64 | { 65 | user { 66 | id 67 | email 68 | } 69 | } 70 | `; 71 | 72 | const anonymousRootValue = {}; 73 | const anonymousResult = await graphql(schema, query, anonymousRootValue); 74 | expect(anonymousResult.errors).to.have.length(1); 75 | expect(anonymousResult.data).to.deep.equal({ 76 | user: { id: SAMPLE_USER.id, email: null }, 77 | }); 78 | 79 | const userRootValue = { currentUser: { id: '1', roles: ['user'] } }; 80 | const userResult = await graphql(schema, query, userRootValue); 81 | 82 | expect(userResult.errors).to.be.empty; 83 | expect(userResult.data).to.deep.equal({ 84 | user: _.pick(SAMPLE_USER, 'id', 'email'), 85 | }); 86 | 87 | const adminRootValue = { currentUser: { id: '2', roles: ['admin'] } }; 88 | const adminResult = await graphql(schema, query, adminRootValue); 89 | expect(adminResult.errors).to.be.empty; 90 | expect(adminResult.data).to.deep.equal({ 91 | user: _.pick(SAMPLE_USER, 'id', 'email'), 92 | }); 93 | }); 94 | 95 | it('should cache result', async () => { 96 | const userType = new GraphQLObjectType({ 97 | name: 'User', 98 | fields: withAssertions({ loggedIn }, { 99 | id: { 100 | type: GraphQLString, 101 | }, 102 | email: { 103 | type: GraphQLString, 104 | assertions: ['loggedIn'], 105 | }, 106 | name: { 107 | type: GraphQLString, 108 | assertions: ['loggedIn'], 109 | }, 110 | }), 111 | }); 112 | 113 | const schema = new GraphQLSchema({ 114 | query: new GraphQLObjectType({ 115 | name: 'Query', 116 | fields: () => ({ 117 | user: { 118 | type: userType, 119 | resolve: () => _.clone(SAMPLE_USER), 120 | }, 121 | }), 122 | }), 123 | }); 124 | 125 | const query = ` 126 | { 127 | user { 128 | id 129 | email 130 | name 131 | } 132 | } 133 | `; 134 | 135 | const anonymousRootValue = {}; 136 | const anonymousResult = await graphql(schema, query, anonymousRootValue); 137 | expect(anonymousResult.errors).to.have.length(2); 138 | expect(anonymousResult.data).to.deep.equal({ 139 | user: { id: SAMPLE_USER.id, email: null, name: null }, 140 | }); 141 | 142 | const userRootValue = { currentUser: { id: '1' } }; 143 | const userResult = await graphql(schema, query, userRootValue); 144 | expect(userResult.errors).to.be.empty; 145 | expect(userResult.data).to.deep.equal({ 146 | user: _.pick(SAMPLE_USER, 'id', 'email', 'name'), 147 | }); 148 | }); 149 | 150 | it('should work with async assertion', async () => { 151 | const throwError = async () => { 152 | throw new Error(); 153 | }; 154 | 155 | const userType = new GraphQLObjectType({ 156 | name: 'User', 157 | fields: withAssertions({ throwError }, { 158 | id: { 159 | type: GraphQLString, 160 | }, 161 | email: { 162 | type: GraphQLString, 163 | assertions: ['throwError'], 164 | }, 165 | }), 166 | }); 167 | 168 | const schema = new GraphQLSchema({ 169 | query: new GraphQLObjectType({ 170 | name: 'Query', 171 | fields: () => ({ 172 | user: { 173 | type: userType, 174 | resolve: () => _.clone(SAMPLE_USER), 175 | }, 176 | }), 177 | }), 178 | }); 179 | 180 | const query = ` 181 | { 182 | user { 183 | id 184 | email 185 | } 186 | } 187 | `; 188 | 189 | const anonymousRootValue = {}; 190 | const anonymousResult = await graphql(schema, query, anonymousRootValue); 191 | expect(anonymousResult.errors).to.have.length(1); 192 | expect(anonymousResult.data).to.deep.equal({ 193 | user: { id: SAMPLE_USER.id, email: null }, 194 | }); 195 | }); 196 | }); 197 | --------------------------------------------------------------------------------