@graphql-eslint/eslint-plugin 3.3.0-alpha-0df1b98.0 → 3.3.0-alpha-b07557f.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/rules/require-id-when-available.md +2 -2
- package/graphql-config.d.ts +2 -1
- package/index.js +121 -83
- package/index.mjs +121 -83
- package/package.json +1 -1
- package/rules/index.d.ts +3 -1
- package/rules/require-id-when-available.d.ts +2 -2
- package/types.d.ts +1 -0
- package/utils.d.ts +1 -0
package/graphql-config.d.ts
CHANGED
@@ -1,3 +1,4 @@
|
|
1
1
|
import { GraphQLConfig } from 'graphql-config';
|
2
2
|
import { ParserOptions } from './types';
|
3
|
-
export declare function
|
3
|
+
export declare function loadCachedGraphQLConfig(options: ParserOptions): GraphQLConfig;
|
4
|
+
export declare function loadGraphQLConfig(options: ParserOptions): GraphQLConfig;
|
package/index.js
CHANGED
@@ -10,10 +10,10 @@ const fs = require('fs');
|
|
10
10
|
const path = require('path');
|
11
11
|
const utils = require('@graphql-tools/utils');
|
12
12
|
const lowerCase = _interopDefault(require('lodash.lowercase'));
|
13
|
+
const graphqlConfig = require('graphql-config');
|
14
|
+
const codeFileLoader = require('@graphql-tools/code-file-loader');
|
13
15
|
const depthLimit = _interopDefault(require('graphql-depth-limit'));
|
14
16
|
const graphqlTagPluck = require('@graphql-tools/graphql-tag-pluck');
|
15
|
-
const graphqlConfig$1 = require('graphql-config');
|
16
|
-
const codeFileLoader = require('@graphql-tools/code-file-loader');
|
17
17
|
const eslint = require('eslint');
|
18
18
|
const codeFrame = require('@babel/code-frame');
|
19
19
|
|
@@ -174,6 +174,47 @@ const configs = {
|
|
174
174
|
'operations-all': operationsAllConfig,
|
175
175
|
};
|
176
176
|
|
177
|
+
let graphQLConfig;
|
178
|
+
function loadCachedGraphQLConfig(options) {
|
179
|
+
// We don't want cache config on test environment
|
180
|
+
// Otherwise schema and documents will be same for all tests
|
181
|
+
if (process.env.NODE_ENV !== 'test' && graphQLConfig) {
|
182
|
+
return graphQLConfig;
|
183
|
+
}
|
184
|
+
graphQLConfig = loadGraphQLConfig(options);
|
185
|
+
return graphQLConfig;
|
186
|
+
}
|
187
|
+
function loadGraphQLConfig(options) {
|
188
|
+
const onDiskConfig = options.skipGraphQLConfig
|
189
|
+
? null
|
190
|
+
: graphqlConfig.loadConfigSync({
|
191
|
+
throwOnEmpty: false,
|
192
|
+
throwOnMissing: false,
|
193
|
+
extensions: [addCodeFileLoaderExtension],
|
194
|
+
});
|
195
|
+
const configOptions = options.projects
|
196
|
+
? { projects: options.projects }
|
197
|
+
: {
|
198
|
+
schema: (options.schema || ''),
|
199
|
+
documents: options.documents || options.operations,
|
200
|
+
extensions: options.extensions,
|
201
|
+
include: options.include,
|
202
|
+
exclude: options.exclude,
|
203
|
+
};
|
204
|
+
graphQLConfig =
|
205
|
+
onDiskConfig ||
|
206
|
+
new graphqlConfig.GraphQLConfig({
|
207
|
+
config: configOptions,
|
208
|
+
filepath: 'virtual-config',
|
209
|
+
}, [addCodeFileLoaderExtension]);
|
210
|
+
return graphQLConfig;
|
211
|
+
}
|
212
|
+
const addCodeFileLoaderExtension = api => {
|
213
|
+
api.loaders.schema.register(new codeFileLoader.CodeFileLoader());
|
214
|
+
api.loaders.documents.register(new codeFileLoader.CodeFileLoader());
|
215
|
+
return { name: 'graphql-eslint-loaders' };
|
216
|
+
};
|
217
|
+
|
177
218
|
function requireSiblingsOperations(ruleName, context) {
|
178
219
|
if (!context.parserServices) {
|
179
220
|
throw new Error(`Rule '${ruleName}' requires 'parserOptions.operations' to be set and loaded. See http://bit.ly/graphql-eslint-operations for more info`);
|
@@ -192,6 +233,32 @@ function requireGraphQLSchemaFromContext(ruleName, context) {
|
|
192
233
|
}
|
193
234
|
return context.parserServices.schema;
|
194
235
|
}
|
236
|
+
const schemaToExtendCache = new Map();
|
237
|
+
function getGraphQLSchemaToExtend(context) {
|
238
|
+
// If parserOptions.schema not set or not loaded, there is no reason to make partial schema aka schemaToExtend
|
239
|
+
if (!context.parserServices.hasTypeInfo) {
|
240
|
+
return null;
|
241
|
+
}
|
242
|
+
const filename = context.getPhysicalFilename();
|
243
|
+
if (!schemaToExtendCache.has(filename)) {
|
244
|
+
const { schema, schemaOptions } = context.parserOptions;
|
245
|
+
const gqlConfig = loadGraphQLConfig({ schema });
|
246
|
+
const projectForFile = gqlConfig.getProjectForFile(filename);
|
247
|
+
let schemaToExtend;
|
248
|
+
try {
|
249
|
+
schemaToExtend = projectForFile.loadSchemaSync(projectForFile.schema, 'GraphQLSchema', {
|
250
|
+
...schemaOptions,
|
251
|
+
ignore: filename,
|
252
|
+
});
|
253
|
+
}
|
254
|
+
catch (_a) {
|
255
|
+
// If error throws just ignore it because maybe schema is located in 1 file
|
256
|
+
schemaToExtend = null;
|
257
|
+
}
|
258
|
+
schemaToExtendCache.set(filename, schemaToExtend);
|
259
|
+
}
|
260
|
+
return schemaToExtendCache.get(filename);
|
261
|
+
}
|
195
262
|
function requireReachableTypesFromContext(ruleName, context) {
|
196
263
|
const schema = requireGraphQLSchemaFromContext(ruleName, context);
|
197
264
|
return context.parserServices.reachableTypes(schema);
|
@@ -326,14 +393,14 @@ function getLocation(loc, fieldName = '', offset) {
|
|
326
393
|
};
|
327
394
|
}
|
328
395
|
|
329
|
-
function validateDocument(sourceNode, context, schema, documentNode, rule) {
|
396
|
+
function validateDocument(sourceNode, context, schema = null, documentNode, rule, isSchemaToExtend = false) {
|
330
397
|
if (documentNode.definitions.length === 0) {
|
331
398
|
return;
|
332
399
|
}
|
333
400
|
try {
|
334
|
-
const validationErrors = schema
|
401
|
+
const validationErrors = schema && !isSchemaToExtend
|
335
402
|
? graphql.validate(schema, documentNode, [rule])
|
336
|
-
: validate.validateSDL(documentNode,
|
403
|
+
: validate.validateSDL(documentNode, schema, [rule]);
|
337
404
|
for (const error of validationErrors) {
|
338
405
|
/*
|
339
406
|
* TODO: Fix ESTree-AST converter because currently it's incorrectly convert loc.end
|
@@ -440,18 +507,24 @@ const validationToRule = (ruleId, ruleName, docs, getDocumentNode) => {
|
|
440
507
|
},
|
441
508
|
},
|
442
509
|
create(context) {
|
510
|
+
if (!ruleFn) {
|
511
|
+
// eslint-disable-next-line no-console
|
512
|
+
console.warn(`You rule "${ruleId}" depends on a GraphQL validation rule "${ruleName}" but it's not available in the "graphql-js" version you are using. Skipping...`);
|
513
|
+
return {};
|
514
|
+
}
|
515
|
+
let schema;
|
516
|
+
if (docs.requiresSchemaToExtend) {
|
517
|
+
schema = getGraphQLSchemaToExtend(context);
|
518
|
+
}
|
519
|
+
if (docs.requiresSchema) {
|
520
|
+
schema = requireGraphQLSchemaFromContext(ruleId, context);
|
521
|
+
}
|
443
522
|
return {
|
444
523
|
Document(node) {
|
445
|
-
if (!ruleFn) {
|
446
|
-
// eslint-disable-next-line no-console
|
447
|
-
console.warn(`You rule "${ruleId}" depends on a GraphQL validation rule "${ruleName}" but it's not available in the "graphql-js" version you are using. Skipping...`);
|
448
|
-
return;
|
449
|
-
}
|
450
|
-
const schema = docs.requiresSchema ? requireGraphQLSchemaFromContext(ruleId, context) : null;
|
451
524
|
const documentNode = getDocumentNode
|
452
525
|
? getDocumentNode({ ruleId, context, schema, node: node.rawNode() })
|
453
526
|
: node.rawNode();
|
454
|
-
validateDocument(node, context, schema, documentNode, ruleFn);
|
527
|
+
validateDocument(node, context, schema, documentNode, ruleFn, docs.requiresSchemaToExtend);
|
455
528
|
},
|
456
529
|
};
|
457
530
|
},
|
@@ -601,7 +674,8 @@ const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-de
|
|
601
674
|
}), validationToRule('possible-type-extension', 'PossibleTypeExtensions', {
|
602
675
|
category: 'Schema',
|
603
676
|
description: `A type extension is only valid if the type is defined and has the same kind.`,
|
604
|
-
recommended: false,
|
677
|
+
recommended: false,
|
678
|
+
requiresSchemaToExtend: true,
|
605
679
|
}), validationToRule('provided-required-arguments', 'ProvidedRequiredArguments', {
|
606
680
|
category: ['Schema', 'Operations'],
|
607
681
|
description: `A field or directive is only valid if all required (non-null without a default value) field arguments have been provided.`,
|
@@ -2866,8 +2940,7 @@ const convertNode = (typeInfo) => (node, key, parent) => {
|
|
2866
2940
|
}
|
2867
2941
|
};
|
2868
2942
|
|
2869
|
-
const
|
2870
|
-
const MESSAGE_ID = 'REQUIRE_ID_WHEN_AVAILABLE';
|
2943
|
+
const REQUIRE_ID_WHEN_AVAILABLE = 'REQUIRE_ID_WHEN_AVAILABLE';
|
2871
2944
|
const DEFAULT_ID_FIELD_NAME = 'id';
|
2872
2945
|
const rule$j = {
|
2873
2946
|
meta: {
|
@@ -2875,7 +2948,7 @@ const rule$j = {
|
|
2875
2948
|
docs: {
|
2876
2949
|
category: 'Operations',
|
2877
2950
|
description: 'Enforce selecting specific fields when they are available on the GraphQL type.',
|
2878
|
-
url:
|
2951
|
+
url: 'https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/require-id-when-available.md',
|
2879
2952
|
requiresSchema: true,
|
2880
2953
|
requiresSiblings: true,
|
2881
2954
|
examples: [
|
@@ -2889,7 +2962,7 @@ const rule$j = {
|
|
2889
2962
|
}
|
2890
2963
|
|
2891
2964
|
# Query
|
2892
|
-
query {
|
2965
|
+
query user {
|
2893
2966
|
user {
|
2894
2967
|
name
|
2895
2968
|
}
|
@@ -2906,7 +2979,7 @@ const rule$j = {
|
|
2906
2979
|
}
|
2907
2980
|
|
2908
2981
|
# Query
|
2909
|
-
query {
|
2982
|
+
query user {
|
2910
2983
|
user {
|
2911
2984
|
id
|
2912
2985
|
name
|
@@ -2918,7 +2991,7 @@ const rule$j = {
|
|
2918
2991
|
recommended: true,
|
2919
2992
|
},
|
2920
2993
|
messages: {
|
2921
|
-
[
|
2994
|
+
[REQUIRE_ID_WHEN_AVAILABLE]: [
|
2922
2995
|
`Field {{ fieldName }} must be selected when it's available on a type. Please make sure to include it in your selection set!`,
|
2923
2996
|
`If you are using fragments, make sure that all used fragments {{ checkedFragments }}specifies the field {{ fieldName }}.`,
|
2924
2997
|
].join('\n'),
|
@@ -2949,16 +3022,14 @@ const rule$j = {
|
|
2949
3022
|
},
|
2950
3023
|
},
|
2951
3024
|
create(context) {
|
2952
|
-
requireGraphQLSchemaFromContext(
|
2953
|
-
const siblings = requireSiblingsOperations(
|
3025
|
+
requireGraphQLSchemaFromContext('require-id-when-available', context);
|
3026
|
+
const siblings = requireSiblingsOperations('require-id-when-available', context);
|
2954
3027
|
const { fieldName = DEFAULT_ID_FIELD_NAME } = context.options[0] || {};
|
2955
|
-
const idNames =
|
3028
|
+
const idNames = Array.isArray(fieldName) ? fieldName : [fieldName];
|
2956
3029
|
const isFound = (s) => s.kind === graphql.Kind.FIELD && idNames.includes(s.name.value);
|
2957
|
-
// Skip check selections in FragmentDefinition
|
2958
|
-
const selector = 'OperationDefinition SelectionSet[parent.kind!=OperationDefinition]';
|
2959
3030
|
return {
|
2960
|
-
|
2961
|
-
var _a;
|
3031
|
+
SelectionSet(node) {
|
3032
|
+
var _a, _b;
|
2962
3033
|
const typeInfo = node.typeInfo();
|
2963
3034
|
if (!typeInfo.gqlType) {
|
2964
3035
|
return;
|
@@ -2975,38 +3046,41 @@ const rule$j = {
|
|
2975
3046
|
return;
|
2976
3047
|
}
|
2977
3048
|
const checkedFragmentSpreads = new Set();
|
3049
|
+
let found = false;
|
2978
3050
|
for (const selection of node.selections) {
|
2979
3051
|
if (isFound(selection)) {
|
2980
|
-
|
3052
|
+
found = true;
|
2981
3053
|
}
|
2982
|
-
if (selection.kind === graphql.Kind.INLINE_FRAGMENT
|
2983
|
-
|
3054
|
+
else if (selection.kind === graphql.Kind.INLINE_FRAGMENT) {
|
3055
|
+
found = (_a = selection.selectionSet) === null || _a === void 0 ? void 0 : _a.selections.some(s => isFound(s));
|
2984
3056
|
}
|
2985
|
-
if (selection.kind === graphql.Kind.FRAGMENT_SPREAD) {
|
3057
|
+
else if (selection.kind === graphql.Kind.FRAGMENT_SPREAD) {
|
2986
3058
|
const [foundSpread] = siblings.getFragment(selection.name.value);
|
2987
3059
|
if (foundSpread) {
|
2988
3060
|
checkedFragmentSpreads.add(foundSpread.document.name.value);
|
2989
|
-
|
2990
|
-
return;
|
2991
|
-
}
|
3061
|
+
found = (_b = foundSpread.document.selectionSet) === null || _b === void 0 ? void 0 : _b.selections.some(s => isFound(s));
|
2992
3062
|
}
|
2993
3063
|
}
|
3064
|
+
if (found) {
|
3065
|
+
break;
|
3066
|
+
}
|
2994
3067
|
}
|
2995
3068
|
const { parent } = node;
|
2996
|
-
const hasIdFieldInInterfaceSelectionSet =
|
2997
|
-
|
2998
|
-
parent.parent
|
2999
|
-
|
3000
|
-
|
3069
|
+
const hasIdFieldInInterfaceSelectionSet = parent &&
|
3070
|
+
parent.kind === graphql.Kind.INLINE_FRAGMENT &&
|
3071
|
+
parent.parent &&
|
3072
|
+
parent.parent.kind === graphql.Kind.SELECTION_SET &&
|
3073
|
+
parent.parent.selections.some(s => isFound(s));
|
3074
|
+
if (!found && !hasIdFieldInInterfaceSelectionSet) {
|
3075
|
+
context.report({
|
3076
|
+
loc: getLocation(node.loc),
|
3077
|
+
messageId: REQUIRE_ID_WHEN_AVAILABLE,
|
3078
|
+
data: {
|
3079
|
+
checkedFragments: checkedFragmentSpreads.size === 0 ? '' : `(${[...checkedFragmentSpreads].join(', ')}) `,
|
3080
|
+
fieldName: idNames.map(name => `"${name}"`).join(' or '),
|
3081
|
+
},
|
3082
|
+
});
|
3001
3083
|
}
|
3002
|
-
context.report({
|
3003
|
-
loc: getLocation(node.loc),
|
3004
|
-
messageId: MESSAGE_ID,
|
3005
|
-
data: {
|
3006
|
-
checkedFragments: checkedFragmentSpreads.size === 0 ? '' : `(${[...checkedFragmentSpreads].join(', ')}) `,
|
3007
|
-
fieldName: idNames.map(name => `"${name}"`).join(' or '),
|
3008
|
-
},
|
3009
|
-
});
|
3010
3084
|
},
|
3011
3085
|
};
|
3012
3086
|
},
|
@@ -3701,42 +3775,6 @@ function getSiblingOperations(options, gqlConfig) {
|
|
3701
3775
|
return siblingOperations;
|
3702
3776
|
}
|
3703
3777
|
|
3704
|
-
let graphqlConfig;
|
3705
|
-
function loadGraphqlConfig(options) {
|
3706
|
-
// We don't want cache config on test environment
|
3707
|
-
// Otherwise schema and documents will be same for all tests
|
3708
|
-
if (process.env.NODE_ENV !== 'test' && graphqlConfig) {
|
3709
|
-
return graphqlConfig;
|
3710
|
-
}
|
3711
|
-
const onDiskConfig = options.skipGraphQLConfig
|
3712
|
-
? null
|
3713
|
-
: graphqlConfig$1.loadConfigSync({
|
3714
|
-
throwOnEmpty: false,
|
3715
|
-
throwOnMissing: false,
|
3716
|
-
extensions: [addCodeFileLoaderExtension],
|
3717
|
-
});
|
3718
|
-
graphqlConfig =
|
3719
|
-
onDiskConfig ||
|
3720
|
-
new graphqlConfig$1.GraphQLConfig({
|
3721
|
-
config: options.projects
|
3722
|
-
? { projects: options.projects }
|
3723
|
-
: {
|
3724
|
-
schema: (options.schema || ''),
|
3725
|
-
documents: options.documents || options.operations,
|
3726
|
-
extensions: options.extensions,
|
3727
|
-
include: options.include,
|
3728
|
-
exclude: options.exclude,
|
3729
|
-
},
|
3730
|
-
filepath: 'virtual-config',
|
3731
|
-
}, [addCodeFileLoaderExtension]);
|
3732
|
-
return graphqlConfig;
|
3733
|
-
}
|
3734
|
-
const addCodeFileLoaderExtension = api => {
|
3735
|
-
api.loaders.schema.register(new codeFileLoader.CodeFileLoader());
|
3736
|
-
api.loaders.documents.register(new codeFileLoader.CodeFileLoader());
|
3737
|
-
return { name: 'graphql-eslint-loaders' };
|
3738
|
-
};
|
3739
|
-
|
3740
3778
|
let reachableTypesCache;
|
3741
3779
|
function getReachableTypes(schema) {
|
3742
3780
|
// We don't want cache reachableTypes on test environment
|
@@ -3819,7 +3857,7 @@ function parse(code, options) {
|
|
3819
3857
|
return parseForESLint(code, options).ast;
|
3820
3858
|
}
|
3821
3859
|
function parseForESLint(code, options = {}) {
|
3822
|
-
const gqlConfig =
|
3860
|
+
const gqlConfig = loadCachedGraphQLConfig(options);
|
3823
3861
|
const schema = getSchema(options, gqlConfig);
|
3824
3862
|
const parserServices = {
|
3825
3863
|
hasTypeInfo: schema !== null,
|
package/index.mjs
CHANGED
@@ -4,10 +4,10 @@ import { statSync, existsSync, readFileSync } from 'fs';
|
|
4
4
|
import { dirname, extname, basename, relative, resolve } from 'path';
|
5
5
|
import { asArray, parseGraphQLSDL } from '@graphql-tools/utils';
|
6
6
|
import lowerCase from 'lodash.lowercase';
|
7
|
-
import depthLimit from 'graphql-depth-limit';
|
8
|
-
import { parseCode } from '@graphql-tools/graphql-tag-pluck';
|
9
7
|
import { loadConfigSync, GraphQLConfig } from 'graphql-config';
|
10
8
|
import { CodeFileLoader } from '@graphql-tools/code-file-loader';
|
9
|
+
import depthLimit from 'graphql-depth-limit';
|
10
|
+
import { parseCode } from '@graphql-tools/graphql-tag-pluck';
|
11
11
|
import { RuleTester, Linter } from 'eslint';
|
12
12
|
import { codeFrameColumns } from '@babel/code-frame';
|
13
13
|
|
@@ -168,6 +168,47 @@ const configs = {
|
|
168
168
|
'operations-all': operationsAllConfig,
|
169
169
|
};
|
170
170
|
|
171
|
+
let graphQLConfig;
|
172
|
+
function loadCachedGraphQLConfig(options) {
|
173
|
+
// We don't want cache config on test environment
|
174
|
+
// Otherwise schema and documents will be same for all tests
|
175
|
+
if (process.env.NODE_ENV !== 'test' && graphQLConfig) {
|
176
|
+
return graphQLConfig;
|
177
|
+
}
|
178
|
+
graphQLConfig = loadGraphQLConfig(options);
|
179
|
+
return graphQLConfig;
|
180
|
+
}
|
181
|
+
function loadGraphQLConfig(options) {
|
182
|
+
const onDiskConfig = options.skipGraphQLConfig
|
183
|
+
? null
|
184
|
+
: loadConfigSync({
|
185
|
+
throwOnEmpty: false,
|
186
|
+
throwOnMissing: false,
|
187
|
+
extensions: [addCodeFileLoaderExtension],
|
188
|
+
});
|
189
|
+
const configOptions = options.projects
|
190
|
+
? { projects: options.projects }
|
191
|
+
: {
|
192
|
+
schema: (options.schema || ''),
|
193
|
+
documents: options.documents || options.operations,
|
194
|
+
extensions: options.extensions,
|
195
|
+
include: options.include,
|
196
|
+
exclude: options.exclude,
|
197
|
+
};
|
198
|
+
graphQLConfig =
|
199
|
+
onDiskConfig ||
|
200
|
+
new GraphQLConfig({
|
201
|
+
config: configOptions,
|
202
|
+
filepath: 'virtual-config',
|
203
|
+
}, [addCodeFileLoaderExtension]);
|
204
|
+
return graphQLConfig;
|
205
|
+
}
|
206
|
+
const addCodeFileLoaderExtension = api => {
|
207
|
+
api.loaders.schema.register(new CodeFileLoader());
|
208
|
+
api.loaders.documents.register(new CodeFileLoader());
|
209
|
+
return { name: 'graphql-eslint-loaders' };
|
210
|
+
};
|
211
|
+
|
171
212
|
function requireSiblingsOperations(ruleName, context) {
|
172
213
|
if (!context.parserServices) {
|
173
214
|
throw new Error(`Rule '${ruleName}' requires 'parserOptions.operations' to be set and loaded. See http://bit.ly/graphql-eslint-operations for more info`);
|
@@ -186,6 +227,32 @@ function requireGraphQLSchemaFromContext(ruleName, context) {
|
|
186
227
|
}
|
187
228
|
return context.parserServices.schema;
|
188
229
|
}
|
230
|
+
const schemaToExtendCache = new Map();
|
231
|
+
function getGraphQLSchemaToExtend(context) {
|
232
|
+
// If parserOptions.schema not set or not loaded, there is no reason to make partial schema aka schemaToExtend
|
233
|
+
if (!context.parserServices.hasTypeInfo) {
|
234
|
+
return null;
|
235
|
+
}
|
236
|
+
const filename = context.getPhysicalFilename();
|
237
|
+
if (!schemaToExtendCache.has(filename)) {
|
238
|
+
const { schema, schemaOptions } = context.parserOptions;
|
239
|
+
const gqlConfig = loadGraphQLConfig({ schema });
|
240
|
+
const projectForFile = gqlConfig.getProjectForFile(filename);
|
241
|
+
let schemaToExtend;
|
242
|
+
try {
|
243
|
+
schemaToExtend = projectForFile.loadSchemaSync(projectForFile.schema, 'GraphQLSchema', {
|
244
|
+
...schemaOptions,
|
245
|
+
ignore: filename,
|
246
|
+
});
|
247
|
+
}
|
248
|
+
catch (_a) {
|
249
|
+
// If error throws just ignore it because maybe schema is located in 1 file
|
250
|
+
schemaToExtend = null;
|
251
|
+
}
|
252
|
+
schemaToExtendCache.set(filename, schemaToExtend);
|
253
|
+
}
|
254
|
+
return schemaToExtendCache.get(filename);
|
255
|
+
}
|
189
256
|
function requireReachableTypesFromContext(ruleName, context) {
|
190
257
|
const schema = requireGraphQLSchemaFromContext(ruleName, context);
|
191
258
|
return context.parserServices.reachableTypes(schema);
|
@@ -320,14 +387,14 @@ function getLocation(loc, fieldName = '', offset) {
|
|
320
387
|
};
|
321
388
|
}
|
322
389
|
|
323
|
-
function validateDocument(sourceNode, context, schema, documentNode, rule) {
|
390
|
+
function validateDocument(sourceNode, context, schema = null, documentNode, rule, isSchemaToExtend = false) {
|
324
391
|
if (documentNode.definitions.length === 0) {
|
325
392
|
return;
|
326
393
|
}
|
327
394
|
try {
|
328
|
-
const validationErrors = schema
|
395
|
+
const validationErrors = schema && !isSchemaToExtend
|
329
396
|
? validate(schema, documentNode, [rule])
|
330
|
-
: validateSDL(documentNode,
|
397
|
+
: validateSDL(documentNode, schema, [rule]);
|
331
398
|
for (const error of validationErrors) {
|
332
399
|
/*
|
333
400
|
* TODO: Fix ESTree-AST converter because currently it's incorrectly convert loc.end
|
@@ -434,18 +501,24 @@ const validationToRule = (ruleId, ruleName, docs, getDocumentNode) => {
|
|
434
501
|
},
|
435
502
|
},
|
436
503
|
create(context) {
|
504
|
+
if (!ruleFn) {
|
505
|
+
// eslint-disable-next-line no-console
|
506
|
+
console.warn(`You rule "${ruleId}" depends on a GraphQL validation rule "${ruleName}" but it's not available in the "graphql-js" version you are using. Skipping...`);
|
507
|
+
return {};
|
508
|
+
}
|
509
|
+
let schema;
|
510
|
+
if (docs.requiresSchemaToExtend) {
|
511
|
+
schema = getGraphQLSchemaToExtend(context);
|
512
|
+
}
|
513
|
+
if (docs.requiresSchema) {
|
514
|
+
schema = requireGraphQLSchemaFromContext(ruleId, context);
|
515
|
+
}
|
437
516
|
return {
|
438
517
|
Document(node) {
|
439
|
-
if (!ruleFn) {
|
440
|
-
// eslint-disable-next-line no-console
|
441
|
-
console.warn(`You rule "${ruleId}" depends on a GraphQL validation rule "${ruleName}" but it's not available in the "graphql-js" version you are using. Skipping...`);
|
442
|
-
return;
|
443
|
-
}
|
444
|
-
const schema = docs.requiresSchema ? requireGraphQLSchemaFromContext(ruleId, context) : null;
|
445
518
|
const documentNode = getDocumentNode
|
446
519
|
? getDocumentNode({ ruleId, context, schema, node: node.rawNode() })
|
447
520
|
: node.rawNode();
|
448
|
-
validateDocument(node, context, schema, documentNode, ruleFn);
|
521
|
+
validateDocument(node, context, schema, documentNode, ruleFn, docs.requiresSchemaToExtend);
|
449
522
|
},
|
450
523
|
};
|
451
524
|
},
|
@@ -595,7 +668,8 @@ const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-de
|
|
595
668
|
}), validationToRule('possible-type-extension', 'PossibleTypeExtensions', {
|
596
669
|
category: 'Schema',
|
597
670
|
description: `A type extension is only valid if the type is defined and has the same kind.`,
|
598
|
-
recommended: false,
|
671
|
+
recommended: false,
|
672
|
+
requiresSchemaToExtend: true,
|
599
673
|
}), validationToRule('provided-required-arguments', 'ProvidedRequiredArguments', {
|
600
674
|
category: ['Schema', 'Operations'],
|
601
675
|
description: `A field or directive is only valid if all required (non-null without a default value) field arguments have been provided.`,
|
@@ -2860,8 +2934,7 @@ const convertNode = (typeInfo) => (node, key, parent) => {
|
|
2860
2934
|
}
|
2861
2935
|
};
|
2862
2936
|
|
2863
|
-
const
|
2864
|
-
const MESSAGE_ID = 'REQUIRE_ID_WHEN_AVAILABLE';
|
2937
|
+
const REQUIRE_ID_WHEN_AVAILABLE = 'REQUIRE_ID_WHEN_AVAILABLE';
|
2865
2938
|
const DEFAULT_ID_FIELD_NAME = 'id';
|
2866
2939
|
const rule$j = {
|
2867
2940
|
meta: {
|
@@ -2869,7 +2942,7 @@ const rule$j = {
|
|
2869
2942
|
docs: {
|
2870
2943
|
category: 'Operations',
|
2871
2944
|
description: 'Enforce selecting specific fields when they are available on the GraphQL type.',
|
2872
|
-
url:
|
2945
|
+
url: 'https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/require-id-when-available.md',
|
2873
2946
|
requiresSchema: true,
|
2874
2947
|
requiresSiblings: true,
|
2875
2948
|
examples: [
|
@@ -2883,7 +2956,7 @@ const rule$j = {
|
|
2883
2956
|
}
|
2884
2957
|
|
2885
2958
|
# Query
|
2886
|
-
query {
|
2959
|
+
query user {
|
2887
2960
|
user {
|
2888
2961
|
name
|
2889
2962
|
}
|
@@ -2900,7 +2973,7 @@ const rule$j = {
|
|
2900
2973
|
}
|
2901
2974
|
|
2902
2975
|
# Query
|
2903
|
-
query {
|
2976
|
+
query user {
|
2904
2977
|
user {
|
2905
2978
|
id
|
2906
2979
|
name
|
@@ -2912,7 +2985,7 @@ const rule$j = {
|
|
2912
2985
|
recommended: true,
|
2913
2986
|
},
|
2914
2987
|
messages: {
|
2915
|
-
[
|
2988
|
+
[REQUIRE_ID_WHEN_AVAILABLE]: [
|
2916
2989
|
`Field {{ fieldName }} must be selected when it's available on a type. Please make sure to include it in your selection set!`,
|
2917
2990
|
`If you are using fragments, make sure that all used fragments {{ checkedFragments }}specifies the field {{ fieldName }}.`,
|
2918
2991
|
].join('\n'),
|
@@ -2943,16 +3016,14 @@ const rule$j = {
|
|
2943
3016
|
},
|
2944
3017
|
},
|
2945
3018
|
create(context) {
|
2946
|
-
requireGraphQLSchemaFromContext(
|
2947
|
-
const siblings = requireSiblingsOperations(
|
3019
|
+
requireGraphQLSchemaFromContext('require-id-when-available', context);
|
3020
|
+
const siblings = requireSiblingsOperations('require-id-when-available', context);
|
2948
3021
|
const { fieldName = DEFAULT_ID_FIELD_NAME } = context.options[0] || {};
|
2949
|
-
const idNames =
|
3022
|
+
const idNames = Array.isArray(fieldName) ? fieldName : [fieldName];
|
2950
3023
|
const isFound = (s) => s.kind === Kind.FIELD && idNames.includes(s.name.value);
|
2951
|
-
// Skip check selections in FragmentDefinition
|
2952
|
-
const selector = 'OperationDefinition SelectionSet[parent.kind!=OperationDefinition]';
|
2953
3024
|
return {
|
2954
|
-
|
2955
|
-
var _a;
|
3025
|
+
SelectionSet(node) {
|
3026
|
+
var _a, _b;
|
2956
3027
|
const typeInfo = node.typeInfo();
|
2957
3028
|
if (!typeInfo.gqlType) {
|
2958
3029
|
return;
|
@@ -2969,38 +3040,41 @@ const rule$j = {
|
|
2969
3040
|
return;
|
2970
3041
|
}
|
2971
3042
|
const checkedFragmentSpreads = new Set();
|
3043
|
+
let found = false;
|
2972
3044
|
for (const selection of node.selections) {
|
2973
3045
|
if (isFound(selection)) {
|
2974
|
-
|
3046
|
+
found = true;
|
2975
3047
|
}
|
2976
|
-
if (selection.kind === Kind.INLINE_FRAGMENT
|
2977
|
-
|
3048
|
+
else if (selection.kind === Kind.INLINE_FRAGMENT) {
|
3049
|
+
found = (_a = selection.selectionSet) === null || _a === void 0 ? void 0 : _a.selections.some(s => isFound(s));
|
2978
3050
|
}
|
2979
|
-
if (selection.kind === Kind.FRAGMENT_SPREAD) {
|
3051
|
+
else if (selection.kind === Kind.FRAGMENT_SPREAD) {
|
2980
3052
|
const [foundSpread] = siblings.getFragment(selection.name.value);
|
2981
3053
|
if (foundSpread) {
|
2982
3054
|
checkedFragmentSpreads.add(foundSpread.document.name.value);
|
2983
|
-
|
2984
|
-
return;
|
2985
|
-
}
|
3055
|
+
found = (_b = foundSpread.document.selectionSet) === null || _b === void 0 ? void 0 : _b.selections.some(s => isFound(s));
|
2986
3056
|
}
|
2987
3057
|
}
|
3058
|
+
if (found) {
|
3059
|
+
break;
|
3060
|
+
}
|
2988
3061
|
}
|
2989
3062
|
const { parent } = node;
|
2990
|
-
const hasIdFieldInInterfaceSelectionSet =
|
2991
|
-
|
2992
|
-
parent.parent
|
2993
|
-
|
2994
|
-
|
3063
|
+
const hasIdFieldInInterfaceSelectionSet = parent &&
|
3064
|
+
parent.kind === Kind.INLINE_FRAGMENT &&
|
3065
|
+
parent.parent &&
|
3066
|
+
parent.parent.kind === Kind.SELECTION_SET &&
|
3067
|
+
parent.parent.selections.some(s => isFound(s));
|
3068
|
+
if (!found && !hasIdFieldInInterfaceSelectionSet) {
|
3069
|
+
context.report({
|
3070
|
+
loc: getLocation(node.loc),
|
3071
|
+
messageId: REQUIRE_ID_WHEN_AVAILABLE,
|
3072
|
+
data: {
|
3073
|
+
checkedFragments: checkedFragmentSpreads.size === 0 ? '' : `(${[...checkedFragmentSpreads].join(', ')}) `,
|
3074
|
+
fieldName: idNames.map(name => `"${name}"`).join(' or '),
|
3075
|
+
},
|
3076
|
+
});
|
2995
3077
|
}
|
2996
|
-
context.report({
|
2997
|
-
loc: getLocation(node.loc),
|
2998
|
-
messageId: MESSAGE_ID,
|
2999
|
-
data: {
|
3000
|
-
checkedFragments: checkedFragmentSpreads.size === 0 ? '' : `(${[...checkedFragmentSpreads].join(', ')}) `,
|
3001
|
-
fieldName: idNames.map(name => `"${name}"`).join(' or '),
|
3002
|
-
},
|
3003
|
-
});
|
3004
3078
|
},
|
3005
3079
|
};
|
3006
3080
|
},
|
@@ -3695,42 +3769,6 @@ function getSiblingOperations(options, gqlConfig) {
|
|
3695
3769
|
return siblingOperations;
|
3696
3770
|
}
|
3697
3771
|
|
3698
|
-
let graphqlConfig;
|
3699
|
-
function loadGraphqlConfig(options) {
|
3700
|
-
// We don't want cache config on test environment
|
3701
|
-
// Otherwise schema and documents will be same for all tests
|
3702
|
-
if (process.env.NODE_ENV !== 'test' && graphqlConfig) {
|
3703
|
-
return graphqlConfig;
|
3704
|
-
}
|
3705
|
-
const onDiskConfig = options.skipGraphQLConfig
|
3706
|
-
? null
|
3707
|
-
: loadConfigSync({
|
3708
|
-
throwOnEmpty: false,
|
3709
|
-
throwOnMissing: false,
|
3710
|
-
extensions: [addCodeFileLoaderExtension],
|
3711
|
-
});
|
3712
|
-
graphqlConfig =
|
3713
|
-
onDiskConfig ||
|
3714
|
-
new GraphQLConfig({
|
3715
|
-
config: options.projects
|
3716
|
-
? { projects: options.projects }
|
3717
|
-
: {
|
3718
|
-
schema: (options.schema || ''),
|
3719
|
-
documents: options.documents || options.operations,
|
3720
|
-
extensions: options.extensions,
|
3721
|
-
include: options.include,
|
3722
|
-
exclude: options.exclude,
|
3723
|
-
},
|
3724
|
-
filepath: 'virtual-config',
|
3725
|
-
}, [addCodeFileLoaderExtension]);
|
3726
|
-
return graphqlConfig;
|
3727
|
-
}
|
3728
|
-
const addCodeFileLoaderExtension = api => {
|
3729
|
-
api.loaders.schema.register(new CodeFileLoader());
|
3730
|
-
api.loaders.documents.register(new CodeFileLoader());
|
3731
|
-
return { name: 'graphql-eslint-loaders' };
|
3732
|
-
};
|
3733
|
-
|
3734
3772
|
let reachableTypesCache;
|
3735
3773
|
function getReachableTypes(schema) {
|
3736
3774
|
// We don't want cache reachableTypes on test environment
|
@@ -3813,7 +3851,7 @@ function parse(code, options) {
|
|
3813
3851
|
return parseForESLint(code, options).ast;
|
3814
3852
|
}
|
3815
3853
|
function parseForESLint(code, options = {}) {
|
3816
|
-
const gqlConfig =
|
3854
|
+
const gqlConfig = loadCachedGraphQLConfig(options);
|
3817
3855
|
const schema = getSchema(options, gqlConfig);
|
3818
3856
|
const parserServices = {
|
3819
3857
|
hasTypeInfo: schema !== null,
|
package/package.json
CHANGED
package/rules/index.d.ts
CHANGED
@@ -48,7 +48,9 @@ export declare const rules: {
|
|
48
48
|
DirectiveDefinition?: boolean;
|
49
49
|
}], false>;
|
50
50
|
'require-field-of-type-query-in-mutation-result': import("..").GraphQLESLintRule<any[], false>;
|
51
|
-
'require-id-when-available': import("..").GraphQLESLintRule<[
|
51
|
+
'require-id-when-available': import("..").GraphQLESLintRule<[{
|
52
|
+
fieldName: string;
|
53
|
+
}], true>;
|
52
54
|
'selection-set-depth': import("..").GraphQLESLintRule<[{
|
53
55
|
maxDepth: number;
|
54
56
|
ignore?: string[];
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import { GraphQLESLintRule } from '../types';
|
2
|
-
|
3
|
-
fieldName: string
|
2
|
+
declare type RequireIdWhenAvailableRuleConfig = {
|
3
|
+
fieldName: string;
|
4
4
|
};
|
5
5
|
declare const rule: GraphQLESLintRule<[RequireIdWhenAvailableRuleConfig], true>;
|
6
6
|
export default rule;
|
package/types.d.ts
CHANGED
package/utils.d.ts
CHANGED
@@ -6,6 +6,7 @@ import { SiblingOperations } from './sibling-operations';
|
|
6
6
|
import { UsedFields, ReachableTypes } from './graphql-ast';
|
7
7
|
export declare function requireSiblingsOperations(ruleName: string, context: GraphQLESLintRuleContext): SiblingOperations | never;
|
8
8
|
export declare function requireGraphQLSchemaFromContext(ruleName: string, context: GraphQLESLintRuleContext): GraphQLSchema | never;
|
9
|
+
export declare function getGraphQLSchemaToExtend(context: GraphQLESLintRuleContext): GraphQLSchema | null;
|
9
10
|
export declare function requireReachableTypesFromContext(ruleName: string, context: GraphQLESLintRuleContext): ReachableTypes | never;
|
10
11
|
export declare function requireUsedFieldsFromContext(ruleName: string, context: GraphQLESLintRuleContext): UsedFields | never;
|
11
12
|
export declare function extractTokens(source: Source): AST.Token[];
|