diff --git a/.gitignore b/.gitignore index 75b1972..bfb2fa8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules lib test/schema.json test/second-schema.json +.vscode/ diff --git a/CHANGELOG.md b/CHANGELOG.md index e2eea16..e481ae9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Change log ### vNEXT +- 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) ### v1.1.0 - Add option to pass schema as string [Christopher Cliff](https://github.com/christophercliff) in [#78](https://github.com/apollographql/eslint-plugin-graphql/pull/78) diff --git a/README.md b/README.md index 32ffa4f..3045a3b 100644 --- a/README.md +++ b/README.md @@ -234,12 +234,12 @@ The full list of available validators is: - `FragmentsOnCompositeTypes` - `KnownArgumentNames` - `KnownDirectives` (*disabled by default in `relay`*) - - `KnownFragmentNames` (*disabled by default in `apollo`, `lokka`, and `relay`*) + - `KnownFragmentNames` (*disabled by default in all envs*) - `KnownTypeNames` - `LoneAnonymousOperation` - `NoFragmentCycles` - `NoUndefinedVariables` (*disabled by default in `relay`*) - - `NoUnusedFragments` (*disabled by default in `apollo`, `lokka`, and `relay`*) + - `NoUnusedFragments` (*disabled by default in all envs*) - `NoUnusedVariables` - `OverlappingFieldsCanBeMerged` - `PossibleFragmentSpreads` @@ -359,7 +359,7 @@ query ViewerName { } ``` -The rule is defined as `graphql/required-fields` and requires a `schema` and `requiredFields`, with an optional `tagName`. +The rule is defined as `graphql/required-fields` and requires a `schema` and `requiredFields`, with an optional `tagName` and `env`. ```js // In a file called .eslintrc.js @@ -368,7 +368,8 @@ module.exports = { 'graphql/required-fields': [ 'error', { - schemaJsonFilepath: require('./schema.json'), + env: 'apollo', + schemaJsonFilepath: path.resolve(__dirname, './schema.json'), requiredFields: ['uuid'], }, ], diff --git a/src/index.js b/src/index.js index 1398491..948d6ee 100644 --- a/src/index.js +++ b/src/index.js @@ -75,6 +75,7 @@ function createRule(context, optionParser) { tagNames.add(tagName); tagRules.push({schema, env, tagName, validators: boundValidators}); } + return { TaggedTemplateExpression(node) { for (const {schema, env, tagName, validators} of tagRules) { @@ -86,7 +87,7 @@ function createRule(context, optionParser) { }; } -const rules = { +export const rules = { 'template-strings': { meta: { schema: { @@ -171,6 +172,14 @@ const rules = { additionalProperties: false, properties: { ...defaultRuleProperties, + env: { + enum: [ + 'lokka', + 'relay', + 'apollo', + 'literal', + ], + }, requiredFields: { type: 'array', items: { @@ -444,11 +453,11 @@ const gqlProcessor = { } } -const processors = reduce(gqlFiles, (result, value) => { +export const processors = reduce(gqlFiles, (result, value) => { return { ...result, [`.${value}`]: gqlProcessor }; }, {}) -module.exports = { - rules, +export default { + rules, processors -}; +} diff --git a/test/__fixtures__/required-fields-invalid-array.graphql b/test/__fixtures__/required-fields-invalid-array.graphql new file mode 100644 index 0000000..0d8c050 --- /dev/null +++ b/test/__fixtures__/required-fields-invalid-array.graphql @@ -0,0 +1 @@ +query { stories { comments { text } } } diff --git a/test/__fixtures__/required-fields-invalid-no-id.graphql b/test/__fixtures__/required-fields-invalid-no-id.graphql new file mode 100644 index 0000000..751a134 --- /dev/null +++ b/test/__fixtures__/required-fields-invalid-no-id.graphql @@ -0,0 +1 @@ +query { greetings { hello } } diff --git a/test/__fixtures__/required-fields-valid-array.graphql b/test/__fixtures__/required-fields-valid-array.graphql new file mode 100644 index 0000000..89cd3e3 --- /dev/null +++ b/test/__fixtures__/required-fields-valid-array.graphql @@ -0,0 +1 @@ +query { stories { id comments { text } } } diff --git a/test/__fixtures__/required-fields-valid-id.graphql b/test/__fixtures__/required-fields-valid-id.graphql new file mode 100644 index 0000000..7d5e7a1 --- /dev/null +++ b/test/__fixtures__/required-fields-valid-id.graphql @@ -0,0 +1 @@ +query { greetings { id, hello, foo } } diff --git a/test/__fixtures__/required-fields-valid-no-id.graphql b/test/__fixtures__/required-fields-valid-no-id.graphql new file mode 100644 index 0000000..bb49957 --- /dev/null +++ b/test/__fixtures__/required-fields-valid-no-id.graphql @@ -0,0 +1 @@ +query { allFilms { films { title } } } diff --git a/test/makeProcessors.js b/test/makeProcessors.js index db01c06..7364094 100644 --- a/test/makeProcessors.js +++ b/test/makeProcessors.js @@ -1,10 +1,38 @@ import assert from 'assert'; -import { - includes, - keys, -} from 'lodash'; +import { CLIEngine } from 'eslint'; +import { includes, keys } from 'lodash'; +import path from 'path'; -import { processors } from '../src'; +import schemaJson from './schema.json'; +import plugin, { processors } from '../src'; + +function execute(file) { + const cli = new CLIEngine({ + extensions: ['.gql', '.graphql'], + baseConfig: { + rules: { + 'graphql/required-fields': [ + 'error', + { + schemaJson, + env: 'literal', + requiredFields: ['id'] + } + ] + } + }, + ignore: false, + useEslintrc: false, + parserOptions: { + ecmaVersion: 6, + sourceType: 'module' + } + }); + cli.addPlugin('eslint-plugin-graphql', plugin); + return cli.executeOnFiles([ + path.join(__dirname, '__fixtures__', `${file}.graphql`) + ]); +} describe('processors', () => { it('should define processors', () => { @@ -15,8 +43,8 @@ describe('processors', () => { }); it('should escape backticks and prepend internalTag', () => { - const query = 'query { someValueWith` }' - const expected = 'ESLintPluginGraphQLFile`query { someValueWith\\` }`' + const query = 'query { someValueWith` }'; + const expected = 'ESLintPluginGraphQLFile`query { someValueWith\\` }`'; const preprocess = processors['.gql'].preprocess; const result = preprocess(query); @@ -28,7 +56,7 @@ describe('processors', () => { { ruleId: 'no-undef' }, { ruleId: 'semi' }, { ruleId: 'graphql/randomString' }, - { ruleId: 'graphql/template-strings' }, + { ruleId: 'graphql/template-strings' } ]; const expected = { ruleId: 'graphql/template-strings' }; const postprocess = processors['.gql'].postprocess; @@ -37,4 +65,33 @@ describe('processors', () => { assert.equal(result.length, 1); assert.equal(result[0].ruleId, expected.ruleId); }); + + describe('graphql/required-fields', () => { + describe('valid', () => { + [ + 'required-fields-valid-no-id', + 'required-fields-valid-id', + 'required-fields-valid-array' + ].forEach(filename => { + it(`does not warn on file ${filename}`, () => { + const results = execute(filename); + assert.equal(results.errorCount, 0); + }); + }); + }); + + describe('invalid', () => { + [ + 'required-fields-invalid-no-id', + 'required-fields-invalid-array' + ].forEach(filename => { + it(`warns on file ${filename}`, () => { + const results = execute(filename); + assert.equal(results.errorCount, 1); + const message = results.results[0].messages[0].message; + assert.ok(new RegExp("'id' field required").test(message)); + }); + }); + }); + }); }); diff --git a/test/makeRule.js b/test/makeRule.js index 15ed05e..bc1b8c7 100644 --- a/test/makeRule.js +++ b/test/makeRule.js @@ -946,7 +946,20 @@ const requiredFieldsTestCases = { invalid: values(namedOperationsValidatorCases).map(({fail: code, errors}) => ({options, parserOptions, code, errors})), }); - // Validate the required-fields rule + // Validate the required-fields rule with env specified + options = [{ + schemaJson, + env: 'apollo', + tagName: 'gql', + requiredFields: ['id'], + }]; + + ruleTester.run('testing required-fields rule', rules['required-fields'], { + valid: requiredFieldsTestCases.pass.map((code) => ({options, parserOptions, code})), + invalid: requiredFieldsTestCases.fail.map(({code, errors}) => ({options, parserOptions, code, errors})), + }); + + // Validate required-fields without optional env argument options = [{ schemaJson, tagName: 'gql',