├── .babelrc ├── .codeclimate.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .npmrc ├── .prettierignore ├── .travis.yml ├── README.md ├── install.js ├── package.json ├── src ├── context.js ├── index.js ├── mocks.js ├── resolvers.js └── schema.graphql ├── test ├── .eslintrc.json ├── context.test.js ├── helpers │ ├── expectMockFields.js │ ├── expectMockList.js │ └── expectNullable.js ├── index.test.js ├── mocks.test.js └── resolvers.test.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "targets": { 7 | "node": "current" 8 | } 9 | } 10 | ] 11 | ], 12 | "plugins": ["babel-plugin-inline-import"], 13 | "ignore": ["node_modules/**"] 14 | } 15 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | duplication: 4 | enabled: true 5 | config: 6 | languages: 7 | - javascript 8 | eslint: 9 | enabled: true 10 | channel: "eslint-4" 11 | checks: 12 | # This appears to be borked in Code Climate’s config 13 | import/extensions: 14 | enabled: false 15 | fixme: 16 | enabled: true 17 | ratings: 18 | paths: 19 | - "**.js" 20 | exclude_paths: 21 | - coverage/ 22 | - dist/ 23 | - node_modules/ 24 | - test/ 25 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | #indentation 9 | indent_style = space 10 | indent_size = 2 11 | #line breaks 12 | end_of_line = lf 13 | #whitespace 14 | insert_final_newline = true 15 | trim_trailing_whitespace = true 16 | 17 | [*.md] 18 | indent_size = 4 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/*{.,-}min.js 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "parserOptions": { 4 | "ecmaVersion": 2016, 5 | "sourceType": "module" 6 | }, 7 | "plugins": ["prettier"], 8 | "env": { 9 | "es6": true, 10 | "node": true 11 | }, 12 | "rules": { 13 | "prettier/prettier": [ 14 | "error", 15 | { 16 | "bracketSpacing": true, 17 | "trailingComma": "all", 18 | "singleQuote": true 19 | } 20 | ] 21 | }, 22 | "extends": ["airbnb-base", "prettier"] 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS* 2 | node_modules 3 | *.log 4 | .vscode 5 | 6 | # Don’t include the built module; npm will do that for us 7 | dist 8 | 9 | # Testing directories 10 | coverage 11 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | node_modules 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: 3 | yarn: true 4 | directories: 5 | - node_modules 6 | env: 7 | global: 8 | # Code Climate is an excellent tool for tracking code quality. 9 | # It’s free for open source, too! Set it up here: http://bit.ly/2l0mvp9 10 | # 11 | # To track code coverage, you’ll need this test reporter ID. 12 | # Visit Settings => Test coverage on your Code Climate project dashboard 13 | - CC_TEST_REPORTER_ID= 14 | node_js: 15 | - 6 16 | - 8 17 | - 9 18 | notifications: 19 | email: false 20 | before_script: 21 | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 22 | - chmod +x ./cc-test-reporter 23 | - ./cc-test-reporter before-build 24 | script: 25 | - yarn test 26 | after_script: 27 | - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT 28 | after_success: 29 | - yarn build 30 | - yarn semantic-release 31 | branches: 32 | only: 33 | - master 34 | - /^greenkeeper/.*$/ 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | GrAMPS · An easier way to manage the data sources powering your GraphQL server 2 | 3 | # GrAMPS GraphQL Data Source Base 4 | 5 | [![Build Status](https://travis-ci.org/gramps-graphql/data-source-base.svg?branch=master)](https://travis-ci.org/gramps-graphql/data-source-base) [![Maintainability](https://api.codeclimate.com/v1/badges/1858e5dd8acfad0d4540/maintainability)](https://codeclimate.com/github/gramps-graphql/data-source-base/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/1858e5dd8acfad0d4540/test_coverage)](https://codeclimate.com/github/gramps-graphql/data-source-base/test_coverage) [![npm version](https://img.shields.io/npm/v/@gramps/data-source-base.svg?style=flat)](https://www.npmjs.com/package/@gramps/data-source-base) [![Greenkeeper badge](https://badges.greenkeeper.io/gramps-graphql/data-source-base.svg)](https://greenkeeper.io/) 6 | 7 | A boilerplate and minimal example for a [GrAMPS data source](https://gramps.js.org/data-source/data-source-overview/). 8 | 9 | ## Quickstart 10 | 11 | Set up a local data source in seconds with: 12 | 13 | ```bash 14 | # 💥 zero dependencies! no global installs! create a new data source 15 | npx graphql-cli create -b gramps-graphql/data-source-base data-source-mydata 16 | 17 | # 📂 move into the newly-created data source 18 | cd $_ 19 | 20 | # 🚀 start the GraphQL Playground with your spankin’ new data source 21 | yarn dev 22 | ``` 23 | 24 | > **NOTE:** We recommend prefixing data source projects with `data-source-` for clarity and the eventual support of CLI tools to add data sources to your gateway. So if you’re creating a user management data source for the Acme company, we recommend `data-source-acme-users` or `data-source-acmeusers` as a directory/repo name. 25 | 26 | > **ALSO NOTE:** `$_` is a handy shortcut for using the last argument passed to the previous command. It [also does other stuff](https://unix.stackexchange.com/questions/280453/understand-the-meaning-of), but that's a rabbit hole for another time. 27 | 28 | After running `yarn dev`, you’ll see a message with URLs for the GraphQL gateway and the [GraphQL Playground](https://github.com/graphcool/graphql-playground). Open the Playground link (usually http://localhost:8080/playground if you don’t already have something running on port 8080), then run a query: 29 | 30 | ```graphql 31 | { 32 | getById(id: 123) { 33 | id 34 | name 35 | lucky_numbers 36 | } 37 | } 38 | ``` 39 | 40 | ### To Develop with Mock Data 41 | 42 | Add the `--mock` flag to enable mock data, which is helpful for working offline. 43 | 44 | ```sh 45 | # Start the gateway with mock data 46 | yarn dev --mock 47 | ``` 48 | 49 | See `src/mocks.js` to modify your mock resolvers. 50 | 51 | > **NOTE:** For more information on the GrAMPS CLI and its available options, [check out the docs](https://gramps.js.org/cli/cli-overview/). 52 | 53 | ### To Run the Tests 54 | 55 | GrAMPS data sources start you off with 100% test coverage so you can build high-reliability GraphQL servers without a bunch of setup work. 56 | 57 | Run the tests with: 58 | 59 | ```bash 60 | yarn test 61 | ``` 62 | 63 | ## What's Inside? 64 | 65 | Inside, you’ll find: 66 | 67 | * **Schema** — type definitions for GraphQL to interpret the data (see the 68 | [GraphQL docs on schemas](http://graphql.org/learn/schema/)) 69 | * **Resolvers** — functions to map the results of calls to model methods to 70 | the schema 71 | * **Mock Resolvers** — mock functions for offline development 72 | * **Context** — an object with methods to interact with data that is passed to resolver functions 73 | 74 | Each file contains a `TODO` comment explaining the changes you’ll need to make to create a working data source. 75 | 76 | The goal of this repo is to provide enough code to allow a working example of a data source and its related tests, but to limit how much boilerplate needs to be edited to get your own data source implemented. 77 | 78 | ## Code Quality and Continuous Integration 79 | 80 | To help ensure a reliable, easy-to-maintain data source, this example also includes: 81 | 82 | * Configuration for Travis CI (for automated testing) and Code Climate 83 | (for quality analysis) 84 | * Starts you off right with test coverage at 💯 85 | * Provides testing helpers for common resolver testing patterns 86 | * Comes with docs! https://gramps.js.org/data-source/data-source-overview/ 87 | 88 | ### Notes for Developers 89 | 90 | Currently, there is no watch capability (PRs welcome!), so the service needs to be stopped (`control` + `C`) and restarted (`yarn dev`) to reflect new changes to the data source. 91 | -------------------------------------------------------------------------------- /install.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a helper file for use with the GraphQL CLI. It can be ignored if 3 | * you’re not working with the GraphQL CLI. 4 | * 5 | * @see https://github.com/graphql-cli/graphql-cli 6 | */ 7 | 8 | // eslint-disable-next-line import/no-extraneous-dependencies 9 | const globby = require('globby'); 10 | const fs = require('fs'); 11 | const pkg = require('./package.json'); 12 | 13 | function replaceInFile(filePath, searchValue, replaceValue) { 14 | const contents = fs.readFileSync(filePath, 'utf8'); 15 | const newContents = contents.replace( 16 | new RegExp(searchValue, 'g'), 17 | replaceValue, 18 | ); 19 | fs.writeFileSync(filePath, newContents); 20 | } 21 | 22 | function replaceDefaultStringsInFiles( 23 | { namespace, prefix, templateName, project }, 24 | fileGlob = ['{src,test}/**/*', 'package.json'], 25 | ) { 26 | const sourceFiles = globby.sync(fileGlob); 27 | const newType = `${prefix}_${namespace}`; 28 | const placeholder = `GrAMPS Data Source: ${namespace}`; 29 | 30 | sourceFiles.forEach(filePath => { 31 | replaceInFile(filePath, templateName, project); 32 | replaceInFile(filePath, 'PFX_DataSourceBase', newType); 33 | replaceInFile(filePath, 'DataSourceBase', namespace); 34 | replaceInFile(filePath, 'GrAMPS GraphQL Data Source Base', placeholder); 35 | }); 36 | } 37 | 38 | const questions = [ 39 | { 40 | type: 'input', 41 | name: 'namespace', 42 | message: 'Choose a unique namespace. Only letters and numbers are allowed.', 43 | default: 'MyDataSource', 44 | validate: input => !input.match(/\W/), 45 | }, 46 | { 47 | type: 'input', 48 | name: 'prefix', 49 | message: 'Choose a short, unique prefix for your types.', 50 | default: 'PFX', 51 | }, 52 | ]; 53 | 54 | module.exports = async ({ project, context }) => { 55 | const opts = await context.prompt(questions); 56 | 57 | replaceDefaultStringsInFiles({ ...opts, templateName: pkg.name, project }); 58 | 59 | // eslint-disable-next-line no-console 60 | console.log(` 61 | 💥 You’re all set: a new GrAMPS data source has been created! 62 | 63 | 💡 Don’t forget to update "description", "contributors", and 64 | "repository" in \`package.json\` with your own information. 65 | 66 | ✅ Next steps: 67 | 1. Change directory: \`cd ${project}\` 68 | 2. Start local server: \`yarn dev\` 69 | 3. Open the GraphQL Playground: http://localhost:8080/playground 70 | 4. [BONUS] Set up Code Climate: http://bit.ly/2l0mvp9 71 | `); 72 | }; 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@gramps/data-source-base", 3 | "description": "GrAMPS GraphQL Data Source Base", 4 | "contributors": [ 5 | "Jason Lengstorf " 6 | ], 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/gramps-graphql/data-source-base.git" 10 | }, 11 | "main": "dist/index.js", 12 | "directories": { 13 | "test": "test" 14 | }, 15 | "files": [ 16 | "dist/" 17 | ], 18 | "scripts": { 19 | "prepush": "npm test", 20 | "prebuild": "del-cli ./dist", 21 | "build": "babel src -d dist", 22 | "postbuild": "cpy ./src/schema.graphql ./dist", 23 | "dev": "gramps dev --data-source .", 24 | "dev:mock-data": "gramps dev --data-source . --mock", 25 | "lint": "eslint src/", 26 | "test:unit": "cross-env NODE_ENV=test jest --coverage", 27 | "test": "npm run lint --silent && npm run test:unit --silent", 28 | "semantic-release": "semantic-release pre && npm publish && semantic-release post" 29 | }, 30 | "keywords": [ 31 | "graphql" 32 | ], 33 | "license": "MIT", 34 | "dependencies": { 35 | "casual": "^1.5.19" 36 | }, 37 | "peerDependencies": { 38 | "graphql": "^0.12.0", 39 | "graphql-tools": "^1.2.1 || ^2.5.1" 40 | }, 41 | "devDependencies": { 42 | "@gramps/cli": "^1.1.3", 43 | "@gramps/gramps": "^1.1.0", 44 | "babel-cli": "^6.24.1", 45 | "babel-eslint": "^8.0.3", 46 | "babel-jest": "^22.0.4", 47 | "babel-plugin-inline-import": "^2.0.6", 48 | "babel-preset-env": "^1.6.1", 49 | "cpy-cli": "^1.0.1", 50 | "cross-env": "^5.1.3", 51 | "del-cli": "^1.1.0", 52 | "eslint": "^4.13.1", 53 | "eslint-config-airbnb-base": "^12.1.0", 54 | "eslint-config-prettier": "^2.9.0", 55 | "eslint-plugin-import": "^2.8.0", 56 | "eslint-plugin-prettier": "^2.4.0", 57 | "globby": "^7.1.1", 58 | "graphql": "^0.12.3", 59 | "graphql-tools": "^2.14.1", 60 | "husky": "^0.14.3", 61 | "inquirer": "^4.0.1", 62 | "jest": "^22.0.4", 63 | "prettier": "^1.9.2", 64 | "semantic-release": "^11.0.2" 65 | }, 66 | "jest": { 67 | "coverageReporters": [ 68 | "text", 69 | "lcov" 70 | ], 71 | "collectCoverageFrom": [ 72 | "src/**/*.js" 73 | ], 74 | "coverageThreshold": { 75 | "global": { 76 | "branches": 80, 77 | "functions": 80, 78 | "lines": 80, 79 | "statements": 80 80 | } 81 | } 82 | }, 83 | "version": "0.0.0-development" 84 | } 85 | -------------------------------------------------------------------------------- /src/context.js: -------------------------------------------------------------------------------- 1 | // TODO Add methods to access your data. 2 | export default { 3 | getById: id => ({ 4 | id, 5 | name: 'GrAMPS GraphQL Data Source Base', 6 | lucky_numbers: [1, 2, 3, 5, 8, 13, 21], 7 | }), 8 | }; 9 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import typeDefs from './schema.graphql'; 2 | import context from './context'; 3 | import resolvers from './resolvers'; 4 | import mocks from './mocks'; 5 | 6 | /* 7 | * For more information on the building GrAMPS data sources, see 8 | * https://gramps.js.org/data-source/data-source-overview/ 9 | */ 10 | export default { 11 | // TODO: Rename the context to describe the data source. 12 | namespace: 'DataSourceBase', 13 | context, 14 | typeDefs, 15 | resolvers, 16 | mocks, 17 | }; 18 | -------------------------------------------------------------------------------- /src/mocks.js: -------------------------------------------------------------------------------- 1 | import { MockList } from 'graphql-tools'; 2 | import casual from 'casual'; 3 | 4 | export default { 5 | // TODO: Update to mock all schema types and fields 6 | PFX_DataSourceBase: () => ({ 7 | id: casual.uuid, 8 | name: casual.name, 9 | lucky_numbers: () => new MockList([0, 3]), 10 | }), 11 | }; 12 | -------------------------------------------------------------------------------- /src/resolvers.js: -------------------------------------------------------------------------------- 1 | export default { 2 | Query: { 3 | // TODO: Update query resolver name and args to match the schema 4 | // TODO: Update the context method to load the correct data 5 | getById: (_, { id }, context) => context.getById(id), 6 | }, 7 | // TODO: Update to map data to your schema type(s) and field(s) 8 | PFX_DataSourceBase: { 9 | name: data => data.name || null, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /src/schema.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | "TODO: rename and add a description of this query" 3 | getById( 4 | "A unique identifier" 5 | id: ID! 6 | ): PFX_DataSourceBase 7 | } 8 | 9 | "TODO: Choose a unique prefix and rename the type descriptively." 10 | type PFX_DataSourceBase { 11 | "A unique identifier" 12 | id: ID! 13 | "Describe each field to help people use the data more effectively." 14 | name: String 15 | """ 16 | An array of very super lucky numbers. 17 | 18 | DISCLAIMER: The luckiness of these numbers is _not scientifically proven_. 19 | """ 20 | lucky_numbers: [Int] 21 | } 22 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/context.test.js: -------------------------------------------------------------------------------- 1 | import context from '../src/context'; 2 | 3 | describe('Data Source Context', () => { 4 | describe('getById()', () => { 5 | it('loads data by its ID', () => { 6 | expect(context.getById(123)).toEqual({ 7 | id: 123, 8 | name: 'GrAMPS GraphQL Data Source Base', 9 | lucky_numbers: [1, 2, 3, 5, 8, 13, 21], 10 | }); 11 | }); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /test/helpers/expectMockFields.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Tests each field in an array to ensure each is mocked. 3 | * @param {Object} resolver a GraphQL mock resolver 4 | * @param {Array} fieldArray array of fields to check 5 | * @return {boolean} true if the test passed, false otherwise 6 | */ 7 | const expectMockFields = (resolver, fieldArray) => 8 | it('returns the proper mock fields', () => 9 | expect(Object.keys(resolver)).toEqual(fieldArray)); 10 | 11 | export default expectMockFields; 12 | -------------------------------------------------------------------------------- /test/helpers/expectMockList.js: -------------------------------------------------------------------------------- 1 | import { MockList } from 'graphql-tools'; 2 | 3 | /** 4 | * Creates a test for each field to ensure it returns a MockList. 5 | * @param {Object} resolver GraphQL mock resolver object 6 | * @param {Array} fieldArray fields to check 7 | * @return {Array} an array of test results 8 | */ 9 | const expectMockList = (resolver, fieldArray) => 10 | fieldArray.map(field => 11 | it(`returns a MockList for the ${field} field`, () => 12 | expect(resolver[field]()).toBeInstanceOf(MockList)), 13 | ); 14 | 15 | export default expectMockList; 16 | -------------------------------------------------------------------------------- /test/helpers/expectNullable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generates a test for each field to ensure it returns null if it’s not set 3 | * @param {Object} resolver GraphQL resolver object 4 | * @param {Array} fieldArray field names to check 5 | * @return {Array} an array of test results 6 | */ 7 | const expectNullable = (resolver, fieldArray) => 8 | fieldArray.map(field => 9 | it(`returns null if ${field} doesn’t exist`, () => 10 | expect(resolver[field]({})).toBeNull()), 11 | ); 12 | 13 | export default expectNullable; 14 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | import dataSource from '../src'; 2 | 3 | // TODO: Update the data source name. 4 | describe(`Data Source: DataSourceBase`, () => { 5 | it('contains a namespace property', () => { 6 | expect(dataSource.namespace).toBe('DataSourceBase'); 7 | }); 8 | 9 | it('contains a context property', () => { 10 | expect(dataSource.context).toBeTruthy(); 11 | }); 12 | 13 | it('contains a typeDefs property', () => { 14 | expect(dataSource.typeDefs).toBeTruthy(); 15 | }); 16 | 17 | it('contains a resolvers property', () => { 18 | expect(dataSource.resolvers).toBeTruthy(); 19 | }); 20 | 21 | it('contains a mocks property', () => { 22 | expect(dataSource.mocks).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/mocks.test.js: -------------------------------------------------------------------------------- 1 | import expectMockFields from './helpers/expectMockFields'; 2 | import expectMockList from './helpers/expectMockList'; 3 | import mocks from '../src/mocks'; 4 | 5 | describe('mock resolvers', () => { 6 | describe('PFX_DataSourceBase', () => { 7 | const mockResolvers = mocks.PFX_DataSourceBase(); 8 | 9 | // This helper creates a test to ensure each field has a mock resolver. 10 | expectMockFields(mockResolvers, ['id', 'name', 'lucky_numbers']); 11 | 12 | // This helper creates a test to check that these fields return `MockList`s. 13 | expectMockList(mockResolvers, ['lucky_numbers']); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /test/resolvers.test.js: -------------------------------------------------------------------------------- 1 | import resolvers from '../src/resolvers'; 2 | import expectNullable from './helpers/expectNullable'; 3 | 4 | describe('Data Source Resolvers', () => { 5 | describe('query resolvers', () => { 6 | describe('getById()', () => { 7 | it('loads a thing by its ID', () => { 8 | expect.assertions(1); 9 | 10 | const mockContext = { 11 | getById: id => Promise.resolve(id), 12 | }; 13 | 14 | return expect( 15 | resolvers.Query.getById({}, { id: 123 }, mockContext), 16 | ).resolves.toEqual(123); 17 | }); 18 | }); 19 | }); 20 | 21 | describe('PFX_DataSourceBase', () => { 22 | const resolver = resolvers.PFX_DataSourceBase; 23 | 24 | expectNullable(resolver, ['name']); 25 | }); 26 | }); 27 | --------------------------------------------------------------------------------