@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.js
CHANGED
|
@@ -8,6 +8,7 @@ const tslib = require('tslib');
|
|
|
8
8
|
const graphql = require('graphql');
|
|
9
9
|
const inspect = _interopDefault(require('object-inspect'));
|
|
10
10
|
const dependencyGraph = require('dependency-graph');
|
|
11
|
+
const parser = require('graphql/language/parser');
|
|
11
12
|
|
|
12
13
|
function keyMap(list, keyFn) {
|
|
13
14
|
return list.reduce((map, item) => {
|
|
@@ -49,7 +50,7 @@ function isVoid(a) {
|
|
|
49
50
|
return typeof a === 'undefined' || a === null;
|
|
50
51
|
}
|
|
51
52
|
function diffArrays(a, b) {
|
|
52
|
-
return a.filter(
|
|
53
|
+
return a.filter(c => !b.some(d => isEqual(d, c)));
|
|
53
54
|
}
|
|
54
55
|
function compareLists(oldList, newList, callbacks) {
|
|
55
56
|
const oldMap = keyMap(oldList, ({ name }) => name);
|
|
@@ -100,7 +101,7 @@ function isDeprecated(fieldOrEnumValue) {
|
|
|
100
101
|
if (fieldOrEnumValue.deprecationReason != null) {
|
|
101
102
|
return true;
|
|
102
103
|
}
|
|
103
|
-
if ((_b = (_a = fieldOrEnumValue.astNode) === null || _a === void 0 ? void 0 : _a.directives) === null || _b === void 0 ? void 0 : _b.some(
|
|
104
|
+
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')) {
|
|
104
105
|
return true;
|
|
105
106
|
}
|
|
106
107
|
return false;
|
|
@@ -115,8 +116,7 @@ function safeChangeForField(oldType, newType) {
|
|
|
115
116
|
return safeChangeForField(ofType, newType.ofType);
|
|
116
117
|
}
|
|
117
118
|
if (graphql.isListType(oldType)) {
|
|
118
|
-
return ((graphql.isListType(newType) &&
|
|
119
|
-
safeChangeForField(oldType.ofType, newType.ofType)) ||
|
|
119
|
+
return ((graphql.isListType(newType) && safeChangeForField(oldType.ofType, newType.ofType)) ||
|
|
120
120
|
(graphql.isNonNullType(newType) && safeChangeForField(oldType, newType.ofType)));
|
|
121
121
|
}
|
|
122
122
|
return false;
|
|
@@ -151,7 +151,7 @@ function getTypePrefix(type) {
|
|
|
151
151
|
return kindsMap[kind.toString()];
|
|
152
152
|
}
|
|
153
153
|
function isPrimitive(type) {
|
|
154
|
-
return
|
|
154
|
+
return ['String', 'Int', 'Float', 'Boolean', 'ID'].indexOf(typeof type === 'string' ? type : type.name) !== -1;
|
|
155
155
|
}
|
|
156
156
|
function isForIntrospection(type) {
|
|
157
157
|
return ([
|
|
@@ -176,7 +176,9 @@ function findDeprecatedUsages(schema, ast) {
|
|
|
176
176
|
if (reason) {
|
|
177
177
|
const fieldDef = typeInfo.getFieldDef();
|
|
178
178
|
if (fieldDef) {
|
|
179
|
-
errors.push(new graphql.GraphQLError(`The argument '${argument === null || argument === void 0 ? void 0 : argument.name}' of '${fieldDef.name}' is deprecated. ${reason}`, [
|
|
179
|
+
errors.push(new graphql.GraphQLError(`The argument '${argument === null || argument === void 0 ? void 0 : argument.name}' of '${fieldDef.name}' is deprecated. ${reason}`, [
|
|
180
|
+
node,
|
|
181
|
+
]));
|
|
180
182
|
}
|
|
181
183
|
}
|
|
182
184
|
}
|
|
@@ -206,7 +208,7 @@ function findDeprecatedUsages(schema, ast) {
|
|
|
206
208
|
}
|
|
207
209
|
function removeFieldIfDirectives(node, directiveNames) {
|
|
208
210
|
if (node.directives) {
|
|
209
|
-
if (node.directives.some(
|
|
211
|
+
if (node.directives.some(d => directiveNames.indexOf(d.name.value) !== -1)) {
|
|
210
212
|
return null;
|
|
211
213
|
}
|
|
212
214
|
}
|
|
@@ -214,7 +216,7 @@ function removeFieldIfDirectives(node, directiveNames) {
|
|
|
214
216
|
}
|
|
215
217
|
function removeDirectives(node, directiveNames) {
|
|
216
218
|
if (node.directives) {
|
|
217
|
-
return Object.assign(Object.assign({}, node), { directives: node.directives.filter(
|
|
219
|
+
return Object.assign(Object.assign({}, node), { directives: node.directives.filter(d => directiveNames.indexOf(d.name.value) === -1) });
|
|
218
220
|
}
|
|
219
221
|
return node;
|
|
220
222
|
}
|
|
@@ -264,11 +266,7 @@ function getReachableTypes(schema) {
|
|
|
264
266
|
}
|
|
265
267
|
}
|
|
266
268
|
};
|
|
267
|
-
for (const type of [
|
|
268
|
-
schema.getQueryType(),
|
|
269
|
-
schema.getMutationType(),
|
|
270
|
-
schema.getSubscriptionType(),
|
|
271
|
-
]) {
|
|
269
|
+
for (const type of [schema.getQueryType(), schema.getMutationType(), schema.getSubscriptionType()]) {
|
|
272
270
|
if (type) {
|
|
273
271
|
collect(type);
|
|
274
272
|
}
|
|
@@ -508,9 +506,7 @@ function directiveLocationRemoved(directive, location) {
|
|
|
508
506
|
function directiveArgumentAdded(directive, arg) {
|
|
509
507
|
return {
|
|
510
508
|
criticality: {
|
|
511
|
-
level: graphql.isNonNullType(arg.type)
|
|
512
|
-
? exports.CriticalityLevel.Breaking
|
|
513
|
-
: exports.CriticalityLevel.NonBreaking,
|
|
509
|
+
level: graphql.isNonNullType(arg.type) ? exports.CriticalityLevel.Breaking : exports.CriticalityLevel.NonBreaking,
|
|
514
510
|
},
|
|
515
511
|
type: exports.ChangeType.DirectiveArgumentAdded,
|
|
516
512
|
message: `Argument '${arg.name}' was added to directive '${directive.name}'`,
|
|
@@ -709,7 +705,7 @@ function compareTwoStrings(str1, str2) {
|
|
|
709
705
|
const pairs2 = wordLetterPairs(str2);
|
|
710
706
|
const union = pairs1.length + pairs2.length;
|
|
711
707
|
let intersection = 0;
|
|
712
|
-
pairs1.forEach(
|
|
708
|
+
pairs1.forEach(pair1 => {
|
|
713
709
|
for (let i = 0, pair2; (pair2 = pairs2[i]); i++) {
|
|
714
710
|
if (pair1 !== pair2)
|
|
715
711
|
continue;
|
|
@@ -723,7 +719,7 @@ function compareTwoStrings(str1, str2) {
|
|
|
723
719
|
function findBestMatch(mainString, targetStrings) {
|
|
724
720
|
if (!areArgsValid(mainString, targetStrings))
|
|
725
721
|
throw new Error('Bad arguments: First argument should be a string, second should be an array of strings');
|
|
726
|
-
const ratings = targetStrings.map(
|
|
722
|
+
const ratings = targetStrings.map(target => ({
|
|
727
723
|
target,
|
|
728
724
|
rating: compareTwoStrings(mainString, target.value),
|
|
729
725
|
}));
|
|
@@ -731,9 +727,7 @@ function findBestMatch(mainString, targetStrings) {
|
|
|
731
727
|
return { ratings, bestMatch };
|
|
732
728
|
}
|
|
733
729
|
function flattenDeep(arr) {
|
|
734
|
-
return Array.isArray(arr)
|
|
735
|
-
? arr.reduce((a, b) => a.concat(flattenDeep(b)), [])
|
|
736
|
-
: [arr];
|
|
730
|
+
return Array.isArray(arr) ? arr.reduce((a, b) => a.concat(flattenDeep(b)), []) : [arr];
|
|
737
731
|
}
|
|
738
732
|
function areArgsValid(mainString, targetStrings) {
|
|
739
733
|
if (typeof mainString !== 'string')
|
|
@@ -742,7 +736,7 @@ function areArgsValid(mainString, targetStrings) {
|
|
|
742
736
|
return false;
|
|
743
737
|
if (!targetStrings.length)
|
|
744
738
|
return false;
|
|
745
|
-
if (targetStrings.find(
|
|
739
|
+
if (targetStrings.find(s => typeof s.value !== 'string'))
|
|
746
740
|
return false;
|
|
747
741
|
return true;
|
|
748
742
|
}
|
|
@@ -757,7 +751,9 @@ function wordLetterPairs(str) {
|
|
|
757
751
|
return flattenDeep(pairs);
|
|
758
752
|
}
|
|
759
753
|
function safeString(obj) {
|
|
760
|
-
return inspect(obj)
|
|
754
|
+
return inspect(obj)
|
|
755
|
+
.replace(/\[Object\: null prototype\] /g, '')
|
|
756
|
+
.replace(/(^')|('$)/g, '');
|
|
761
757
|
}
|
|
762
758
|
|
|
763
759
|
function inputFieldRemoved(input, field) {
|
|
@@ -872,14 +868,12 @@ function changesInInputField(input, oldField, newField, addChange) {
|
|
|
872
868
|
}
|
|
873
869
|
}
|
|
874
870
|
if (isNotEqual(oldField.defaultValue, newField.defaultValue)) {
|
|
875
|
-
if (Array.isArray(oldField.defaultValue) &&
|
|
876
|
-
Array.isArray(newField.defaultValue)) {
|
|
871
|
+
if (Array.isArray(oldField.defaultValue) && Array.isArray(newField.defaultValue)) {
|
|
877
872
|
if (diffArrays(oldField.defaultValue, newField.defaultValue).length > 0) {
|
|
878
873
|
addChange(inputFieldDefaultValueChanged(input, oldField, newField));
|
|
879
874
|
}
|
|
880
875
|
}
|
|
881
|
-
else if (JSON.stringify(oldField.defaultValue) !==
|
|
882
|
-
JSON.stringify(newField.defaultValue)) {
|
|
876
|
+
else if (JSON.stringify(oldField.defaultValue) !== JSON.stringify(newField.defaultValue)) {
|
|
883
877
|
addChange(inputFieldDefaultValueChanged(input, oldField, newField));
|
|
884
878
|
}
|
|
885
879
|
}
|
|
@@ -1103,15 +1097,13 @@ function changesInArgument(type, field, oldArg, newArg, addChange) {
|
|
|
1103
1097
|
addChange(fieldArgumentDescriptionChanged(type, field, oldArg, newArg));
|
|
1104
1098
|
}
|
|
1105
1099
|
if (isNotEqual(oldArg.defaultValue, newArg.defaultValue)) {
|
|
1106
|
-
if (Array.isArray(oldArg.defaultValue) &&
|
|
1107
|
-
Array.isArray(newArg.defaultValue)) {
|
|
1100
|
+
if (Array.isArray(oldArg.defaultValue) && Array.isArray(newArg.defaultValue)) {
|
|
1108
1101
|
const diff = diffArrays(oldArg.defaultValue, newArg.defaultValue);
|
|
1109
1102
|
if (diff.length > 0) {
|
|
1110
1103
|
addChange(fieldArgumentDefaultChanged(type, field, oldArg, newArg));
|
|
1111
1104
|
}
|
|
1112
1105
|
}
|
|
1113
|
-
else if (JSON.stringify(oldArg.defaultValue) !==
|
|
1114
|
-
JSON.stringify(newArg.defaultValue)) {
|
|
1106
|
+
else if (JSON.stringify(oldArg.defaultValue) !== JSON.stringify(newArg.defaultValue)) {
|
|
1115
1107
|
addChange(fieldArgumentDefaultChanged(type, field, oldArg, newArg));
|
|
1116
1108
|
}
|
|
1117
1109
|
}
|
|
@@ -1216,9 +1208,9 @@ function changesInDirective(oldDirective, newDirective, addChange) {
|
|
|
1216
1208
|
removed: diffArrays(oldDirective.locations, newDirective.locations),
|
|
1217
1209
|
};
|
|
1218
1210
|
// locations added
|
|
1219
|
-
locations.added.forEach(
|
|
1211
|
+
locations.added.forEach(location => addChange(directiveLocationAdded(newDirective, location)));
|
|
1220
1212
|
// locations removed
|
|
1221
|
-
locations.removed.forEach(
|
|
1213
|
+
locations.removed.forEach(location => addChange(directiveLocationRemoved(oldDirective, location)));
|
|
1222
1214
|
compareLists(oldDirective.args, newDirective.args, {
|
|
1223
1215
|
onAdded(arg) {
|
|
1224
1216
|
addChange(directiveArgumentAdded(newDirective, arg));
|
|
@@ -1249,7 +1241,7 @@ function diffSchema(oldSchema, newSchema) {
|
|
|
1249
1241
|
changes.push(change);
|
|
1250
1242
|
}
|
|
1251
1243
|
changesInSchema(oldSchema, newSchema, addChange);
|
|
1252
|
-
compareLists(Object.values(oldSchema.getTypeMap()).filter(
|
|
1244
|
+
compareLists(Object.values(oldSchema.getTypeMap()).filter(t => !isPrimitive(t)), Object.values(newSchema.getTypeMap()).filter(t => !isPrimitive(t)), {
|
|
1253
1245
|
onAdded(type) {
|
|
1254
1246
|
addChange(typeAdded(type));
|
|
1255
1247
|
},
|
|
@@ -1334,7 +1326,7 @@ function changesInType(oldType, newType, addChange) {
|
|
|
1334
1326
|
}
|
|
1335
1327
|
|
|
1336
1328
|
const dangerousBreaking = ({ changes }) => {
|
|
1337
|
-
return changes.map(
|
|
1329
|
+
return changes.map(change => {
|
|
1338
1330
|
if (change.criticality.level === exports.CriticalityLevel.Dangerous) {
|
|
1339
1331
|
return Object.assign(Object.assign({}, change), { criticality: Object.assign(Object.assign({}, change.criticality), { level: exports.CriticalityLevel.Breaking }) });
|
|
1340
1332
|
}
|
|
@@ -1346,8 +1338,8 @@ function parsePath(path) {
|
|
|
1346
1338
|
return path.split('.');
|
|
1347
1339
|
}
|
|
1348
1340
|
|
|
1349
|
-
const suppressRemovalOfDeprecatedField = ({ changes, oldSchema
|
|
1350
|
-
return changes.map(
|
|
1341
|
+
const suppressRemovalOfDeprecatedField = ({ changes, oldSchema }) => {
|
|
1342
|
+
return changes.map(change => {
|
|
1351
1343
|
if (change.type === exports.ChangeType.FieldRemoved &&
|
|
1352
1344
|
change.criticality.level === exports.CriticalityLevel.Breaking &&
|
|
1353
1345
|
change.path) {
|
|
@@ -1402,15 +1394,15 @@ const descriptionChangeTypes = [
|
|
|
1402
1394
|
exports.ChangeType.TypeDescriptionChanged,
|
|
1403
1395
|
];
|
|
1404
1396
|
const ignoreDescriptionChanges = ({ changes }) => {
|
|
1405
|
-
return changes.filter(
|
|
1397
|
+
return changes.filter(change => descriptionChangeTypes.indexOf(change.type) === -1);
|
|
1406
1398
|
};
|
|
1407
1399
|
|
|
1408
|
-
const considerUsage = ({ changes, config
|
|
1400
|
+
const considerUsage = ({ changes, config }) => tslib.__awaiter(void 0, void 0, void 0, function* () {
|
|
1409
1401
|
if (!config) {
|
|
1410
1402
|
throw new Error(`considerUsage rule is missing config`);
|
|
1411
1403
|
}
|
|
1412
1404
|
const collectedBreakingField = [];
|
|
1413
|
-
changes.forEach(
|
|
1405
|
+
changes.forEach(change => {
|
|
1414
1406
|
if (change.criticality.level === exports.CriticalityLevel.Breaking && change.path) {
|
|
1415
1407
|
const [typeName, fieldName, argumentName] = parsePath(change.path);
|
|
1416
1408
|
collectedBreakingField.push({
|
|
@@ -1427,11 +1419,11 @@ const considerUsage = ({ changes, config, }) => tslib.__awaiter(void 0, void 0,
|
|
|
1427
1419
|
const suppressedPaths = collectedBreakingField
|
|
1428
1420
|
.filter((_, i) => usageList[i] === true)
|
|
1429
1421
|
.map(({ type, field, argument }) => [type, field, argument].filter(Boolean).join('.'));
|
|
1430
|
-
return changes.map(
|
|
1422
|
+
return changes.map(change => {
|
|
1431
1423
|
// Turns those "safe to break" changes into "dangerous"
|
|
1432
1424
|
if (change.criticality.level === exports.CriticalityLevel.Breaking &&
|
|
1433
1425
|
change.path &&
|
|
1434
|
-
suppressedPaths.some(
|
|
1426
|
+
suppressedPaths.some(p => change.path.startsWith(p))) {
|
|
1435
1427
|
return Object.assign(Object.assign({}, change), { criticality: Object.assign(Object.assign({}, change.criticality), { level: exports.CriticalityLevel.Dangerous }), message: `${change.message} (non-breaking based on usage)` });
|
|
1436
1428
|
}
|
|
1437
1429
|
return change;
|
|
@@ -1440,7 +1432,7 @@ const considerUsage = ({ changes, config, }) => tslib.__awaiter(void 0, void 0,
|
|
|
1440
1432
|
|
|
1441
1433
|
const safeUnreachable = ({ changes, oldSchema }) => {
|
|
1442
1434
|
const reachable = getReachableTypes(oldSchema);
|
|
1443
|
-
return changes.map(
|
|
1435
|
+
return changes.map(change => {
|
|
1444
1436
|
if (change.criticality.level === exports.CriticalityLevel.Breaking && change.path) {
|
|
1445
1437
|
const [typeName] = parsePath(change.path);
|
|
1446
1438
|
if (!reachable.has(typeName)) {
|
|
@@ -1547,7 +1539,7 @@ function calculateDepth({ node, currentDepth, maxDepth, getFragment, }) {
|
|
|
1547
1539
|
return 1 + maxInnerDepth;
|
|
1548
1540
|
}
|
|
1549
1541
|
case graphql.Kind.SELECTION_SET: {
|
|
1550
|
-
return Math.max(...node.selections.map(
|
|
1542
|
+
return Math.max(...node.selections.map(selection => {
|
|
1551
1543
|
return calculateDepth({
|
|
1552
1544
|
node: selection,
|
|
1553
1545
|
currentDepth: currentDepth,
|
|
@@ -1557,7 +1549,7 @@ function calculateDepth({ node, currentDepth, maxDepth, getFragment, }) {
|
|
|
1557
1549
|
}));
|
|
1558
1550
|
}
|
|
1559
1551
|
case graphql.Kind.DOCUMENT: {
|
|
1560
|
-
return Math.max(...node.definitions.map(
|
|
1552
|
+
return Math.max(...node.definitions.map(def => {
|
|
1561
1553
|
return calculateDepth({
|
|
1562
1554
|
node: def,
|
|
1563
1555
|
currentDepth: currentDepth,
|
|
@@ -1569,7 +1561,7 @@ function calculateDepth({ node, currentDepth, maxDepth, getFragment, }) {
|
|
|
1569
1561
|
case graphql.Kind.OPERATION_DEFINITION:
|
|
1570
1562
|
case graphql.Kind.INLINE_FRAGMENT:
|
|
1571
1563
|
case graphql.Kind.FRAGMENT_DEFINITION: {
|
|
1572
|
-
return Math.max(...node.selectionSet.selections.map(
|
|
1564
|
+
return Math.max(...node.selectionSet.selections.map(selection => {
|
|
1573
1565
|
return calculateDepth({
|
|
1574
1566
|
node: selection,
|
|
1575
1567
|
currentDepth,
|
|
@@ -1590,13 +1582,26 @@ function calculateDepth({ node, currentDepth, maxDepth, getFragment, }) {
|
|
|
1590
1582
|
}
|
|
1591
1583
|
}
|
|
1592
1584
|
}
|
|
1585
|
+
function countDepth(node, parentDepth, getFragmentReference) {
|
|
1586
|
+
let depth = parentDepth;
|
|
1587
|
+
if ('selectionSet' in node && node.selectionSet) {
|
|
1588
|
+
for (let child of node.selectionSet.selections) {
|
|
1589
|
+
depth = Math.max(depth, countDepth(child, parentDepth + 1, getFragmentReference));
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
if (node.kind == graphql.Kind.FRAGMENT_SPREAD) {
|
|
1593
|
+
const fragment = getFragmentReference(node.name.value);
|
|
1594
|
+
if (fragment) {
|
|
1595
|
+
depth = Math.max(depth, countDepth(fragment, parentDepth + 1, getFragmentReference));
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
return depth;
|
|
1599
|
+
}
|
|
1593
1600
|
|
|
1594
1601
|
function transformDocumentWithApollo(doc, { keepClientFields }) {
|
|
1595
1602
|
return graphql.visit(doc, {
|
|
1596
1603
|
Field(node) {
|
|
1597
|
-
return keepClientFields
|
|
1598
|
-
? removeDirectives(node, ['client'])
|
|
1599
|
-
: removeFieldIfDirectives(node, ['client']);
|
|
1604
|
+
return keepClientFields ? removeDirectives(node, ['client']) : removeFieldIfDirectives(node, ['client']);
|
|
1600
1605
|
},
|
|
1601
1606
|
});
|
|
1602
1607
|
}
|
|
@@ -1606,6 +1611,116 @@ function transformSchemaWithApollo(schema) {
|
|
|
1606
1611
|
`));
|
|
1607
1612
|
}
|
|
1608
1613
|
|
|
1614
|
+
function validateAliasCount({ source, doc, maxAliasCount, fragmentGraph, }) {
|
|
1615
|
+
const getFragmentByFragmentName = (fragmentName) => fragmentGraph.getNodeData(fragmentName);
|
|
1616
|
+
for (const definition of doc.definitions) {
|
|
1617
|
+
if (definition.kind !== graphql.Kind.OPERATION_DEFINITION) {
|
|
1618
|
+
continue;
|
|
1619
|
+
}
|
|
1620
|
+
const aliasCount = countAliases(definition, getFragmentByFragmentName);
|
|
1621
|
+
if (aliasCount > maxAliasCount) {
|
|
1622
|
+
return new graphql.GraphQLError(`Too many aliases (${aliasCount}). Maximum allowed is ${maxAliasCount}`, [definition], source, definition.loc && definition.loc.start ? [definition.loc.start] : undefined);
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
function countAliases(node, getFragmentByName) {
|
|
1627
|
+
let aliases = 0;
|
|
1628
|
+
if ('alias' in node && node.alias) {
|
|
1629
|
+
++aliases;
|
|
1630
|
+
}
|
|
1631
|
+
if ('selectionSet' in node && node.selectionSet) {
|
|
1632
|
+
for (let child of node.selectionSet.selections) {
|
|
1633
|
+
aliases += countAliases(child, getFragmentByName);
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
else if (node.kind === graphql.Kind.FRAGMENT_SPREAD) {
|
|
1637
|
+
const fragmentNode = getFragmentByName(node.name.value);
|
|
1638
|
+
if (fragmentNode) {
|
|
1639
|
+
aliases += countAliases(fragmentNode, getFragmentByName);
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
return aliases;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
function validateDirectiveCount({ source, doc, maxDirectiveCount, fragmentGraph, }) {
|
|
1646
|
+
const getFragmentByFragmentName = (fragmentName) => fragmentGraph.getNodeData(fragmentName);
|
|
1647
|
+
for (const definition of doc.definitions) {
|
|
1648
|
+
if (definition.kind !== graphql.Kind.OPERATION_DEFINITION) {
|
|
1649
|
+
continue;
|
|
1650
|
+
}
|
|
1651
|
+
const directiveCount = countDirectives(definition, getFragmentByFragmentName);
|
|
1652
|
+
if (directiveCount > maxDirectiveCount) {
|
|
1653
|
+
return new graphql.GraphQLError(`Too many directives (${directiveCount}). Maximum allowed is ${maxDirectiveCount}`, [definition], source, definition.loc && definition.loc.start ? [definition.loc.start] : undefined);
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
function countDirectives(node, getFragmentByName) {
|
|
1658
|
+
let directives = 0;
|
|
1659
|
+
if (node.directives) {
|
|
1660
|
+
directives += node.directives.length;
|
|
1661
|
+
}
|
|
1662
|
+
if ('selectionSet' in node && node.selectionSet) {
|
|
1663
|
+
for (let child of node.selectionSet.selections) {
|
|
1664
|
+
directives += countDirectives(child, getFragmentByName);
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
if (node.kind == graphql.Kind.FRAGMENT_SPREAD) {
|
|
1668
|
+
const fragment = getFragmentByName(node.name.value);
|
|
1669
|
+
if (fragment) {
|
|
1670
|
+
directives += countDirectives(fragment, getFragmentByName);
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
return directives;
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
class ParserWithLexer extends parser.Parser {
|
|
1677
|
+
constructor(source, options) {
|
|
1678
|
+
super(source, options);
|
|
1679
|
+
this.__tokenCount = 0;
|
|
1680
|
+
const lexer = this._lexer;
|
|
1681
|
+
this._lexer = new Proxy(lexer, {
|
|
1682
|
+
get: (target, prop, receiver) => {
|
|
1683
|
+
if (prop === 'advance') {
|
|
1684
|
+
return () => {
|
|
1685
|
+
const token = target.advance();
|
|
1686
|
+
if (token.kind !== graphql.TokenKind.EOF) {
|
|
1687
|
+
this.__tokenCount++;
|
|
1688
|
+
}
|
|
1689
|
+
return token;
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1692
|
+
return Reflect.get(target, prop, receiver);
|
|
1693
|
+
},
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1696
|
+
get tokenCount() {
|
|
1697
|
+
return this.__tokenCount;
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
function calculateTokenCount(args) {
|
|
1701
|
+
const parser = new ParserWithLexer(args.source);
|
|
1702
|
+
const document = parser.parseDocument();
|
|
1703
|
+
let { tokenCount } = parser;
|
|
1704
|
+
graphql.visit(document, {
|
|
1705
|
+
FragmentSpread(node) {
|
|
1706
|
+
const fragmentSource = args.getReferencedFragmentSource(node.name.value);
|
|
1707
|
+
if (fragmentSource) {
|
|
1708
|
+
tokenCount += calculateTokenCount({
|
|
1709
|
+
source: fragmentSource,
|
|
1710
|
+
getReferencedFragmentSource: args.getReferencedFragmentSource,
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
},
|
|
1714
|
+
});
|
|
1715
|
+
return tokenCount;
|
|
1716
|
+
}
|
|
1717
|
+
function validateTokenCount(args) {
|
|
1718
|
+
const tokenCount = calculateTokenCount(args);
|
|
1719
|
+
if (tokenCount > args.maxTokenCount) {
|
|
1720
|
+
return new graphql.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);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1609
1724
|
function validate(schema, sources, options) {
|
|
1610
1725
|
const config = Object.assign({ strictDeprecated: true, strictFragments: true, keepClientFields: false, apollo: false }, options);
|
|
1611
1726
|
const invalidDocuments = [];
|
|
@@ -1615,43 +1730,41 @@ function validate(schema, sources, options) {
|
|
|
1615
1730
|
const fragments = [];
|
|
1616
1731
|
const fragmentNames = [];
|
|
1617
1732
|
const graph = new dependencyGraph.DepGraph({ circular: true });
|
|
1618
|
-
documents.forEach(
|
|
1619
|
-
doc.fragments.forEach(
|
|
1733
|
+
documents.forEach(doc => {
|
|
1734
|
+
doc.fragments.forEach(fragment => {
|
|
1620
1735
|
fragmentNames.push(fragment.node.name.value);
|
|
1621
1736
|
fragments.push(fragment);
|
|
1622
1737
|
graph.addNode(fragment.node.name.value, fragment.node);
|
|
1623
1738
|
});
|
|
1624
1739
|
});
|
|
1625
|
-
fragments.forEach(
|
|
1740
|
+
fragments.forEach(fragment => {
|
|
1626
1741
|
const depends = extractFragments(graphql.print(fragment.node));
|
|
1627
1742
|
if (depends) {
|
|
1628
|
-
depends.forEach(
|
|
1743
|
+
depends.forEach(name => {
|
|
1629
1744
|
graph.addDependency(fragment.node.name.value, name);
|
|
1630
1745
|
});
|
|
1631
1746
|
}
|
|
1632
1747
|
});
|
|
1633
1748
|
documents
|
|
1634
1749
|
// since we include fragments, validate only operations
|
|
1635
|
-
.filter(
|
|
1636
|
-
.forEach(
|
|
1750
|
+
.filter(doc => doc.hasOperations)
|
|
1751
|
+
.forEach(doc => {
|
|
1637
1752
|
const docWithOperations = {
|
|
1638
1753
|
kind: graphql.Kind.DOCUMENT,
|
|
1639
|
-
definitions: doc.operations.map(
|
|
1754
|
+
definitions: doc.operations.map(d => d.node),
|
|
1640
1755
|
};
|
|
1641
1756
|
const extractedFragments = (extractFragments(graphql.print(docWithOperations)) || [])
|
|
1642
1757
|
// resolve all nested fragments
|
|
1643
|
-
.map(
|
|
1758
|
+
.map(fragmentName => resolveFragment(graph.getNodeData(fragmentName), graph))
|
|
1644
1759
|
// flatten arrays
|
|
1645
1760
|
.reduce((list, current) => list.concat(current), [])
|
|
1646
1761
|
// remove duplicates
|
|
1647
|
-
.filter((def, i, all) => all.findIndex(
|
|
1762
|
+
.filter((def, i, all) => all.findIndex(item => item.name.value === def.name.value) === i);
|
|
1648
1763
|
const merged = {
|
|
1649
1764
|
kind: graphql.Kind.DOCUMENT,
|
|
1650
1765
|
definitions: [...docWithOperations.definitions, ...extractedFragments],
|
|
1651
1766
|
};
|
|
1652
|
-
let transformedSchema = config.apollo
|
|
1653
|
-
? transformSchemaWithApollo(schema)
|
|
1654
|
-
: schema;
|
|
1767
|
+
let transformedSchema = config.apollo ? transformSchemaWithApollo(schema) : schema;
|
|
1655
1768
|
const transformedDoc = config.apollo
|
|
1656
1769
|
? transformDocumentWithApollo(merged, {
|
|
1657
1770
|
keepClientFields: config.keepClientFields,
|
|
@@ -1669,12 +1782,41 @@ function validate(schema, sources, options) {
|
|
|
1669
1782
|
errors.push(depthError);
|
|
1670
1783
|
}
|
|
1671
1784
|
}
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1785
|
+
if (config.maxAliasCount) {
|
|
1786
|
+
const aliasError = validateAliasCount({
|
|
1787
|
+
source: doc.source,
|
|
1788
|
+
doc: transformedDoc,
|
|
1789
|
+
maxAliasCount: config.maxAliasCount,
|
|
1790
|
+
fragmentGraph: graph,
|
|
1791
|
+
});
|
|
1792
|
+
if (aliasError) {
|
|
1793
|
+
errors.push(aliasError);
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
if (config.maxDirectiveCount) {
|
|
1797
|
+
const directiveError = validateDirectiveCount({
|
|
1798
|
+
source: doc.source,
|
|
1799
|
+
doc: transformedDoc,
|
|
1800
|
+
maxDirectiveCount: config.maxDirectiveCount,
|
|
1801
|
+
fragmentGraph: graph,
|
|
1802
|
+
});
|
|
1803
|
+
if (directiveError) {
|
|
1804
|
+
errors.push(directiveError);
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
if (config.maxTokenCount) {
|
|
1808
|
+
const tokenCountError = validateTokenCount({
|
|
1809
|
+
source: doc.source,
|
|
1810
|
+
document: transformedDoc,
|
|
1811
|
+
maxTokenCount: config.maxTokenCount,
|
|
1812
|
+
getReferencedFragmentSource: fragmentName => graphql.print(graph.getNodeData(fragmentName)),
|
|
1813
|
+
});
|
|
1814
|
+
if (tokenCountError) {
|
|
1815
|
+
errors.push(tokenCountError);
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
const deprecated = config.strictDeprecated ? findDeprecatedUsages(transformedSchema, transformedDoc) : [];
|
|
1819
|
+
const duplicatedFragments = config.strictFragments ? findDuplicatedFragments(fragmentNames) : [];
|
|
1678
1820
|
if (sumLengths(errors, duplicatedFragments, deprecated) > 0) {
|
|
1679
1821
|
invalidDocuments.push({
|
|
1680
1822
|
source: doc.source,
|
|
@@ -1688,7 +1830,7 @@ function validate(schema, sources, options) {
|
|
|
1688
1830
|
function findDuplicatedFragments(fragmentNames) {
|
|
1689
1831
|
return fragmentNames
|
|
1690
1832
|
.filter((name, i, all) => all.indexOf(name) !== i)
|
|
1691
|
-
.map(
|
|
1833
|
+
.map(name => new graphql.GraphQLError(`Name of '${name}' fragment is not unique`));
|
|
1692
1834
|
}
|
|
1693
1835
|
//
|
|
1694
1836
|
// PostInfo -> AuthorInfo
|
|
@@ -1697,13 +1839,10 @@ function findDuplicatedFragments(fragmentNames) {
|
|
|
1697
1839
|
function resolveFragment(fragment, graph) {
|
|
1698
1840
|
return graph
|
|
1699
1841
|
.dependenciesOf(fragment.name.value)
|
|
1700
|
-
.reduce((list, current) => [
|
|
1701
|
-
...list,
|
|
1702
|
-
...resolveFragment(graph.getNodeData(current), graph),
|
|
1703
|
-
], [fragment]);
|
|
1842
|
+
.reduce((list, current) => [...list, ...resolveFragment(graph.getNodeData(current), graph)], [fragment]);
|
|
1704
1843
|
}
|
|
1705
1844
|
function extractFragments(document) {
|
|
1706
|
-
return (document.match(/[\.]{3}[a-z0-9\_]+\b/gi) || []).map(
|
|
1845
|
+
return (document.match(/[\.]{3}[a-z0-9\_]+\b/gi) || []).map(name => name.replace('...', ''));
|
|
1707
1846
|
}
|
|
1708
1847
|
function sumLengths(...arrays) {
|
|
1709
1848
|
return arrays.reduce((sum, { length }) => sum + length, 0);
|
|
@@ -1712,20 +1851,19 @@ function sumLengths(...arrays) {
|
|
|
1712
1851
|
function similar(schema, typeName, threshold = 0.4) {
|
|
1713
1852
|
const typeMap = schema.getTypeMap();
|
|
1714
1853
|
const targets = Object.keys(schema.getTypeMap())
|
|
1715
|
-
.filter(
|
|
1716
|
-
.map(
|
|
1854
|
+
.filter(name => !isPrimitive(name) && !isForIntrospection(name))
|
|
1855
|
+
.map(name => ({
|
|
1717
1856
|
typeId: name,
|
|
1718
1857
|
value: stripType(typeMap[name]),
|
|
1719
1858
|
}));
|
|
1720
1859
|
const results = {};
|
|
1721
|
-
if (typeof typeName !== 'undefined' &&
|
|
1722
|
-
!targets.some((t) => t.typeId === typeName)) {
|
|
1860
|
+
if (typeof typeName !== 'undefined' && !targets.some(t => t.typeId === typeName)) {
|
|
1723
1861
|
throw new Error(`Type '${typeName}' doesn't exist`);
|
|
1724
1862
|
}
|
|
1725
|
-
(typeName ? [{ typeId: typeName, value: '' }] : targets).forEach(
|
|
1863
|
+
(typeName ? [{ typeId: typeName, value: '' }] : targets).forEach(source => {
|
|
1726
1864
|
const sourceType = schema.getType(source.typeId);
|
|
1727
|
-
const matchWith = targets.filter(
|
|
1728
|
-
|
|
1865
|
+
const matchWith = targets.filter(target => schema.getType(target.typeId).astNode.kind === sourceType.astNode.kind &&
|
|
1866
|
+
target.typeId !== source.typeId);
|
|
1729
1867
|
if (matchWith.length > 0) {
|
|
1730
1868
|
const found = similarTo(sourceType, matchWith, threshold);
|
|
1731
1869
|
if (found) {
|
|
@@ -1736,7 +1874,7 @@ function similar(schema, typeName, threshold = 0.4) {
|
|
|
1736
1874
|
return results;
|
|
1737
1875
|
}
|
|
1738
1876
|
function similarTo(type, targets, threshold) {
|
|
1739
|
-
const types = targets.filter(
|
|
1877
|
+
const types = targets.filter(target => target.typeId !== type.name);
|
|
1740
1878
|
const result = findBestMatch(stripType(type), types);
|
|
1741
1879
|
if (result.bestMatch.rating < threshold) {
|
|
1742
1880
|
return;
|
|
@@ -1744,7 +1882,7 @@ function similarTo(type, targets, threshold) {
|
|
|
1744
1882
|
return {
|
|
1745
1883
|
bestMatch: result.bestMatch,
|
|
1746
1884
|
ratings: result.ratings
|
|
1747
|
-
.filter(
|
|
1885
|
+
.filter(r => r.rating >= threshold && r.target !== result.bestMatch.target)
|
|
1748
1886
|
.sort((a, b) => a.rating - b.rating)
|
|
1749
1887
|
.reverse(),
|
|
1750
1888
|
};
|
|
@@ -1756,7 +1894,7 @@ function stripType(type) {
|
|
|
1756
1894
|
.replace(/\}$/g, '')
|
|
1757
1895
|
.trim()
|
|
1758
1896
|
.split('\n')
|
|
1759
|
-
.map(
|
|
1897
|
+
.map(s => s.trim())
|
|
1760
1898
|
.sort((a, b) => a.localeCompare(b))
|
|
1761
1899
|
.join(' ');
|
|
1762
1900
|
}
|
|
@@ -1768,7 +1906,7 @@ function coverage(schema, sources) {
|
|
|
1768
1906
|
};
|
|
1769
1907
|
const typeMap = schema.getTypeMap();
|
|
1770
1908
|
const typeInfo = new graphql.TypeInfo(schema);
|
|
1771
|
-
const visitor =
|
|
1909
|
+
const visitor = source => ({
|
|
1772
1910
|
Field(node) {
|
|
1773
1911
|
const fieldDef = typeInfo.getFieldDef();
|
|
1774
1912
|
const parent = typeInfo.getParentType();
|
|
@@ -1786,20 +1924,14 @@ function coverage(schema, sources) {
|
|
|
1786
1924
|
typeCoverage.hits++;
|
|
1787
1925
|
fieldCoverage.hits++;
|
|
1788
1926
|
if (node.loc) {
|
|
1789
|
-
fieldCoverage.locations[sourceName] = [
|
|
1790
|
-
node.loc,
|
|
1791
|
-
...(locations || []),
|
|
1792
|
-
];
|
|
1927
|
+
fieldCoverage.locations[sourceName] = [node.loc, ...(locations || [])];
|
|
1793
1928
|
}
|
|
1794
1929
|
if (node.arguments) {
|
|
1795
1930
|
for (const argNode of node.arguments) {
|
|
1796
1931
|
const argCoverage = fieldCoverage.children[argNode.name.value];
|
|
1797
1932
|
argCoverage.hits++;
|
|
1798
1933
|
if (argNode.loc) {
|
|
1799
|
-
argCoverage.locations[sourceName] = [
|
|
1800
|
-
argNode.loc,
|
|
1801
|
-
...(argCoverage.locations[sourceName] || []),
|
|
1802
|
-
];
|
|
1934
|
+
argCoverage.locations[sourceName] = [argNode.loc, ...(argCoverage.locations[sourceName] || [])];
|
|
1803
1935
|
}
|
|
1804
1936
|
}
|
|
1805
1937
|
}
|
|
@@ -1837,17 +1969,39 @@ function coverage(schema, sources) {
|
|
|
1837
1969
|
const documents = coverage.sources.map(readDocument);
|
|
1838
1970
|
documents.forEach((doc, i) => {
|
|
1839
1971
|
const source = coverage.sources[i];
|
|
1840
|
-
doc.operations.forEach(
|
|
1972
|
+
doc.operations.forEach(op => {
|
|
1841
1973
|
graphql.visit(op.node, graphql.visitWithTypeInfo(typeInfo, visitor(source)));
|
|
1842
1974
|
});
|
|
1843
|
-
doc.fragments.forEach(
|
|
1975
|
+
doc.fragments.forEach(fr => {
|
|
1844
1976
|
graphql.visit(fr.node, graphql.visitWithTypeInfo(typeInfo, visitor(source)));
|
|
1845
1977
|
});
|
|
1846
1978
|
});
|
|
1847
1979
|
return coverage;
|
|
1848
1980
|
}
|
|
1849
1981
|
|
|
1982
|
+
function calculateOperationComplexity(node, config, getFragmentByName, depth = 0) {
|
|
1983
|
+
let cost = config.scalarCost;
|
|
1984
|
+
if ('selectionSet' in node && node.selectionSet) {
|
|
1985
|
+
cost = config.objectCost;
|
|
1986
|
+
for (let child of node.selectionSet.selections) {
|
|
1987
|
+
cost += config.depthCostFactor * calculateOperationComplexity(child, config, getFragmentByName, depth + 1);
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
if (node.kind == graphql.Kind.FRAGMENT_SPREAD) {
|
|
1991
|
+
const fragment = getFragmentByName(node.name.value);
|
|
1992
|
+
if (fragment) {
|
|
1993
|
+
cost += config.depthCostFactor * calculateOperationComplexity(fragment, config, getFragmentByName, depth + 1);
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
return cost;
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1850
1999
|
exports.DiffRule = DiffRule;
|
|
2000
|
+
exports.calculateOperationComplexity = calculateOperationComplexity;
|
|
2001
|
+
exports.calculateTokenCount = calculateTokenCount;
|
|
2002
|
+
exports.countAliases = countAliases;
|
|
2003
|
+
exports.countDepth = countDepth;
|
|
2004
|
+
exports.countDirectives = countDirectives;
|
|
1851
2005
|
exports.coverage = coverage;
|
|
1852
2006
|
exports.diff = diff;
|
|
1853
2007
|
exports.getTypePrefix = getTypePrefix;
|