Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for .graphqlconfig #80

Merged
merged 1 commit into from
Jul 31, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Change log
### vNEXT
- add support for [.graphqlconfig](https://github.com/graphcool/graphql-config) [Roman Hotsiy](https://github.com/RomanGotsiy) in [#80](https://github.com/apollographql/eslint-plugin-graphql/pull/80)

### v1.2.0
- Add env config option for required-fields rule [Justin Schulz](https://github.com/PepperTeasdale) in [#75](https://github.com/apollographql/eslint-plugin-graphql/pull/75)
Expand Down
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,50 @@ module.exports = {
}
```

### Example config when using [.graphqlconfig](https://github.com/graphcool/graphql-config)

If you have `.graphqlconfig` file in the root of your repo you can omit schema-related
properties (`schemaJson`, `schemaJsonFilepath` and `schemaString`) from rule config.

```js
// In a file called .eslintrc.js
module.exports = {
parser: "babel-eslint",
rules: {
"graphql/template-strings": ['error', {
// Import default settings for your GraphQL client. Supported values:
// 'apollo', 'relay', 'lokka', 'literal'
env: 'literal'
// no need to specify schema here, it will be automatically determined using .graphqlconfig
}]
},
plugins: [
'graphql'
]
}
```

In case you use additional schemas, specify `projectName` from `.graphqlconfig` for each `tagName`:
```js
module.exports = {
parser: "babel-eslint",
rules: {
"graphql/template-strings": ['error', {
env: 'apollo',
tagName: 'FirstGQL',
projectName: 'FirstGQLProject'
}, {
env: 'relay',
tagName: 'SecondGQL',
projectName: 'SecondGQLProject'
}]
},
plugins: [
'graphql'
]
}
```

### Selecting Validation Rules

GraphQL validation rules can be configured in the eslint rule configuration using the `validators` option. The default selection depends on the `env` setting. If no `env` is specified, all rules are enabled by default.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"license": "MIT",
"dependencies": {
"graphql": "^0.10.1",
"graphql-config": "^1.0.0",
"lodash": "^4.11.1"
}
}
80 changes: 46 additions & 34 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ import {
includes,
} from 'lodash';

import {
getGraphQLProjectConfig,
ConfigNotFoundError
} from 'graphql-config'

import * as customRules from './rules';

const allGraphQLValidatorNames = allGraphQLValidators.map(rule => rule.name);
Expand Down Expand Up @@ -61,6 +66,9 @@ const defaultRuleProperties = {
type: 'string',
pattern: '^[$_a-zA-Z$_][a-zA-Z0-9$_]+(\\.[a-zA-Z0-9$_]+)?$',
},
projectName: {
type: 'string'
}
}

function createRule(context, optionParser) {
Expand All @@ -87,6 +95,27 @@ function createRule(context, optionParser) {
};
}

const schemaPropsExclusiveness = {
oneOf: [{
required: ['schemaJson'],
not: { required: ['schemaString', 'schemaJsonFilepath', 'projectName']}
}, {
required: ['schemaJsonFilepath'],
not: { required: ['schemaJson', 'schemaString', 'projectName']}
}, {
required: ['schemaString'],
not: { required: ['schemaJson', 'schemaJsonFilepath', 'projectName']}
}, {
not: {
anyOf: [
{ required: ['schemaString'] },
{ required: ['schemaJson'] },
{ required: ['schemaJsonFilepath'] },
]
}
}],
}

export const rules = {
'template-strings': {
meta: {
Expand Down Expand Up @@ -119,17 +148,8 @@ export const rules = {
}],
},
},
// schemaJson, schemaJsonFilepath and schemaString are mutually exclusive:
oneOf: [{
required: ['schemaJson'],
not: { required: ['schemaString', 'schemaJsonFilepath'], },
}, {
required: ['schemaJsonFilepath'],
not: { required: ['schemaString', 'schemaJson'], },
}, {
required: ['schemaString'],
not: { required: ['schemaJson', 'schemaJsonFilepath'], },
}],
// schemaJson, schemaJsonFilepath, schemaString and projectName are mutually exclusive:
...schemaPropsExclusiveness,
}
},
},
Expand All @@ -143,16 +163,7 @@ export const rules = {
items: {
additionalProperties: false,
properties: { ...defaultRuleProperties },
oneOf: [{
required: ['schemaJson'],
not: { required: ['schemaString', 'schemaJsonFilepath'], },
}, {
required: ['schemaJsonFilepath'],
not: { required: ['schemaString', 'schemaJson'], },
}, {
required: ['schemaString'],
not: { required: ['schemaJson', 'schemaJsonFilepath'], },
}],
...schemaPropsExclusiveness,
},
},
},
Expand Down Expand Up @@ -187,16 +198,7 @@ export const rules = {
},
},
},
oneOf: [{
required: ['schemaJson'],
not: { required: ['schemaString', 'schemaJsonFilepath'], },
}, {
required: ['schemaJsonFilepath'],
not: { required: ['schemaString', 'schemaJson'], },
}, {
required: ['schemaString'],
not: { required: ['schemaJson', 'schemaJsonFilepath'], },
}],
...schemaPropsExclusiveness,
},
},
},
Expand All @@ -218,6 +220,7 @@ function parseOptions(optionGroup) {
schemaJsonFilepath, // Or Schema via absolute filepath
schemaString, // Or Schema as string,
env,
projectName,
tagName: tagNameOption,
validators: validatorNamesOption,
} = optionGroup;
Expand All @@ -231,8 +234,17 @@ function parseOptions(optionGroup) {
} else if (schemaString) {
schema = initSchemaFromString(schemaString);
} else {
throw new Error('Must pass in `schemaJson` option with schema object '
+ 'or `schemaJsonFilepath` with absolute path to the json file.');
try {
const config = getGraphQLProjectConfig('.', projectName);
schema = config.getSchema()
} catch (e) {
if (e instanceof ConfigNotFoundError) {
throw new Error('Must provide .graphqlconfig file or pass in `schemaJson` option ' +
'with schema object or `schemaJsonFilepath` with absolute path to the json file.');
}
throw e;
}

}

// Validate env
Expand Down Expand Up @@ -458,6 +470,6 @@ export const processors = reduce(gqlFiles, (result, value) => {
}, {})

export default {
rules,
rules,
processors
}
91 changes: 91 additions & 0 deletions test/graphqlconfig/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { rules } from '../../src';
import { RuleTester } from 'eslint';

const rule = rules['template-strings'];

const parserOptions = {
'ecmaVersion': 6,
'sourceType': 'module',
};

const ruleTester = new RuleTester({parserOptions});

describe('graphqlconfig support', () => {
describe('simple', () => {
const options = [
{ tagName: 'gql' }
];

beforeEach(() => {
process.chdir('test/graphqlconfig/simple');
})

afterEach(() => {
process.chdir('../../..');
})

ruleTester.run('validation works using schema from .graphqlconfig', rule, {
valid: [
{
options,
code: 'const x = gql`{ number, sum(a: 1, b: 1) }`;',
},
],
invalid: [
{
options,
code: 'const y = gql`{ hero(episode: NEWHOPE) { id, name } }`;',
errors: [{
message: 'Cannot query field "hero" on type "RootQuery".',
type: 'TaggedTemplateExpression',
}],
},
],
});
});

describe('multiproject', () => {
const options = [
{ projectName: 'gql', tagName: 'gql' },
{ projectName: 'swapi', tagName: 'swapi' },
];


beforeEach(() => {
process.chdir('test/graphqlconfig/multiproject');
})

afterEach(() => {
process.chdir('../../..');
})

ruleTester.run('validation works using multiple schema from .graphqlconfig', rule, {
valid: [
{
options,
code: [
'const x = gql`{ number, sum(a: 1, b: 1) }`;',
'const y = swapi`{ hero(episode: NEWHOPE) { id, name } }`;',
].join('\n'),
},
],

invalid: [
{
options,
code: [
'const x = swapi`{ number, sum(a: 1, b: 1) }`;',
'const y = gql`{ hero(episode: NEWHOPE) { id, name } }`;',
].join('\n'),
errors: [{
message: 'Cannot query field "number" on type "Query".',
type: 'TaggedTemplateExpression',
}, {
message: 'Cannot query field "hero" on type "RootQuery".',
type: 'TaggedTemplateExpression',
}],
},
],
});
});
});
10 changes: 10 additions & 0 deletions test/graphqlconfig/multiproject/.graphqlconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"projects": {
"gql": {
"schemaPath": "../../schema.graphql"
},
"swapi": {
"schemaPath": "../../second-schema.graphql"
}
}
}
3 changes: 3 additions & 0 deletions test/graphqlconfig/simple/.graphqlconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"schemaPath": "../../schema.graphql"
}
1 change: 1 addition & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ require('babel-core').transform('code', {
// The tests, however, can and should be written with ECMAScript 2015.
require('./makeRule');
require('./makeProcessors');
require('./graphqlconfig/');
Loading