@graphql-inspector/core 0.0.0-canary.e88122e → 0.0.0-canary.fec2df4
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 +231 -94
- package/index.mjs +228 -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 +12 -2
- package/validate/query-depth.d.ts +2 -1
- package/validate/token-count.d.ts +5 -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,68 @@ 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
|
+
|
|
1609
1676
|
function validate(schema, sources, options) {
|
|
1610
1677
|
const config = Object.assign({ strictDeprecated: true, strictFragments: true, keepClientFields: false, apollo: false }, options);
|
|
1611
1678
|
const invalidDocuments = [];
|
|
@@ -1615,43 +1682,41 @@ function validate(schema, sources, options) {
|
|
|
1615
1682
|
const fragments = [];
|
|
1616
1683
|
const fragmentNames = [];
|
|
1617
1684
|
const graph = new dependencyGraph.DepGraph({ circular: true });
|
|
1618
|
-
documents.forEach(
|
|
1619
|
-
doc.fragments.forEach(
|
|
1685
|
+
documents.forEach(doc => {
|
|
1686
|
+
doc.fragments.forEach(fragment => {
|
|
1620
1687
|
fragmentNames.push(fragment.node.name.value);
|
|
1621
1688
|
fragments.push(fragment);
|
|
1622
1689
|
graph.addNode(fragment.node.name.value, fragment.node);
|
|
1623
1690
|
});
|
|
1624
1691
|
});
|
|
1625
|
-
fragments.forEach(
|
|
1692
|
+
fragments.forEach(fragment => {
|
|
1626
1693
|
const depends = extractFragments(graphql.print(fragment.node));
|
|
1627
1694
|
if (depends) {
|
|
1628
|
-
depends.forEach(
|
|
1695
|
+
depends.forEach(name => {
|
|
1629
1696
|
graph.addDependency(fragment.node.name.value, name);
|
|
1630
1697
|
});
|
|
1631
1698
|
}
|
|
1632
1699
|
});
|
|
1633
1700
|
documents
|
|
1634
1701
|
// since we include fragments, validate only operations
|
|
1635
|
-
.filter(
|
|
1636
|
-
.forEach(
|
|
1702
|
+
.filter(doc => doc.hasOperations)
|
|
1703
|
+
.forEach(doc => {
|
|
1637
1704
|
const docWithOperations = {
|
|
1638
1705
|
kind: graphql.Kind.DOCUMENT,
|
|
1639
|
-
definitions: doc.operations.map(
|
|
1706
|
+
definitions: doc.operations.map(d => d.node),
|
|
1640
1707
|
};
|
|
1641
1708
|
const extractedFragments = (extractFragments(graphql.print(docWithOperations)) || [])
|
|
1642
1709
|
// resolve all nested fragments
|
|
1643
|
-
.map(
|
|
1710
|
+
.map(fragmentName => resolveFragment(graph.getNodeData(fragmentName), graph))
|
|
1644
1711
|
// flatten arrays
|
|
1645
1712
|
.reduce((list, current) => list.concat(current), [])
|
|
1646
1713
|
// remove duplicates
|
|
1647
|
-
.filter((def, i, all) => all.findIndex(
|
|
1714
|
+
.filter((def, i, all) => all.findIndex(item => item.name.value === def.name.value) === i);
|
|
1648
1715
|
const merged = {
|
|
1649
1716
|
kind: graphql.Kind.DOCUMENT,
|
|
1650
1717
|
definitions: [...docWithOperations.definitions, ...extractedFragments],
|
|
1651
1718
|
};
|
|
1652
|
-
let transformedSchema = config.apollo
|
|
1653
|
-
? transformSchemaWithApollo(schema)
|
|
1654
|
-
: schema;
|
|
1719
|
+
let transformedSchema = config.apollo ? transformSchemaWithApollo(schema) : schema;
|
|
1655
1720
|
const transformedDoc = config.apollo
|
|
1656
1721
|
? transformDocumentWithApollo(merged, {
|
|
1657
1722
|
keepClientFields: config.keepClientFields,
|
|
@@ -1669,12 +1734,30 @@ function validate(schema, sources, options) {
|
|
|
1669
1734
|
errors.push(depthError);
|
|
1670
1735
|
}
|
|
1671
1736
|
}
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1737
|
+
if (config.maxAliasCount) {
|
|
1738
|
+
const aliasError = validateAliasCount({
|
|
1739
|
+
source: doc.source,
|
|
1740
|
+
doc: transformedDoc,
|
|
1741
|
+
maxAliasCount: config.maxAliasCount,
|
|
1742
|
+
fragmentGraph: graph,
|
|
1743
|
+
});
|
|
1744
|
+
if (aliasError) {
|
|
1745
|
+
errors.push(aliasError);
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
if (config.maxDirectiveCount) {
|
|
1749
|
+
const directiveError = validateDirectiveCount({
|
|
1750
|
+
source: doc.source,
|
|
1751
|
+
doc: transformedDoc,
|
|
1752
|
+
maxDirectiveCount: config.maxDirectiveCount,
|
|
1753
|
+
fragmentGraph: graph,
|
|
1754
|
+
});
|
|
1755
|
+
if (directiveError) {
|
|
1756
|
+
errors.push(directiveError);
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
const deprecated = config.strictDeprecated ? findDeprecatedUsages(transformedSchema, transformedDoc) : [];
|
|
1760
|
+
const duplicatedFragments = config.strictFragments ? findDuplicatedFragments(fragmentNames) : [];
|
|
1678
1761
|
if (sumLengths(errors, duplicatedFragments, deprecated) > 0) {
|
|
1679
1762
|
invalidDocuments.push({
|
|
1680
1763
|
source: doc.source,
|
|
@@ -1688,7 +1771,7 @@ function validate(schema, sources, options) {
|
|
|
1688
1771
|
function findDuplicatedFragments(fragmentNames) {
|
|
1689
1772
|
return fragmentNames
|
|
1690
1773
|
.filter((name, i, all) => all.indexOf(name) !== i)
|
|
1691
|
-
.map(
|
|
1774
|
+
.map(name => new graphql.GraphQLError(`Name of '${name}' fragment is not unique`));
|
|
1692
1775
|
}
|
|
1693
1776
|
//
|
|
1694
1777
|
// PostInfo -> AuthorInfo
|
|
@@ -1697,13 +1780,10 @@ function findDuplicatedFragments(fragmentNames) {
|
|
|
1697
1780
|
function resolveFragment(fragment, graph) {
|
|
1698
1781
|
return graph
|
|
1699
1782
|
.dependenciesOf(fragment.name.value)
|
|
1700
|
-
.reduce((list, current) => [
|
|
1701
|
-
...list,
|
|
1702
|
-
...resolveFragment(graph.getNodeData(current), graph),
|
|
1703
|
-
], [fragment]);
|
|
1783
|
+
.reduce((list, current) => [...list, ...resolveFragment(graph.getNodeData(current), graph)], [fragment]);
|
|
1704
1784
|
}
|
|
1705
1785
|
function extractFragments(document) {
|
|
1706
|
-
return (document.match(/[\.]{3}[a-z0-9\_]+\b/gi) || []).map(
|
|
1786
|
+
return (document.match(/[\.]{3}[a-z0-9\_]+\b/gi) || []).map(name => name.replace('...', ''));
|
|
1707
1787
|
}
|
|
1708
1788
|
function sumLengths(...arrays) {
|
|
1709
1789
|
return arrays.reduce((sum, { length }) => sum + length, 0);
|
|
@@ -1712,20 +1792,19 @@ function sumLengths(...arrays) {
|
|
|
1712
1792
|
function similar(schema, typeName, threshold = 0.4) {
|
|
1713
1793
|
const typeMap = schema.getTypeMap();
|
|
1714
1794
|
const targets = Object.keys(schema.getTypeMap())
|
|
1715
|
-
.filter(
|
|
1716
|
-
.map(
|
|
1795
|
+
.filter(name => !isPrimitive(name) && !isForIntrospection(name))
|
|
1796
|
+
.map(name => ({
|
|
1717
1797
|
typeId: name,
|
|
1718
1798
|
value: stripType(typeMap[name]),
|
|
1719
1799
|
}));
|
|
1720
1800
|
const results = {};
|
|
1721
|
-
if (typeof typeName !== 'undefined' &&
|
|
1722
|
-
!targets.some((t) => t.typeId === typeName)) {
|
|
1801
|
+
if (typeof typeName !== 'undefined' && !targets.some(t => t.typeId === typeName)) {
|
|
1723
1802
|
throw new Error(`Type '${typeName}' doesn't exist`);
|
|
1724
1803
|
}
|
|
1725
|
-
(typeName ? [{ typeId: typeName, value: '' }] : targets).forEach(
|
|
1804
|
+
(typeName ? [{ typeId: typeName, value: '' }] : targets).forEach(source => {
|
|
1726
1805
|
const sourceType = schema.getType(source.typeId);
|
|
1727
|
-
const matchWith = targets.filter(
|
|
1728
|
-
|
|
1806
|
+
const matchWith = targets.filter(target => schema.getType(target.typeId).astNode.kind === sourceType.astNode.kind &&
|
|
1807
|
+
target.typeId !== source.typeId);
|
|
1729
1808
|
if (matchWith.length > 0) {
|
|
1730
1809
|
const found = similarTo(sourceType, matchWith, threshold);
|
|
1731
1810
|
if (found) {
|
|
@@ -1736,7 +1815,7 @@ function similar(schema, typeName, threshold = 0.4) {
|
|
|
1736
1815
|
return results;
|
|
1737
1816
|
}
|
|
1738
1817
|
function similarTo(type, targets, threshold) {
|
|
1739
|
-
const types = targets.filter(
|
|
1818
|
+
const types = targets.filter(target => target.typeId !== type.name);
|
|
1740
1819
|
const result = findBestMatch(stripType(type), types);
|
|
1741
1820
|
if (result.bestMatch.rating < threshold) {
|
|
1742
1821
|
return;
|
|
@@ -1744,7 +1823,7 @@ function similarTo(type, targets, threshold) {
|
|
|
1744
1823
|
return {
|
|
1745
1824
|
bestMatch: result.bestMatch,
|
|
1746
1825
|
ratings: result.ratings
|
|
1747
|
-
.filter(
|
|
1826
|
+
.filter(r => r.rating >= threshold && r.target !== result.bestMatch.target)
|
|
1748
1827
|
.sort((a, b) => a.rating - b.rating)
|
|
1749
1828
|
.reverse(),
|
|
1750
1829
|
};
|
|
@@ -1756,7 +1835,7 @@ function stripType(type) {
|
|
|
1756
1835
|
.replace(/\}$/g, '')
|
|
1757
1836
|
.trim()
|
|
1758
1837
|
.split('\n')
|
|
1759
|
-
.map(
|
|
1838
|
+
.map(s => s.trim())
|
|
1760
1839
|
.sort((a, b) => a.localeCompare(b))
|
|
1761
1840
|
.join(' ');
|
|
1762
1841
|
}
|
|
@@ -1768,7 +1847,7 @@ function coverage(schema, sources) {
|
|
|
1768
1847
|
};
|
|
1769
1848
|
const typeMap = schema.getTypeMap();
|
|
1770
1849
|
const typeInfo = new graphql.TypeInfo(schema);
|
|
1771
|
-
const visitor =
|
|
1850
|
+
const visitor = source => ({
|
|
1772
1851
|
Field(node) {
|
|
1773
1852
|
const fieldDef = typeInfo.getFieldDef();
|
|
1774
1853
|
const parent = typeInfo.getParentType();
|
|
@@ -1786,20 +1865,14 @@ function coverage(schema, sources) {
|
|
|
1786
1865
|
typeCoverage.hits++;
|
|
1787
1866
|
fieldCoverage.hits++;
|
|
1788
1867
|
if (node.loc) {
|
|
1789
|
-
fieldCoverage.locations[sourceName] = [
|
|
1790
|
-
node.loc,
|
|
1791
|
-
...(locations || []),
|
|
1792
|
-
];
|
|
1868
|
+
fieldCoverage.locations[sourceName] = [node.loc, ...(locations || [])];
|
|
1793
1869
|
}
|
|
1794
1870
|
if (node.arguments) {
|
|
1795
1871
|
for (const argNode of node.arguments) {
|
|
1796
1872
|
const argCoverage = fieldCoverage.children[argNode.name.value];
|
|
1797
1873
|
argCoverage.hits++;
|
|
1798
1874
|
if (argNode.loc) {
|
|
1799
|
-
argCoverage.locations[sourceName] = [
|
|
1800
|
-
argNode.loc,
|
|
1801
|
-
...(argCoverage.locations[sourceName] || []),
|
|
1802
|
-
];
|
|
1875
|
+
argCoverage.locations[sourceName] = [argNode.loc, ...(argCoverage.locations[sourceName] || [])];
|
|
1803
1876
|
}
|
|
1804
1877
|
}
|
|
1805
1878
|
}
|
|
@@ -1837,17 +1910,81 @@ function coverage(schema, sources) {
|
|
|
1837
1910
|
const documents = coverage.sources.map(readDocument);
|
|
1838
1911
|
documents.forEach((doc, i) => {
|
|
1839
1912
|
const source = coverage.sources[i];
|
|
1840
|
-
doc.operations.forEach(
|
|
1913
|
+
doc.operations.forEach(op => {
|
|
1841
1914
|
graphql.visit(op.node, graphql.visitWithTypeInfo(typeInfo, visitor(source)));
|
|
1842
1915
|
});
|
|
1843
|
-
doc.fragments.forEach(
|
|
1916
|
+
doc.fragments.forEach(fr => {
|
|
1844
1917
|
graphql.visit(fr.node, graphql.visitWithTypeInfo(typeInfo, visitor(source)));
|
|
1845
1918
|
});
|
|
1846
1919
|
});
|
|
1847
1920
|
return coverage;
|
|
1848
1921
|
}
|
|
1849
1922
|
|
|
1923
|
+
function calculateOperationComplexity(node, config, getFragmentByName, depth = 0) {
|
|
1924
|
+
let cost = config.scalarCost;
|
|
1925
|
+
if ('selectionSet' in node && node.selectionSet) {
|
|
1926
|
+
cost = config.objectCost;
|
|
1927
|
+
for (let child of node.selectionSet.selections) {
|
|
1928
|
+
cost += config.depthCostFactor * calculateOperationComplexity(child, config, getFragmentByName, depth + 1);
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
if (node.kind == graphql.Kind.FRAGMENT_SPREAD) {
|
|
1932
|
+
const fragment = getFragmentByName(node.name.value);
|
|
1933
|
+
if (fragment) {
|
|
1934
|
+
cost += config.depthCostFactor * calculateOperationComplexity(fragment, config, getFragmentByName, depth + 1);
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
return cost;
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
class ParserWithLexer extends parser.Parser {
|
|
1941
|
+
constructor(source, options) {
|
|
1942
|
+
super(source, options);
|
|
1943
|
+
this.__tokenCount = 0;
|
|
1944
|
+
const lexer = this._lexer;
|
|
1945
|
+
this._lexer = new Proxy(lexer, {
|
|
1946
|
+
get: (target, prop, receiver) => {
|
|
1947
|
+
if (prop === 'advance') {
|
|
1948
|
+
return () => {
|
|
1949
|
+
const token = target.advance();
|
|
1950
|
+
if (token.kind !== graphql.TokenKind.EOF) {
|
|
1951
|
+
this.__tokenCount++;
|
|
1952
|
+
}
|
|
1953
|
+
return token;
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
return Reflect.get(target, prop, receiver);
|
|
1957
|
+
},
|
|
1958
|
+
});
|
|
1959
|
+
}
|
|
1960
|
+
get tokenCount() {
|
|
1961
|
+
return this.__tokenCount;
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
function calculateTokenCount(args) {
|
|
1965
|
+
const parser = new ParserWithLexer(args.source);
|
|
1966
|
+
const document = parser.parseDocument();
|
|
1967
|
+
let { tokenCount } = parser;
|
|
1968
|
+
graphql.visit(document, {
|
|
1969
|
+
FragmentSpread(node) {
|
|
1970
|
+
const fragmentSource = args.getReferencedFragmentSource(node.name.value);
|
|
1971
|
+
if (fragmentSource) {
|
|
1972
|
+
tokenCount += calculateTokenCount({
|
|
1973
|
+
source: fragmentSource,
|
|
1974
|
+
getReferencedFragmentSource: args.getReferencedFragmentSource,
|
|
1975
|
+
});
|
|
1976
|
+
}
|
|
1977
|
+
},
|
|
1978
|
+
});
|
|
1979
|
+
return tokenCount;
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1850
1982
|
exports.DiffRule = DiffRule;
|
|
1983
|
+
exports.calculateOperationComplexity = calculateOperationComplexity;
|
|
1984
|
+
exports.calculateTokenCount = calculateTokenCount;
|
|
1985
|
+
exports.countAliases = countAliases;
|
|
1986
|
+
exports.countDepth = countDepth;
|
|
1987
|
+
exports.countDirectives = countDirectives;
|
|
1851
1988
|
exports.coverage = coverage;
|
|
1852
1989
|
exports.diff = diff;
|
|
1853
1990
|
exports.getTypePrefix = getTypePrefix;
|