├── .babelrc ├── .circleci └── config.yml ├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── __tests__ ├── __snapshots__ │ └── computed.test.js.snap └── computed.test.js ├── example ├── index.js └── schema.js ├── index.js ├── package.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env" 4 | ], 5 | "plugins": [ 6 | "@babel/plugin-transform-runtime" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | test-defaults: &test-defaults 4 | working_directory: ~/graphql-directive-uid 5 | steps: 6 | - attach_workspace: 7 | at: ~/graphql-directive-uid 8 | - run: | 9 | yarn lint 10 | yarn test 11 | jobs: 12 | install-dependencies: 13 | working_directory: ~/graphql-directive-uid 14 | docker: 15 | - image: circleci/node:6 16 | steps: 17 | - checkout 18 | - attach_workspace: 19 | at: ~/graphql-directive-uid 20 | - restore_cache: 21 | keys: 22 | - dependencies-{{ checksum "yarn.lock" }} 23 | - dependencies- 24 | - run: yarn install --frozen-lockfile 25 | - save_cache: 26 | key: dependencies-{{ checksum "yarn.lock" }} 27 | paths: node_modules 28 | - persist_to_workspace: 29 | root: . 30 | paths: . 31 | 32 | lint-and-test-node-6: 33 | <<: *test-defaults 34 | docker: 35 | - image: circleci/node:6 36 | 37 | lint-and-test-node-8: 38 | <<: *test-defaults 39 | docker: 40 | - image: circleci/node:8 41 | 42 | lint-and-test-node-9: 43 | <<: *test-defaults 44 | docker: 45 | - image: circleci/node:9 46 | 47 | publish: 48 | working_directory: ~/graphql-directive-uid 49 | docker: 50 | - image: circleci/node:6 51 | steps: 52 | - attach_workspace: 53 | at: ~/graphql-directive-uid 54 | - run: | 55 | npm config set //registry.npmjs.org/:_authToken $NPM_TOKEN 56 | npm publish 57 | workflows: 58 | version: 2 59 | build-and-test: 60 | jobs: 61 | - install-dependencies 62 | - lint-and-test-node-6: 63 | requires: 64 | - install-dependencies 65 | - lint-and-test-node-8: 66 | requires: 67 | - install-dependencies 68 | - lint-and-test-node-9: 69 | requires: 70 | - install-dependencies 71 | - publish: 72 | requires: 73 | - install-dependencies 74 | filters: 75 | tags: 76 | only: /v\d+\.\d+\.\d+.*/ 77 | branches: 78 | ignore: /.*/ -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | 'import/no-extraneous-dependencies': 0, 4 | 'no-param-reassign': 0, 5 | }, 6 | extends: 'callstack-io', 7 | env: { 8 | jest: true, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | dist -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Luke Czyszczonik - 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graphql-directive-computed-property 2 | 3 | [![Version][version-badge]][package] 4 | [![downloads][downloads-badge]][npmtrends] 5 | [![PRs Welcome][prs-badge]][prs] 6 | [![MIT License][license-badge]][build] 7 | 8 | # Introduction 9 | 10 | The directive allows creating a computed property from fields where is defined. 11 | 12 | # Table of Contents 13 | 14 | * [Introduction](#introduction) 15 | * [Installation](#installation) 16 | * [Usage](#Usage) 17 | * [Parameters](#parameters) 18 | * [Contributing](#contributing) 19 | * [LICENSE](#license) 20 | 21 | # Installation 22 | 23 | ``` 24 | yarn add graphql-directive-computed-property 25 | ``` 26 | 27 | _This package requires [graphql](https://www.npmjs.com/package/graphql) and [graphql-tools](https://www.npmjs.com/package/graphql-tools) as peer dependency_ 28 | 29 | # Usage 30 | 31 | ```js 32 | const { makeExecutableSchema } = require('graphql-tools'); 33 | const computedDirective = require('graphql-directive-computed-property'); 34 | 35 | const typeDefs = ` 36 | type User { 37 | firstName: String 38 | lastName: String 39 | fullName: String @computed(value: "$firstName $lastName") 40 | } 41 | 42 | type Query { 43 | me: User 44 | } 45 | `; 46 | 47 | const resolvers = { 48 | Query: { 49 | me: () => ({ 50 | firstName: 'John', 51 | lastName: 'Doe', 52 | }), 53 | }, 54 | }; 55 | 56 | module.exports = makeExecutableSchema({ 57 | typeDefs, 58 | resolvers, 59 | schemaDirectives: { 60 | computed: computedDirective, 61 | }, 62 | }); 63 | ``` 64 | 65 | Query: 66 | 67 | ```graphql 68 | query { 69 | me { 70 | fullName 71 | } 72 | } 73 | ``` 74 | 75 | Result: 76 | 77 | ```js 78 | { 79 | fullName: 'John Doe' 80 | } 81 | ``` 82 | ### Computed property work well with other directives like [@rest](https://www.npmjs.com/package/graphql-directive-rest): 83 | 84 | Example: 85 | 86 | ```admin: String @rest(url: "${URL_TO_API}") @computed(value: "Are you admin? $admin")``` 87 | 88 | 89 | # Directive Parameters 90 | 91 | Directive params: 92 | 93 | ### `value`: String 94 | 95 | The calculated value. It can contain other fields from the type in which it is defined. 96 | 97 | Example: 98 | 99 | `@computed(value: "$firstName $lastName")` 100 | 101 | `@computed(value: "$price $")` 102 | 103 | ## Contributing 104 | 105 | I would love to see your contribution. ❤️ 106 | 107 | For local development (and testing), all you have to do is to run `yarn` and then `yarn dev`. This will start the Apollo server and you are ready to contribute :tada: 108 | 109 | Run yarn test (try `--watch` flag) for unit tests (we are using Jest) 110 | 111 | # LICENSE 112 | 113 | The MIT License (MIT) 2018 - Luke Czyszczonik - 114 | 115 | [npm]: https://www.npmjs.com/ 116 | [node]: https://nodejs.org 117 | [build-badge]: https://img.shields.io/travis/graphql-community/graphql-directive-computed-property.svg?style=flat-square 118 | [build]: https://travis-ci.org/graphql-community/graphql-directive-computed-property 119 | [coverage-badge]: https://img.shields.io/codecov/c/github/graphql-community/graphql-directive-computed-property.svg?style=flat-square 120 | [coverage]: https://codecov.io/github/graphql-community/graphql-directive-computed-property 121 | [version-badge]: https://img.shields.io/npm/v/graphql-directive-computed-property.svg?style=flat-square 122 | [package]: https://www.npmjs.com/package/graphql-directive-computed-property 123 | [downloads-badge]: https://img.shields.io/npm/dm/graphql-directive-computed-property.svg?style=flat-square 124 | [npmtrends]: http://www.npmtrends.com/graphql-directive-computed-property 125 | [license-badge]: https://img.shields.io/npm/l/graphql-directive-computed-property.svg?style=flat-square 126 | [license]: https://github.com/graphql-community/graphql-directive-computed-property/blob/master/LICENSE 127 | [prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square 128 | [prs]: http://makeapullrequest.com 129 | [donate-badge]: https://img.shields.io/badge/$-support-green.svg?style=flat-square 130 | [coc-badge]: https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat-square 131 | [coc]: https://github.com/graphql-community/graphql-directive-computed-property/blob/master/CODE_OF_CONDUCT.md 132 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/computed.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`check if not replace original resolver 1`] = ` 4 | Object { 5 | "data": Object { 6 | "me": Object { 7 | "admin": "Are you admin? yes", 8 | "firstName": "John", 9 | "lastName": "Doe", 10 | }, 11 | }, 12 | } 13 | `; 14 | 15 | exports[`getDirectiveDeclaration should be defined 1`] = ` 16 | GraphQLDirective { 17 | "args": Array [ 18 | Object { 19 | "astNode": undefined, 20 | "defaultValue": undefined, 21 | "description": null, 22 | "name": "value", 23 | "type": "String", 24 | }, 25 | ], 26 | "astNode": undefined, 27 | "description": undefined, 28 | "locations": Array [ 29 | "FIELD_DEFINITION", 30 | ], 31 | "name": "rest", 32 | } 33 | `; 34 | 35 | exports[`if return computed property 1`] = ` 36 | Object { 37 | "data": Object { 38 | "me": Object { 39 | "firstName": "John", 40 | "fullName": "John Doe", 41 | "lastName": "Doe", 42 | }, 43 | }, 44 | } 45 | `; 46 | -------------------------------------------------------------------------------- /__tests__/computed.test.js: -------------------------------------------------------------------------------- 1 | const { graphql } = require('graphql'); 2 | const nock = require('nock'); 3 | const directive = require('../index'); 4 | const schema = require('../example/schema'); 5 | 6 | beforeAll(() => { 7 | nock.disableNetConnect(); 8 | }); 9 | 10 | afterEach(() => { 11 | nock.cleanAll(); 12 | }); 13 | 14 | afterAll(() => { 15 | nock.enableNetConnect(); 16 | }); 17 | 18 | test('getDirectiveDeclaration should be defined', () => { 19 | expect(directive.getDirectiveDeclaration()).toMatchSnapshot(); 20 | }); 21 | 22 | test('if return computed property', async () => { 23 | const response = await graphql( 24 | schema, 25 | ` 26 | query { 27 | me { 28 | firstName 29 | lastName 30 | fullName 31 | } 32 | } 33 | ` 34 | ); 35 | 36 | expect(response).toMatchSnapshot(); 37 | }); 38 | 39 | test('check if not replace original resolver', async () => { 40 | nock('https://yesno.wtf:443', { encodedQueryParams: true }) 41 | .get('/api') 42 | .reply(200, { answer: 'yes' }); 43 | 44 | const response = await graphql( 45 | schema, 46 | ` 47 | query { 48 | me { 49 | firstName 50 | lastName 51 | admin 52 | } 53 | } 54 | ` 55 | ); 56 | 57 | expect(response).toMatchSnapshot(); 58 | }); 59 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const bodyParser = require('body-parser'); 3 | const { graphqlExpress, graphiqlExpress } = require('apollo-server-express'); 4 | const schema = require('./schema'); 5 | 6 | const PORT = process.env.PORT || 3003; 7 | const HOST = process.env.HOST || 'localhost'; 8 | 9 | const app = express(); 10 | app.use('/graphql', bodyParser.json(), graphqlExpress({ schema })); 11 | app.use('/graphiql', graphiqlExpress({ endpointURL: '/graphql' })); 12 | 13 | app.listen(PORT, () => { 14 | console.log(`Server started at: http://${HOST}:${PORT}/graphiql`); // eslint-disable-line no-console 15 | }); 16 | -------------------------------------------------------------------------------- /example/schema.js: -------------------------------------------------------------------------------- 1 | const { makeExecutableSchema } = require('graphql-tools'); 2 | const computedDirective = require('../index'); 3 | const restDirective = require('graphql-directive-rest'); 4 | 5 | const ADMIN_URL = 'https://yesno.wtf/api'; 6 | 7 | const typeDefs = ` 8 | type User { 9 | firstName: String 10 | lastName: String 11 | admin: String @rest(url: "${ADMIN_URL}" extractFromResponse: "answer") @computed(value: "Are you admin? $admin") 12 | fullName: String @computed(value: "$firstName $lastName") 13 | } 14 | 15 | type Query { 16 | me: User 17 | } 18 | `; 19 | 20 | const resolvers = { 21 | Query: { 22 | me: () => ({ 23 | firstName: 'John', 24 | lastName: 'Doe', 25 | }), 26 | }, 27 | }; 28 | 29 | module.exports = makeExecutableSchema({ 30 | typeDefs, 31 | resolvers, 32 | schemaDirectives: { 33 | computed: computedDirective, 34 | rest: restDirective, 35 | }, 36 | }); 37 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { 2 | DirectiveLocation, 3 | GraphQLDirective, 4 | GraphQLString, 5 | defaultFieldResolver, 6 | } = require('graphql'); 7 | const { SchemaDirectiveVisitor } = require('graphql-tools'); 8 | 9 | class computedDirective extends SchemaDirectiveVisitor { 10 | static getDirectiveDeclaration(directiveName = 'rest') { 11 | return new GraphQLDirective({ 12 | name: directiveName, 13 | locations: [DirectiveLocation.FIELD_DEFINITION], 14 | args: { 15 | value: { type: GraphQLString }, 16 | }, 17 | }); 18 | } 19 | 20 | visitFieldDefinition(field) { 21 | const { resolve = defaultFieldResolver, name } = field; 22 | 23 | field.resolve = async (root, args, context, info) => { 24 | const result = await resolve.call(this, root, args, context, info); 25 | 26 | const updatedRoot = Object.assign(root, { [name]: result }); 27 | 28 | let value = this.args.value; 29 | 30 | for (const property in updatedRoot) { 31 | if (Object.prototype.hasOwnProperty.call(updatedRoot, property)) { 32 | value = value.replace( 33 | new RegExp(`\\$${property}`, 'g'), 34 | updatedRoot[property] 35 | ); 36 | } 37 | } 38 | 39 | return value; 40 | }; 41 | } 42 | } 43 | 44 | module.exports = computedDirective; 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-directive-computed-property", 3 | "version": "0.1.1", 4 | "description": "GraphQL directive for create computed property", 5 | "main": "dist/index.js", 6 | "repository": "git@github.com:graphql-community/graphql-directive-computed-property.git", 7 | "author": "Czysty ", 8 | "license": "MIT", 9 | "scripts": { 10 | "dev": "nodemon example/index.js", 11 | "test": "jest", 12 | "lint": "eslint . --ignore-path .gitignore", 13 | "build": "babel index.js -d dist", 14 | "postinstall": "npm run build" 15 | }, 16 | "peerDependencies": { 17 | "graphql": "^0.13.2", 18 | "graphql-tools": "^2.23.1 || ^4.0.0" 19 | }, 20 | "devDependencies": { 21 | "@babel/cli": "^7.0.0", 22 | "@babel/core": "^7.0.0", 23 | "@babel/plugin-transform-runtime": "^7.0.0", 24 | "@babel/preset-env": "^7.0.0", 25 | "@babel/runtime": "^7.1.5", 26 | "apollo-server-express": "^1.3.4", 27 | "babel-core": "^7.0.0-bridge.0", 28 | "babel-jest": "^23.4.2", 29 | "body-parser": "^1.18.2", 30 | "eslint": "^4.19.1", 31 | "eslint-config-callstack-io": "^1.1.1", 32 | "express": "^4.16.3", 33 | "graphql": "^0.13.2", 34 | "graphql-directive-rest": "^0.1.1", 35 | "graphql-tools": "^4.0.3", 36 | "jest": "^23.6.0", 37 | "nock": "^9.2.3", 38 | "nodemon": "^1.17.3" 39 | } 40 | } 41 | --------------------------------------------------------------------------------