@graphql-inspector/core 3.3.0 → 3.4.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/README.md +5 -3
- package/index.d.ts +7 -2
- package/index.js +248 -94
- package/index.mjs +245 -96
- package/package.json +1 -1
- package/validate/alias-count.d.ts +10 -0
- package/validate/complexity.d.ts +16 -0
- package/validate/directive-count.d.ts +10 -0
- package/validate/index.d.ts +17 -2
- package/validate/query-depth.d.ts +2 -1
- package/validate/token-count.d.ts +12 -0
package/index.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { __awaiter } from 'tslib';
|
|
2
|
-
import { Kind, TypeInfo, visit, visitWithTypeInfo, GraphQLError, getNamedType, isScalarType, isInterfaceType, isObjectType, isUnionType, isInputObjectType, isListType, isNonNullType, isWrappingType, isEnumType, parse, extendSchema, print, validate as validate$1, printType } from 'graphql';
|
|
2
|
+
import { Kind, TypeInfo, visit, visitWithTypeInfo, GraphQLError, getNamedType, isScalarType, isInterfaceType, isObjectType, isUnionType, isInputObjectType, isListType, isNonNullType, isWrappingType, isEnumType, parse, extendSchema, TokenKind, print, validate as validate$1, printType } from 'graphql';
|
|
3
3
|
import inspect from 'object-inspect';
|
|
4
4
|
import { DepGraph } from 'dependency-graph';
|
|
5
|
+
import { Parser } from 'graphql/language/parser';
|
|
5
6
|
|
|
6
7
|
function keyMap(list, keyFn) {
|
|
7
8
|
return list.reduce((map, item) => {
|
|
@@ -43,7 +44,7 @@ function isVoid(a) {
|
|
|
43
44
|
return typeof a === 'undefined' || a === null;
|
|
44
45
|
}
|
|
45
46
|
function diffArrays(a, b) {
|
|
46
|
-
return a.filter(
|
|
47
|
+
return a.filter(c => !b.some(d => isEqual(d, c)));
|
|
47
48
|
}
|
|
48
49
|
function compareLists(oldList, newList, callbacks) {
|
|
49
50
|
const oldMap = keyMap(oldList, ({ name }) => name);
|
|
@@ -94,7 +95,7 @@ function isDeprecated(fieldOrEnumValue) {
|
|
|
94
95
|
if (fieldOrEnumValue.deprecationReason != null) {
|
|
95
96
|
return true;
|
|
96
97
|
}
|
|
97
|
-
if ((_b = (_a = fieldOrEnumValue.astNode) === null || _a === void 0 ? void 0 : _a.directives) === null || _b === void 0 ? void 0 : _b.some(
|
|
98
|
+
if ((_b = (_a = fieldOrEnumValue.astNode) === null || _a === void 0 ? void 0 : _a.directives) === null || _b === void 0 ? void 0 : _b.some(directive => directive.name.value === 'deprecated')) {
|
|
98
99
|
return true;
|
|
99
100
|
}
|
|
100
101
|
return false;
|
|
@@ -109,8 +110,7 @@ function safeChangeForField(oldType, newType) {
|
|
|
109
110
|
return safeChangeForField(ofType, newType.ofType);
|
|
110
111
|
}
|
|
111
112
|
if (isListType(oldType)) {
|
|
112
|
-
return ((isListType(newType) &&
|
|
113
|
-
safeChangeForField(oldType.ofType, newType.ofType)) ||
|
|
113
|
+
return ((isListType(newType) && safeChangeForField(oldType.ofType, newType.ofType)) ||
|
|
114
114
|
(isNonNullType(newType) && safeChangeForField(oldType, newType.ofType)));
|
|
115
115
|
}
|
|
116
116
|
return false;
|
|
@@ -145,7 +145,7 @@ function getTypePrefix(type) {
|
|
|
145
145
|
return kindsMap[kind.toString()];
|
|
146
146
|
}
|
|
147
147
|
function isPrimitive(type) {
|
|
148
|
-
return
|
|
148
|
+
return ['String', 'Int', 'Float', 'Boolean', 'ID'].indexOf(typeof type === 'string' ? type : type.name) !== -1;
|
|
149
149
|
}
|
|
150
150
|
function isForIntrospection(type) {
|
|
151
151
|
return ([
|
|
@@ -170,7 +170,9 @@ function findDeprecatedUsages(schema, ast) {
|
|
|
170
170
|
if (reason) {
|
|
171
171
|
const fieldDef = typeInfo.getFieldDef();
|
|
172
172
|
if (fieldDef) {
|
|
173
|
-
errors.push(new GraphQLError(`The argument '${argument === null || argument === void 0 ? void 0 : argument.name}' of '${fieldDef.name}' is deprecated. ${reason}`, [
|
|
173
|
+
errors.push(new GraphQLError(`The argument '${argument === null || argument === void 0 ? void 0 : argument.name}' of '${fieldDef.name}' is deprecated. ${reason}`, [
|
|
174
|
+
node,
|
|
175
|
+
]));
|
|
174
176
|
}
|
|
175
177
|
}
|
|
176
178
|
}
|
|
@@ -200,7 +202,7 @@ function findDeprecatedUsages(schema, ast) {
|
|
|
200
202
|
}
|
|
201
203
|
function removeFieldIfDirectives(node, directiveNames) {
|
|
202
204
|
if (node.directives) {
|
|
203
|
-
if (node.directives.some(
|
|
205
|
+
if (node.directives.some(d => directiveNames.indexOf(d.name.value) !== -1)) {
|
|
204
206
|
return null;
|
|
205
207
|
}
|
|
206
208
|
}
|
|
@@ -208,7 +210,7 @@ function removeFieldIfDirectives(node, directiveNames) {
|
|
|
208
210
|
}
|
|
209
211
|
function removeDirectives(node, directiveNames) {
|
|
210
212
|
if (node.directives) {
|
|
211
|
-
return Object.assign(Object.assign({}, node), { directives: node.directives.filter(
|
|
213
|
+
return Object.assign(Object.assign({}, node), { directives: node.directives.filter(d => directiveNames.indexOf(d.name.value) === -1) });
|
|
212
214
|
}
|
|
213
215
|
return node;
|
|
214
216
|
}
|
|
@@ -258,11 +260,7 @@ function getReachableTypes(schema) {
|
|
|
258
260
|
}
|
|
259
261
|
}
|
|
260
262
|
};
|
|
261
|
-
for (const type of [
|
|
262
|
-
schema.getQueryType(),
|
|
263
|
-
schema.getMutationType(),
|
|
264
|
-
schema.getSubscriptionType(),
|
|
265
|
-
]) {
|
|
263
|
+
for (const type of [schema.getQueryType(), schema.getMutationType(), schema.getSubscriptionType()]) {
|
|
266
264
|
if (type) {
|
|
267
265
|
collect(type);
|
|
268
266
|
}
|
|
@@ -504,9 +502,7 @@ function directiveLocationRemoved(directive, location) {
|
|
|
504
502
|
function directiveArgumentAdded(directive, arg) {
|
|
505
503
|
return {
|
|
506
504
|
criticality: {
|
|
507
|
-
level: isNonNullType(arg.type)
|
|
508
|
-
? CriticalityLevel.Breaking
|
|
509
|
-
: CriticalityLevel.NonBreaking,
|
|
505
|
+
level: isNonNullType(arg.type) ? CriticalityLevel.Breaking : CriticalityLevel.NonBreaking,
|
|
510
506
|
},
|
|
511
507
|
type: ChangeType.DirectiveArgumentAdded,
|
|
512
508
|
message: `Argument '${arg.name}' was added to directive '${directive.name}'`,
|
|
@@ -705,7 +701,7 @@ function compareTwoStrings(str1, str2) {
|
|
|
705
701
|
const pairs2 = wordLetterPairs(str2);
|
|
706
702
|
const union = pairs1.length + pairs2.length;
|
|
707
703
|
let intersection = 0;
|
|
708
|
-
pairs1.forEach(
|
|
704
|
+
pairs1.forEach(pair1 => {
|
|
709
705
|
for (let i = 0, pair2; (pair2 = pairs2[i]); i++) {
|
|
710
706
|
if (pair1 !== pair2)
|
|
711
707
|
continue;
|
|
@@ -719,7 +715,7 @@ function compareTwoStrings(str1, str2) {
|
|
|
719
715
|
function findBestMatch(mainString, targetStrings) {
|
|
720
716
|
if (!areArgsValid(mainString, targetStrings))
|
|
721
717
|
throw new Error('Bad arguments: First argument should be a string, second should be an array of strings');
|
|
722
|
-
const ratings = targetStrings.map(
|
|
718
|
+
const ratings = targetStrings.map(target => ({
|
|
723
719
|
target,
|
|
724
720
|
rating: compareTwoStrings(mainString, target.value),
|
|
725
721
|
}));
|
|
@@ -727,9 +723,7 @@ function findBestMatch(mainString, targetStrings) {
|
|
|
727
723
|
return { ratings, bestMatch };
|
|
728
724
|
}
|
|
729
725
|
function flattenDeep(arr) {
|
|
730
|
-
return Array.isArray(arr)
|
|
731
|
-
? arr.reduce((a, b) => a.concat(flattenDeep(b)), [])
|
|
732
|
-
: [arr];
|
|
726
|
+
return Array.isArray(arr) ? arr.reduce((a, b) => a.concat(flattenDeep(b)), []) : [arr];
|
|
733
727
|
}
|
|
734
728
|
function areArgsValid(mainString, targetStrings) {
|
|
735
729
|
if (typeof mainString !== 'string')
|
|
@@ -738,7 +732,7 @@ function areArgsValid(mainString, targetStrings) {
|
|
|
738
732
|
return false;
|
|
739
733
|
if (!targetStrings.length)
|
|
740
734
|
return false;
|
|
741
|
-
if (targetStrings.find(
|
|
735
|
+
if (targetStrings.find(s => typeof s.value !== 'string'))
|
|
742
736
|
return false;
|
|
743
737
|
return true;
|
|
744
738
|
}
|
|
@@ -753,7 +747,9 @@ function wordLetterPairs(str) {
|
|
|
753
747
|
return flattenDeep(pairs);
|
|
754
748
|
}
|
|
755
749
|
function safeString(obj) {
|
|
756
|
-
return inspect(obj)
|
|
750
|
+
return inspect(obj)
|
|
751
|
+
.replace(/\[Object\: null prototype\] /g, '')
|
|
752
|
+
.replace(/(^')|('$)/g, '');
|
|
757
753
|
}
|
|
758
754
|
|
|
759
755
|
function inputFieldRemoved(input, field) {
|
|
@@ -868,14 +864,12 @@ function changesInInputField(input, oldField, newField, addChange) {
|
|
|
868
864
|
}
|
|
869
865
|
}
|
|
870
866
|
if (isNotEqual(oldField.defaultValue, newField.defaultValue)) {
|
|
871
|
-
if (Array.isArray(oldField.defaultValue) &&
|
|
872
|
-
Array.isArray(newField.defaultValue)) {
|
|
867
|
+
if (Array.isArray(oldField.defaultValue) && Array.isArray(newField.defaultValue)) {
|
|
873
868
|
if (diffArrays(oldField.defaultValue, newField.defaultValue).length > 0) {
|
|
874
869
|
addChange(inputFieldDefaultValueChanged(input, oldField, newField));
|
|
875
870
|
}
|
|
876
871
|
}
|
|
877
|
-
else if (JSON.stringify(oldField.defaultValue) !==
|
|
878
|
-
JSON.stringify(newField.defaultValue)) {
|
|
872
|
+
else if (JSON.stringify(oldField.defaultValue) !== JSON.stringify(newField.defaultValue)) {
|
|
879
873
|
addChange(inputFieldDefaultValueChanged(input, oldField, newField));
|
|
880
874
|
}
|
|
881
875
|
}
|
|
@@ -1099,15 +1093,13 @@ function changesInArgument(type, field, oldArg, newArg, addChange) {
|
|
|
1099
1093
|
addChange(fieldArgumentDescriptionChanged(type, field, oldArg, newArg));
|
|
1100
1094
|
}
|
|
1101
1095
|
if (isNotEqual(oldArg.defaultValue, newArg.defaultValue)) {
|
|
1102
|
-
if (Array.isArray(oldArg.defaultValue) &&
|
|
1103
|
-
Array.isArray(newArg.defaultValue)) {
|
|
1096
|
+
if (Array.isArray(oldArg.defaultValue) && Array.isArray(newArg.defaultValue)) {
|
|
1104
1097
|
const diff = diffArrays(oldArg.defaultValue, newArg.defaultValue);
|
|
1105
1098
|
if (diff.length > 0) {
|
|
1106
1099
|
addChange(fieldArgumentDefaultChanged(type, field, oldArg, newArg));
|
|
1107
1100
|
}
|
|
1108
1101
|
}
|
|
1109
|
-
else if (JSON.stringify(oldArg.defaultValue) !==
|
|
1110
|
-
JSON.stringify(newArg.defaultValue)) {
|
|
1102
|
+
else if (JSON.stringify(oldArg.defaultValue) !== JSON.stringify(newArg.defaultValue)) {
|
|
1111
1103
|
addChange(fieldArgumentDefaultChanged(type, field, oldArg, newArg));
|
|
1112
1104
|
}
|
|
1113
1105
|
}
|
|
@@ -1212,9 +1204,9 @@ function changesInDirective(oldDirective, newDirective, addChange) {
|
|
|
1212
1204
|
removed: diffArrays(oldDirective.locations, newDirective.locations),
|
|
1213
1205
|
};
|
|
1214
1206
|
// locations added
|
|
1215
|
-
locations.added.forEach(
|
|
1207
|
+
locations.added.forEach(location => addChange(directiveLocationAdded(newDirective, location)));
|
|
1216
1208
|
// locations removed
|
|
1217
|
-
locations.removed.forEach(
|
|
1209
|
+
locations.removed.forEach(location => addChange(directiveLocationRemoved(oldDirective, location)));
|
|
1218
1210
|
compareLists(oldDirective.args, newDirective.args, {
|
|
1219
1211
|
onAdded(arg) {
|
|
1220
1212
|
addChange(directiveArgumentAdded(newDirective, arg));
|
|
@@ -1245,7 +1237,7 @@ function diffSchema(oldSchema, newSchema) {
|
|
|
1245
1237
|
changes.push(change);
|
|
1246
1238
|
}
|
|
1247
1239
|
changesInSchema(oldSchema, newSchema, addChange);
|
|
1248
|
-
compareLists(Object.values(oldSchema.getTypeMap()).filter(
|
|
1240
|
+
compareLists(Object.values(oldSchema.getTypeMap()).filter(t => !isPrimitive(t)), Object.values(newSchema.getTypeMap()).filter(t => !isPrimitive(t)), {
|
|
1249
1241
|
onAdded(type) {
|
|
1250
1242
|
addChange(typeAdded(type));
|
|
1251
1243
|
},
|
|
@@ -1330,7 +1322,7 @@ function changesInType(oldType, newType, addChange) {
|
|
|
1330
1322
|
}
|
|
1331
1323
|
|
|
1332
1324
|
const dangerousBreaking = ({ changes }) => {
|
|
1333
|
-
return changes.map(
|
|
1325
|
+
return changes.map(change => {
|
|
1334
1326
|
if (change.criticality.level === CriticalityLevel.Dangerous) {
|
|
1335
1327
|
return Object.assign(Object.assign({}, change), { criticality: Object.assign(Object.assign({}, change.criticality), { level: CriticalityLevel.Breaking }) });
|
|
1336
1328
|
}
|
|
@@ -1342,8 +1334,8 @@ function parsePath(path) {
|
|
|
1342
1334
|
return path.split('.');
|
|
1343
1335
|
}
|
|
1344
1336
|
|
|
1345
|
-
const suppressRemovalOfDeprecatedField = ({ changes, oldSchema
|
|
1346
|
-
return changes.map(
|
|
1337
|
+
const suppressRemovalOfDeprecatedField = ({ changes, oldSchema }) => {
|
|
1338
|
+
return changes.map(change => {
|
|
1347
1339
|
if (change.type === ChangeType.FieldRemoved &&
|
|
1348
1340
|
change.criticality.level === CriticalityLevel.Breaking &&
|
|
1349
1341
|
change.path) {
|
|
@@ -1398,15 +1390,15 @@ const descriptionChangeTypes = [
|
|
|
1398
1390
|
ChangeType.TypeDescriptionChanged,
|
|
1399
1391
|
];
|
|
1400
1392
|
const ignoreDescriptionChanges = ({ changes }) => {
|
|
1401
|
-
return changes.filter(
|
|
1393
|
+
return changes.filter(change => descriptionChangeTypes.indexOf(change.type) === -1);
|
|
1402
1394
|
};
|
|
1403
1395
|
|
|
1404
|
-
const considerUsage = ({ changes, config
|
|
1396
|
+
const considerUsage = ({ changes, config }) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1405
1397
|
if (!config) {
|
|
1406
1398
|
throw new Error(`considerUsage rule is missing config`);
|
|
1407
1399
|
}
|
|
1408
1400
|
const collectedBreakingField = [];
|
|
1409
|
-
changes.forEach(
|
|
1401
|
+
changes.forEach(change => {
|
|
1410
1402
|
if (change.criticality.level === CriticalityLevel.Breaking && change.path) {
|
|
1411
1403
|
const [typeName, fieldName, argumentName] = parsePath(change.path);
|
|
1412
1404
|
collectedBreakingField.push({
|
|
@@ -1423,11 +1415,11 @@ const considerUsage = ({ changes, config, }) => __awaiter(void 0, void 0, void 0
|
|
|
1423
1415
|
const suppressedPaths = collectedBreakingField
|
|
1424
1416
|
.filter((_, i) => usageList[i] === true)
|
|
1425
1417
|
.map(({ type, field, argument }) => [type, field, argument].filter(Boolean).join('.'));
|
|
1426
|
-
return changes.map(
|
|
1418
|
+
return changes.map(change => {
|
|
1427
1419
|
// Turns those "safe to break" changes into "dangerous"
|
|
1428
1420
|
if (change.criticality.level === CriticalityLevel.Breaking &&
|
|
1429
1421
|
change.path &&
|
|
1430
|
-
suppressedPaths.some(
|
|
1422
|
+
suppressedPaths.some(p => change.path.startsWith(p))) {
|
|
1431
1423
|
return Object.assign(Object.assign({}, change), { criticality: Object.assign(Object.assign({}, change.criticality), { level: CriticalityLevel.Dangerous }), message: `${change.message} (non-breaking based on usage)` });
|
|
1432
1424
|
}
|
|
1433
1425
|
return change;
|
|
@@ -1436,7 +1428,7 @@ const considerUsage = ({ changes, config, }) => __awaiter(void 0, void 0, void 0
|
|
|
1436
1428
|
|
|
1437
1429
|
const safeUnreachable = ({ changes, oldSchema }) => {
|
|
1438
1430
|
const reachable = getReachableTypes(oldSchema);
|
|
1439
|
-
return changes.map(
|
|
1431
|
+
return changes.map(change => {
|
|
1440
1432
|
if (change.criticality.level === CriticalityLevel.Breaking && change.path) {
|
|
1441
1433
|
const [typeName] = parsePath(change.path);
|
|
1442
1434
|
if (!reachable.has(typeName)) {
|
|
@@ -1543,7 +1535,7 @@ function calculateDepth({ node, currentDepth, maxDepth, getFragment, }) {
|
|
|
1543
1535
|
return 1 + maxInnerDepth;
|
|
1544
1536
|
}
|
|
1545
1537
|
case Kind.SELECTION_SET: {
|
|
1546
|
-
return Math.max(...node.selections.map(
|
|
1538
|
+
return Math.max(...node.selections.map(selection => {
|
|
1547
1539
|
return calculateDepth({
|
|
1548
1540
|
node: selection,
|
|
1549
1541
|
currentDepth: currentDepth,
|
|
@@ -1553,7 +1545,7 @@ function calculateDepth({ node, currentDepth, maxDepth, getFragment, }) {
|
|
|
1553
1545
|
}));
|
|
1554
1546
|
}
|
|
1555
1547
|
case Kind.DOCUMENT: {
|
|
1556
|
-
return Math.max(...node.definitions.map(
|
|
1548
|
+
return Math.max(...node.definitions.map(def => {
|
|
1557
1549
|
return calculateDepth({
|
|
1558
1550
|
node: def,
|
|
1559
1551
|
currentDepth: currentDepth,
|
|
@@ -1565,7 +1557,7 @@ function calculateDepth({ node, currentDepth, maxDepth, getFragment, }) {
|
|
|
1565
1557
|
case Kind.OPERATION_DEFINITION:
|
|
1566
1558
|
case Kind.INLINE_FRAGMENT:
|
|
1567
1559
|
case Kind.FRAGMENT_DEFINITION: {
|
|
1568
|
-
return Math.max(...node.selectionSet.selections.map(
|
|
1560
|
+
return Math.max(...node.selectionSet.selections.map(selection => {
|
|
1569
1561
|
return calculateDepth({
|
|
1570
1562
|
node: selection,
|
|
1571
1563
|
currentDepth,
|
|
@@ -1586,13 +1578,26 @@ function calculateDepth({ node, currentDepth, maxDepth, getFragment, }) {
|
|
|
1586
1578
|
}
|
|
1587
1579
|
}
|
|
1588
1580
|
}
|
|
1581
|
+
function countDepth(node, parentDepth, getFragmentReference) {
|
|
1582
|
+
let depth = parentDepth;
|
|
1583
|
+
if ('selectionSet' in node && node.selectionSet) {
|
|
1584
|
+
for (let child of node.selectionSet.selections) {
|
|
1585
|
+
depth = Math.max(depth, countDepth(child, parentDepth + 1, getFragmentReference));
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
if (node.kind == Kind.FRAGMENT_SPREAD) {
|
|
1589
|
+
const fragment = getFragmentReference(node.name.value);
|
|
1590
|
+
if (fragment) {
|
|
1591
|
+
depth = Math.max(depth, countDepth(fragment, parentDepth + 1, getFragmentReference));
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
return depth;
|
|
1595
|
+
}
|
|
1589
1596
|
|
|
1590
1597
|
function transformDocumentWithApollo(doc, { keepClientFields }) {
|
|
1591
1598
|
return visit(doc, {
|
|
1592
1599
|
Field(node) {
|
|
1593
|
-
return keepClientFields
|
|
1594
|
-
? removeDirectives(node, ['client'])
|
|
1595
|
-
: removeFieldIfDirectives(node, ['client']);
|
|
1600
|
+
return keepClientFields ? removeDirectives(node, ['client']) : removeFieldIfDirectives(node, ['client']);
|
|
1596
1601
|
},
|
|
1597
1602
|
});
|
|
1598
1603
|
}
|
|
@@ -1602,6 +1607,116 @@ function transformSchemaWithApollo(schema) {
|
|
|
1602
1607
|
`));
|
|
1603
1608
|
}
|
|
1604
1609
|
|
|
1610
|
+
function validateAliasCount({ source, doc, maxAliasCount, fragmentGraph, }) {
|
|
1611
|
+
const getFragmentByFragmentName = (fragmentName) => fragmentGraph.getNodeData(fragmentName);
|
|
1612
|
+
for (const definition of doc.definitions) {
|
|
1613
|
+
if (definition.kind !== Kind.OPERATION_DEFINITION) {
|
|
1614
|
+
continue;
|
|
1615
|
+
}
|
|
1616
|
+
const aliasCount = countAliases(definition, getFragmentByFragmentName);
|
|
1617
|
+
if (aliasCount > maxAliasCount) {
|
|
1618
|
+
return new GraphQLError(`Too many aliases (${aliasCount}). Maximum allowed is ${maxAliasCount}`, [definition], source, definition.loc && definition.loc.start ? [definition.loc.start] : undefined);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
function countAliases(node, getFragmentByName) {
|
|
1623
|
+
let aliases = 0;
|
|
1624
|
+
if ('alias' in node && node.alias) {
|
|
1625
|
+
++aliases;
|
|
1626
|
+
}
|
|
1627
|
+
if ('selectionSet' in node && node.selectionSet) {
|
|
1628
|
+
for (let child of node.selectionSet.selections) {
|
|
1629
|
+
aliases += countAliases(child, getFragmentByName);
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
else if (node.kind === Kind.FRAGMENT_SPREAD) {
|
|
1633
|
+
const fragmentNode = getFragmentByName(node.name.value);
|
|
1634
|
+
if (fragmentNode) {
|
|
1635
|
+
aliases += countAliases(fragmentNode, getFragmentByName);
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
return aliases;
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
function validateDirectiveCount({ source, doc, maxDirectiveCount, fragmentGraph, }) {
|
|
1642
|
+
const getFragmentByFragmentName = (fragmentName) => fragmentGraph.getNodeData(fragmentName);
|
|
1643
|
+
for (const definition of doc.definitions) {
|
|
1644
|
+
if (definition.kind !== Kind.OPERATION_DEFINITION) {
|
|
1645
|
+
continue;
|
|
1646
|
+
}
|
|
1647
|
+
const directiveCount = countDirectives(definition, getFragmentByFragmentName);
|
|
1648
|
+
if (directiveCount > maxDirectiveCount) {
|
|
1649
|
+
return new GraphQLError(`Too many directives (${directiveCount}). Maximum allowed is ${maxDirectiveCount}`, [definition], source, definition.loc && definition.loc.start ? [definition.loc.start] : undefined);
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
function countDirectives(node, getFragmentByName) {
|
|
1654
|
+
let directives = 0;
|
|
1655
|
+
if (node.directives) {
|
|
1656
|
+
directives += node.directives.length;
|
|
1657
|
+
}
|
|
1658
|
+
if ('selectionSet' in node && node.selectionSet) {
|
|
1659
|
+
for (let child of node.selectionSet.selections) {
|
|
1660
|
+
directives += countDirectives(child, getFragmentByName);
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
if (node.kind == Kind.FRAGMENT_SPREAD) {
|
|
1664
|
+
const fragment = getFragmentByName(node.name.value);
|
|
1665
|
+
if (fragment) {
|
|
1666
|
+
directives += countDirectives(fragment, getFragmentByName);
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
return directives;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
class ParserWithLexer extends Parser {
|
|
1673
|
+
constructor(source, options) {
|
|
1674
|
+
super(source, options);
|
|
1675
|
+
this.__tokenCount = 0;
|
|
1676
|
+
const lexer = this._lexer;
|
|
1677
|
+
this._lexer = new Proxy(lexer, {
|
|
1678
|
+
get: (target, prop, receiver) => {
|
|
1679
|
+
if (prop === 'advance') {
|
|
1680
|
+
return () => {
|
|
1681
|
+
const token = target.advance();
|
|
1682
|
+
if (token.kind !== TokenKind.EOF) {
|
|
1683
|
+
this.__tokenCount++;
|
|
1684
|
+
}
|
|
1685
|
+
return token;
|
|
1686
|
+
};
|
|
1687
|
+
}
|
|
1688
|
+
return Reflect.get(target, prop, receiver);
|
|
1689
|
+
},
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
get tokenCount() {
|
|
1693
|
+
return this.__tokenCount;
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
function calculateTokenCount(args) {
|
|
1697
|
+
const parser = new ParserWithLexer(args.source);
|
|
1698
|
+
const document = parser.parseDocument();
|
|
1699
|
+
let { tokenCount } = parser;
|
|
1700
|
+
visit(document, {
|
|
1701
|
+
FragmentSpread(node) {
|
|
1702
|
+
const fragmentSource = args.getReferencedFragmentSource(node.name.value);
|
|
1703
|
+
if (fragmentSource) {
|
|
1704
|
+
tokenCount += calculateTokenCount({
|
|
1705
|
+
source: fragmentSource,
|
|
1706
|
+
getReferencedFragmentSource: args.getReferencedFragmentSource,
|
|
1707
|
+
});
|
|
1708
|
+
}
|
|
1709
|
+
},
|
|
1710
|
+
});
|
|
1711
|
+
return tokenCount;
|
|
1712
|
+
}
|
|
1713
|
+
function validateTokenCount(args) {
|
|
1714
|
+
const tokenCount = calculateTokenCount(args);
|
|
1715
|
+
if (tokenCount > args.maxTokenCount) {
|
|
1716
|
+
return new GraphQLError(`Query exceeds maximum token count of ${args.maxTokenCount} (actual: ${tokenCount})`, args.document, args.source, args.document.loc && args.document.loc.start ? [args.document.loc.start] : undefined);
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1605
1720
|
function validate(schema, sources, options) {
|
|
1606
1721
|
const config = Object.assign({ strictDeprecated: true, strictFragments: true, keepClientFields: false, apollo: false }, options);
|
|
1607
1722
|
const invalidDocuments = [];
|
|
@@ -1611,43 +1726,41 @@ function validate(schema, sources, options) {
|
|
|
1611
1726
|
const fragments = [];
|
|
1612
1727
|
const fragmentNames = [];
|
|
1613
1728
|
const graph = new DepGraph({ circular: true });
|
|
1614
|
-
documents.forEach(
|
|
1615
|
-
doc.fragments.forEach(
|
|
1729
|
+
documents.forEach(doc => {
|
|
1730
|
+
doc.fragments.forEach(fragment => {
|
|
1616
1731
|
fragmentNames.push(fragment.node.name.value);
|
|
1617
1732
|
fragments.push(fragment);
|
|
1618
1733
|
graph.addNode(fragment.node.name.value, fragment.node);
|
|
1619
1734
|
});
|
|
1620
1735
|
});
|
|
1621
|
-
fragments.forEach(
|
|
1736
|
+
fragments.forEach(fragment => {
|
|
1622
1737
|
const depends = extractFragments(print(fragment.node));
|
|
1623
1738
|
if (depends) {
|
|
1624
|
-
depends.forEach(
|
|
1739
|
+
depends.forEach(name => {
|
|
1625
1740
|
graph.addDependency(fragment.node.name.value, name);
|
|
1626
1741
|
});
|
|
1627
1742
|
}
|
|
1628
1743
|
});
|
|
1629
1744
|
documents
|
|
1630
1745
|
// since we include fragments, validate only operations
|
|
1631
|
-
.filter(
|
|
1632
|
-
.forEach(
|
|
1746
|
+
.filter(doc => doc.hasOperations)
|
|
1747
|
+
.forEach(doc => {
|
|
1633
1748
|
const docWithOperations = {
|
|
1634
1749
|
kind: Kind.DOCUMENT,
|
|
1635
|
-
definitions: doc.operations.map(
|
|
1750
|
+
definitions: doc.operations.map(d => d.node),
|
|
1636
1751
|
};
|
|
1637
1752
|
const extractedFragments = (extractFragments(print(docWithOperations)) || [])
|
|
1638
1753
|
// resolve all nested fragments
|
|
1639
|
-
.map(
|
|
1754
|
+
.map(fragmentName => resolveFragment(graph.getNodeData(fragmentName), graph))
|
|
1640
1755
|
// flatten arrays
|
|
1641
1756
|
.reduce((list, current) => list.concat(current), [])
|
|
1642
1757
|
// remove duplicates
|
|
1643
|
-
.filter((def, i, all) => all.findIndex(
|
|
1758
|
+
.filter((def, i, all) => all.findIndex(item => item.name.value === def.name.value) === i);
|
|
1644
1759
|
const merged = {
|
|
1645
1760
|
kind: Kind.DOCUMENT,
|
|
1646
1761
|
definitions: [...docWithOperations.definitions, ...extractedFragments],
|
|
1647
1762
|
};
|
|
1648
|
-
let transformedSchema = config.apollo
|
|
1649
|
-
? transformSchemaWithApollo(schema)
|
|
1650
|
-
: schema;
|
|
1763
|
+
let transformedSchema = config.apollo ? transformSchemaWithApollo(schema) : schema;
|
|
1651
1764
|
const transformedDoc = config.apollo
|
|
1652
1765
|
? transformDocumentWithApollo(merged, {
|
|
1653
1766
|
keepClientFields: config.keepClientFields,
|
|
@@ -1665,12 +1778,41 @@ function validate(schema, sources, options) {
|
|
|
1665
1778
|
errors.push(depthError);
|
|
1666
1779
|
}
|
|
1667
1780
|
}
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1781
|
+
if (config.maxAliasCount) {
|
|
1782
|
+
const aliasError = validateAliasCount({
|
|
1783
|
+
source: doc.source,
|
|
1784
|
+
doc: transformedDoc,
|
|
1785
|
+
maxAliasCount: config.maxAliasCount,
|
|
1786
|
+
fragmentGraph: graph,
|
|
1787
|
+
});
|
|
1788
|
+
if (aliasError) {
|
|
1789
|
+
errors.push(aliasError);
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
if (config.maxDirectiveCount) {
|
|
1793
|
+
const directiveError = validateDirectiveCount({
|
|
1794
|
+
source: doc.source,
|
|
1795
|
+
doc: transformedDoc,
|
|
1796
|
+
maxDirectiveCount: config.maxDirectiveCount,
|
|
1797
|
+
fragmentGraph: graph,
|
|
1798
|
+
});
|
|
1799
|
+
if (directiveError) {
|
|
1800
|
+
errors.push(directiveError);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
if (config.maxTokenCount) {
|
|
1804
|
+
const tokenCountError = validateTokenCount({
|
|
1805
|
+
source: doc.source,
|
|
1806
|
+
document: transformedDoc,
|
|
1807
|
+
maxTokenCount: config.maxTokenCount,
|
|
1808
|
+
getReferencedFragmentSource: fragmentName => print(graph.getNodeData(fragmentName)),
|
|
1809
|
+
});
|
|
1810
|
+
if (tokenCountError) {
|
|
1811
|
+
errors.push(tokenCountError);
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
const deprecated = config.strictDeprecated ? findDeprecatedUsages(transformedSchema, transformedDoc) : [];
|
|
1815
|
+
const duplicatedFragments = config.strictFragments ? findDuplicatedFragments(fragmentNames) : [];
|
|
1674
1816
|
if (sumLengths(errors, duplicatedFragments, deprecated) > 0) {
|
|
1675
1817
|
invalidDocuments.push({
|
|
1676
1818
|
source: doc.source,
|
|
@@ -1684,7 +1826,7 @@ function validate(schema, sources, options) {
|
|
|
1684
1826
|
function findDuplicatedFragments(fragmentNames) {
|
|
1685
1827
|
return fragmentNames
|
|
1686
1828
|
.filter((name, i, all) => all.indexOf(name) !== i)
|
|
1687
|
-
.map(
|
|
1829
|
+
.map(name => new GraphQLError(`Name of '${name}' fragment is not unique`));
|
|
1688
1830
|
}
|
|
1689
1831
|
//
|
|
1690
1832
|
// PostInfo -> AuthorInfo
|
|
@@ -1693,13 +1835,10 @@ function findDuplicatedFragments(fragmentNames) {
|
|
|
1693
1835
|
function resolveFragment(fragment, graph) {
|
|
1694
1836
|
return graph
|
|
1695
1837
|
.dependenciesOf(fragment.name.value)
|
|
1696
|
-
.reduce((list, current) => [
|
|
1697
|
-
...list,
|
|
1698
|
-
...resolveFragment(graph.getNodeData(current), graph),
|
|
1699
|
-
], [fragment]);
|
|
1838
|
+
.reduce((list, current) => [...list, ...resolveFragment(graph.getNodeData(current), graph)], [fragment]);
|
|
1700
1839
|
}
|
|
1701
1840
|
function extractFragments(document) {
|
|
1702
|
-
return (document.match(/[\.]{3}[a-z0-9\_]+\b/gi) || []).map(
|
|
1841
|
+
return (document.match(/[\.]{3}[a-z0-9\_]+\b/gi) || []).map(name => name.replace('...', ''));
|
|
1703
1842
|
}
|
|
1704
1843
|
function sumLengths(...arrays) {
|
|
1705
1844
|
return arrays.reduce((sum, { length }) => sum + length, 0);
|
|
@@ -1708,20 +1847,19 @@ function sumLengths(...arrays) {
|
|
|
1708
1847
|
function similar(schema, typeName, threshold = 0.4) {
|
|
1709
1848
|
const typeMap = schema.getTypeMap();
|
|
1710
1849
|
const targets = Object.keys(schema.getTypeMap())
|
|
1711
|
-
.filter(
|
|
1712
|
-
.map(
|
|
1850
|
+
.filter(name => !isPrimitive(name) && !isForIntrospection(name))
|
|
1851
|
+
.map(name => ({
|
|
1713
1852
|
typeId: name,
|
|
1714
1853
|
value: stripType(typeMap[name]),
|
|
1715
1854
|
}));
|
|
1716
1855
|
const results = {};
|
|
1717
|
-
if (typeof typeName !== 'undefined' &&
|
|
1718
|
-
!targets.some((t) => t.typeId === typeName)) {
|
|
1856
|
+
if (typeof typeName !== 'undefined' && !targets.some(t => t.typeId === typeName)) {
|
|
1719
1857
|
throw new Error(`Type '${typeName}' doesn't exist`);
|
|
1720
1858
|
}
|
|
1721
|
-
(typeName ? [{ typeId: typeName, value: '' }] : targets).forEach(
|
|
1859
|
+
(typeName ? [{ typeId: typeName, value: '' }] : targets).forEach(source => {
|
|
1722
1860
|
const sourceType = schema.getType(source.typeId);
|
|
1723
|
-
const matchWith = targets.filter(
|
|
1724
|
-
|
|
1861
|
+
const matchWith = targets.filter(target => schema.getType(target.typeId).astNode.kind === sourceType.astNode.kind &&
|
|
1862
|
+
target.typeId !== source.typeId);
|
|
1725
1863
|
if (matchWith.length > 0) {
|
|
1726
1864
|
const found = similarTo(sourceType, matchWith, threshold);
|
|
1727
1865
|
if (found) {
|
|
@@ -1732,7 +1870,7 @@ function similar(schema, typeName, threshold = 0.4) {
|
|
|
1732
1870
|
return results;
|
|
1733
1871
|
}
|
|
1734
1872
|
function similarTo(type, targets, threshold) {
|
|
1735
|
-
const types = targets.filter(
|
|
1873
|
+
const types = targets.filter(target => target.typeId !== type.name);
|
|
1736
1874
|
const result = findBestMatch(stripType(type), types);
|
|
1737
1875
|
if (result.bestMatch.rating < threshold) {
|
|
1738
1876
|
return;
|
|
@@ -1740,7 +1878,7 @@ function similarTo(type, targets, threshold) {
|
|
|
1740
1878
|
return {
|
|
1741
1879
|
bestMatch: result.bestMatch,
|
|
1742
1880
|
ratings: result.ratings
|
|
1743
|
-
.filter(
|
|
1881
|
+
.filter(r => r.rating >= threshold && r.target !== result.bestMatch.target)
|
|
1744
1882
|
.sort((a, b) => a.rating - b.rating)
|
|
1745
1883
|
.reverse(),
|
|
1746
1884
|
};
|
|
@@ -1752,7 +1890,7 @@ function stripType(type) {
|
|
|
1752
1890
|
.replace(/\}$/g, '')
|
|
1753
1891
|
.trim()
|
|
1754
1892
|
.split('\n')
|
|
1755
|
-
.map(
|
|
1893
|
+
.map(s => s.trim())
|
|
1756
1894
|
.sort((a, b) => a.localeCompare(b))
|
|
1757
1895
|
.join(' ');
|
|
1758
1896
|
}
|
|
@@ -1764,7 +1902,7 @@ function coverage(schema, sources) {
|
|
|
1764
1902
|
};
|
|
1765
1903
|
const typeMap = schema.getTypeMap();
|
|
1766
1904
|
const typeInfo = new TypeInfo(schema);
|
|
1767
|
-
const visitor =
|
|
1905
|
+
const visitor = source => ({
|
|
1768
1906
|
Field(node) {
|
|
1769
1907
|
const fieldDef = typeInfo.getFieldDef();
|
|
1770
1908
|
const parent = typeInfo.getParentType();
|
|
@@ -1782,20 +1920,14 @@ function coverage(schema, sources) {
|
|
|
1782
1920
|
typeCoverage.hits++;
|
|
1783
1921
|
fieldCoverage.hits++;
|
|
1784
1922
|
if (node.loc) {
|
|
1785
|
-
fieldCoverage.locations[sourceName] = [
|
|
1786
|
-
node.loc,
|
|
1787
|
-
...(locations || []),
|
|
1788
|
-
];
|
|
1923
|
+
fieldCoverage.locations[sourceName] = [node.loc, ...(locations || [])];
|
|
1789
1924
|
}
|
|
1790
1925
|
if (node.arguments) {
|
|
1791
1926
|
for (const argNode of node.arguments) {
|
|
1792
1927
|
const argCoverage = fieldCoverage.children[argNode.name.value];
|
|
1793
1928
|
argCoverage.hits++;
|
|
1794
1929
|
if (argNode.loc) {
|
|
1795
|
-
argCoverage.locations[sourceName] = [
|
|
1796
|
-
argNode.loc,
|
|
1797
|
-
...(argCoverage.locations[sourceName] || []),
|
|
1798
|
-
];
|
|
1930
|
+
argCoverage.locations[sourceName] = [argNode.loc, ...(argCoverage.locations[sourceName] || [])];
|
|
1799
1931
|
}
|
|
1800
1932
|
}
|
|
1801
1933
|
}
|
|
@@ -1833,14 +1965,31 @@ function coverage(schema, sources) {
|
|
|
1833
1965
|
const documents = coverage.sources.map(readDocument);
|
|
1834
1966
|
documents.forEach((doc, i) => {
|
|
1835
1967
|
const source = coverage.sources[i];
|
|
1836
|
-
doc.operations.forEach(
|
|
1968
|
+
doc.operations.forEach(op => {
|
|
1837
1969
|
visit(op.node, visitWithTypeInfo(typeInfo, visitor(source)));
|
|
1838
1970
|
});
|
|
1839
|
-
doc.fragments.forEach(
|
|
1971
|
+
doc.fragments.forEach(fr => {
|
|
1840
1972
|
visit(fr.node, visitWithTypeInfo(typeInfo, visitor(source)));
|
|
1841
1973
|
});
|
|
1842
1974
|
});
|
|
1843
1975
|
return coverage;
|
|
1844
1976
|
}
|
|
1845
1977
|
|
|
1846
|
-
|
|
1978
|
+
function calculateOperationComplexity(node, config, getFragmentByName, depth = 0) {
|
|
1979
|
+
let cost = config.scalarCost;
|
|
1980
|
+
if ('selectionSet' in node && node.selectionSet) {
|
|
1981
|
+
cost = config.objectCost;
|
|
1982
|
+
for (let child of node.selectionSet.selections) {
|
|
1983
|
+
cost += config.depthCostFactor * calculateOperationComplexity(child, config, getFragmentByName, depth + 1);
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
if (node.kind == Kind.FRAGMENT_SPREAD) {
|
|
1987
|
+
const fragment = getFragmentByName(node.name.value);
|
|
1988
|
+
if (fragment) {
|
|
1989
|
+
cost += config.depthCostFactor * calculateOperationComplexity(fragment, config, getFragmentByName, depth + 1);
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
return cost;
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
export { ChangeType, CriticalityLevel, DiffRule, calculateOperationComplexity, calculateTokenCount, countAliases, countDepth, countDirectives, coverage, diff, getTypePrefix, similar, validate };
|