├── .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 |
2 |
3 | # GrAMPS GraphQL Data Source Base
4 |
5 | [](https://travis-ci.org/gramps-graphql/data-source-base) [](https://codeclimate.com/github/gramps-graphql/data-source-base/maintainability) [](https://codeclimate.com/github/gramps-graphql/data-source-base/test_coverage) [](https://www.npmjs.com/package/@gramps/data-source-base) [](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 |
--------------------------------------------------------------------------------