@graphql-inspector/core 0.0.0-canary.02a53c5 → 0.0.0-canary.0f79587
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 +6 -6
- package/diff/index.d.ts +2 -1
- package/diff/rules/config.d.ts +2 -0
- package/diff/rules/consider-usage.d.ts +29 -0
- package/diff/rules/index.d.ts +2 -0
- package/diff/rules/safe-unreachable.d.ts +2 -0
- package/diff/rules/types.d.ts +3 -2
- package/index.d.ts +1 -1
- package/{index.cjs.js → index.js} +273 -101
- package/{index.esm.js → index.mjs} +272 -102
- package/package.json +14 -4
- package/utils/compare.d.ts +1 -2
- package/utils/graphql.d.ts +1 -0
- package/utils/isDeprecated.d.ts +2 -0
- package/index.cjs.js.map +0 -1
- package/index.esm.js.map +0 -1
package/README.md
CHANGED
|
@@ -36,17 +36,17 @@ import {
|
|
|
36
36
|
Change,
|
|
37
37
|
InvalidDocument,
|
|
38
38
|
SimilarMap,
|
|
39
|
-
SchemaCoverage
|
|
40
|
-
} from '@graphql-inspector/core'
|
|
39
|
+
SchemaCoverage
|
|
40
|
+
} from '@graphql-inspector/core'
|
|
41
41
|
|
|
42
42
|
// diff
|
|
43
|
-
const changes: Change[] = diff(schemaA, schemaB)
|
|
43
|
+
const changes: Change[] = diff(schemaA, schemaB)
|
|
44
44
|
// validate
|
|
45
|
-
const invalid: InvalidDocument[] = validate(documentsGlob, schema)
|
|
45
|
+
const invalid: InvalidDocument[] = validate(documentsGlob, schema)
|
|
46
46
|
// similar
|
|
47
|
-
const similar: SimilarMap = similar(schema, typename, threshold)
|
|
47
|
+
const similar: SimilarMap = similar(schema, typename, threshold)
|
|
48
48
|
// coverage
|
|
49
|
-
const schemaCoverage: SchemaCoverage = coverage(schema, documents)
|
|
49
|
+
const schemaCoverage: SchemaCoverage = coverage(schema, documents)
|
|
50
50
|
// ...
|
|
51
51
|
```
|
|
52
52
|
|
package/diff/index.d.ts
CHANGED
|
@@ -5,4 +5,5 @@ import * as rules from './rules';
|
|
|
5
5
|
export * from './rules/types';
|
|
6
6
|
export declare const DiffRule: typeof rules;
|
|
7
7
|
export * from './onComplete/types';
|
|
8
|
-
export
|
|
8
|
+
export type { UsageHandler } from './rules/consider-usage';
|
|
9
|
+
export declare function diff(oldSchema: GraphQLSchema, newSchema: GraphQLSchema, rules?: Rule[], config?: rules.ConsiderUsageConfig): Promise<Change[]>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Rule } from './types';
|
|
2
|
+
export declare type UsageHandler = (input: Array<{
|
|
3
|
+
type: string;
|
|
4
|
+
field?: string;
|
|
5
|
+
argument?: string;
|
|
6
|
+
}>) => Promise<boolean[]>;
|
|
7
|
+
export interface ConsiderUsageConfig {
|
|
8
|
+
/**
|
|
9
|
+
* Checks if it's safe to introduce a breaking change on a field
|
|
10
|
+
*
|
|
11
|
+
* Because the function is async and resolves to a boolean value
|
|
12
|
+
* you can add pretty much anything here, many different conditions or
|
|
13
|
+
* even any source of data.
|
|
14
|
+
*
|
|
15
|
+
* In the CLI we use a GraphQL endpoint with a query
|
|
16
|
+
* that checks the usage and returns stats like:
|
|
17
|
+
* min/max count and min/max precentage
|
|
18
|
+
* So we know when to allow for a breaking change.
|
|
19
|
+
*
|
|
20
|
+
* Because it returns a boolean,
|
|
21
|
+
* we can't attach any data or even customize a message of an api change.
|
|
22
|
+
* This is the first iteration, we're going to improve it soon.
|
|
23
|
+
*
|
|
24
|
+
* true - NON_BREAKING
|
|
25
|
+
* false - BREAKING
|
|
26
|
+
*/
|
|
27
|
+
checkUsage?: UsageHandler;
|
|
28
|
+
}
|
|
29
|
+
export declare const considerUsage: Rule<ConsiderUsageConfig>;
|
package/diff/rules/index.d.ts
CHANGED
package/diff/rules/types.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { GraphQLSchema } from 'graphql';
|
|
2
2
|
import { Change } from '../changes/change';
|
|
3
|
-
export declare type Rule = (input: {
|
|
3
|
+
export declare type Rule<TConfig = any> = (input: {
|
|
4
4
|
changes: Change[];
|
|
5
5
|
oldSchema: GraphQLSchema;
|
|
6
6
|
newSchema: GraphQLSchema;
|
|
7
|
-
|
|
7
|
+
config: TConfig;
|
|
8
|
+
}) => Change[] | Promise<Change[]>;
|
package/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { diff, DiffRule, Rule, CompletionArgs, CompletionHandler } from './diff';
|
|
1
|
+
export { diff, DiffRule, Rule, CompletionArgs, CompletionHandler, UsageHandler, } from './diff';
|
|
2
2
|
export { validate, InvalidDocument } from './validate';
|
|
3
3
|
export { similar, SimilarMap } from './similar';
|
|
4
4
|
export * from './coverage';
|
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
|
+
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
|
|
6
|
+
|
|
7
|
+
const tslib = require('tslib');
|
|
5
8
|
const graphql = require('graphql');
|
|
9
|
+
const inspect = _interopDefault(require('object-inspect'));
|
|
6
10
|
const dependencyGraph = require('dependency-graph');
|
|
7
|
-
require('object-inspect');
|
|
8
11
|
|
|
9
12
|
function keyMap(list, keyFn) {
|
|
10
13
|
return list.reduce((map, item) => {
|
|
@@ -17,7 +20,21 @@ function isEqual(a, b) {
|
|
|
17
20
|
if (a.length !== b.length)
|
|
18
21
|
return false;
|
|
19
22
|
for (var index = 0; index < a.length; index++) {
|
|
20
|
-
if (a[index]
|
|
23
|
+
if (!isEqual(a[index], b[index])) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
if (a && b && typeof a === 'object' && typeof b === 'object') {
|
|
30
|
+
const aRecord = a;
|
|
31
|
+
const bRecord = b;
|
|
32
|
+
const aKeys = Object.keys(aRecord);
|
|
33
|
+
const bKeys = Object.keys(bRecord);
|
|
34
|
+
if (aKeys.length !== bKeys.length)
|
|
35
|
+
return false;
|
|
36
|
+
for (const key of aKeys) {
|
|
37
|
+
if (!isEqual(aRecord[key], bRecord[key])) {
|
|
21
38
|
return false;
|
|
22
39
|
}
|
|
23
40
|
}
|
|
@@ -32,7 +49,7 @@ function isVoid(a) {
|
|
|
32
49
|
return typeof a === 'undefined' || a === null;
|
|
33
50
|
}
|
|
34
51
|
function diffArrays(a, b) {
|
|
35
|
-
return a.filter((c) => !b.some((d) => d
|
|
52
|
+
return a.filter((c) => !b.some((d) => isEqual(d, c)));
|
|
36
53
|
}
|
|
37
54
|
function compareLists(oldList, newList, callbacks) {
|
|
38
55
|
const oldMap = keyMap(oldList, ({ name }) => name);
|
|
@@ -75,6 +92,20 @@ function compareLists(oldList, newList, callbacks) {
|
|
|
75
92
|
};
|
|
76
93
|
}
|
|
77
94
|
|
|
95
|
+
function isDeprecated(fieldOrEnumValue) {
|
|
96
|
+
var _a, _b;
|
|
97
|
+
if ('isDeprecated' in fieldOrEnumValue) {
|
|
98
|
+
return fieldOrEnumValue['isDeprecated'];
|
|
99
|
+
}
|
|
100
|
+
if (fieldOrEnumValue.deprecationReason != null) {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
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
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
78
109
|
function safeChangeForField(oldType, newType) {
|
|
79
110
|
if (!graphql.isWrappingType(oldType) && !graphql.isWrappingType(newType)) {
|
|
80
111
|
return oldType.toString() === newType.toString();
|
|
@@ -98,7 +129,7 @@ function safeChangeForInputValue(oldType, newType) {
|
|
|
98
129
|
return safeChangeForInputValue(oldType.ofType, newType.ofType);
|
|
99
130
|
}
|
|
100
131
|
if (graphql.isNonNullType(oldType)) {
|
|
101
|
-
const ofType = graphql.isNonNullType(newType) ? newType : newType;
|
|
132
|
+
const ofType = graphql.isNonNullType(newType) ? newType.ofType : newType;
|
|
102
133
|
return safeChangeForInputValue(oldType.ofType, ofType);
|
|
103
134
|
}
|
|
104
135
|
return false;
|
|
@@ -117,7 +148,7 @@ function getTypePrefix(type) {
|
|
|
117
148
|
[graphql.Kind.ENUM_TYPE_DEFINITION]: 'enum',
|
|
118
149
|
[graphql.Kind.INPUT_OBJECT_TYPE_DEFINITION]: 'input',
|
|
119
150
|
};
|
|
120
|
-
return kindsMap[kind];
|
|
151
|
+
return kindsMap[kind.toString()];
|
|
121
152
|
}
|
|
122
153
|
function isPrimitive(type) {
|
|
123
154
|
return (['String', 'Int', 'Float', 'Boolean', 'ID'].indexOf(typeof type === 'string' ? type : type.name) !== -1);
|
|
@@ -152,7 +183,7 @@ function findDeprecatedUsages(schema, ast) {
|
|
|
152
183
|
},
|
|
153
184
|
Field(node) {
|
|
154
185
|
const fieldDef = typeInfo.getFieldDef();
|
|
155
|
-
if (fieldDef && fieldDef
|
|
186
|
+
if (fieldDef && isDeprecated(fieldDef)) {
|
|
156
187
|
const parentType = typeInfo.getParentType();
|
|
157
188
|
if (parentType) {
|
|
158
189
|
const reason = fieldDef.deprecationReason;
|
|
@@ -162,7 +193,7 @@ function findDeprecatedUsages(schema, ast) {
|
|
|
162
193
|
},
|
|
163
194
|
EnumValue(node) {
|
|
164
195
|
const enumVal = typeInfo.getEnumValue();
|
|
165
|
-
if (enumVal && enumVal
|
|
196
|
+
if (enumVal && isDeprecated(enumVal)) {
|
|
166
197
|
const type = graphql.getNamedType(typeInfo.getInputType());
|
|
167
198
|
if (type) {
|
|
168
199
|
const reason = enumVal.deprecationReason;
|
|
@@ -187,6 +218,75 @@ function removeDirectives(node, directiveNames) {
|
|
|
187
218
|
}
|
|
188
219
|
return node;
|
|
189
220
|
}
|
|
221
|
+
function getReachableTypes(schema) {
|
|
222
|
+
const reachableTypes = new Set();
|
|
223
|
+
const collect = (type) => {
|
|
224
|
+
const typeName = type.name;
|
|
225
|
+
if (reachableTypes.has(typeName)) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
reachableTypes.add(typeName);
|
|
229
|
+
if (graphql.isScalarType(type)) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
else if (graphql.isInterfaceType(type) || graphql.isObjectType(type)) {
|
|
233
|
+
if (graphql.isInterfaceType(type)) {
|
|
234
|
+
const { objects, interfaces } = schema.getImplementations(type);
|
|
235
|
+
for (const child of objects) {
|
|
236
|
+
collect(child);
|
|
237
|
+
}
|
|
238
|
+
for (const child of interfaces) {
|
|
239
|
+
collect(child);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const fields = type.getFields();
|
|
243
|
+
for (const fieldName in fields) {
|
|
244
|
+
const field = fields[fieldName];
|
|
245
|
+
collect(resolveOutputType(field.type));
|
|
246
|
+
const args = field.args;
|
|
247
|
+
for (const argName in args) {
|
|
248
|
+
const arg = args[argName];
|
|
249
|
+
collect(resolveInputType(arg.type));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
else if (graphql.isUnionType(type)) {
|
|
254
|
+
const types = type.getTypes();
|
|
255
|
+
for (const child of types) {
|
|
256
|
+
collect(child);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
else if (graphql.isInputObjectType(type)) {
|
|
260
|
+
const fields = type.getFields();
|
|
261
|
+
for (const fieldName in fields) {
|
|
262
|
+
const field = fields[fieldName];
|
|
263
|
+
collect(resolveInputType(field.type));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
for (const type of [
|
|
268
|
+
schema.getQueryType(),
|
|
269
|
+
schema.getMutationType(),
|
|
270
|
+
schema.getSubscriptionType(),
|
|
271
|
+
]) {
|
|
272
|
+
if (type) {
|
|
273
|
+
collect(type);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return reachableTypes;
|
|
277
|
+
}
|
|
278
|
+
function resolveOutputType(output) {
|
|
279
|
+
if (graphql.isListType(output) || graphql.isNonNullType(output)) {
|
|
280
|
+
return resolveOutputType(output.ofType);
|
|
281
|
+
}
|
|
282
|
+
return output;
|
|
283
|
+
}
|
|
284
|
+
function resolveInputType(input) {
|
|
285
|
+
if (graphql.isListType(input) || graphql.isNonNullType(input)) {
|
|
286
|
+
return resolveInputType(input.ofType);
|
|
287
|
+
}
|
|
288
|
+
return input;
|
|
289
|
+
}
|
|
190
290
|
|
|
191
291
|
(function (ChangeType) {
|
|
192
292
|
// Argument
|
|
@@ -473,7 +573,7 @@ function enumValueRemoved(oldEnum, value) {
|
|
|
473
573
|
reason: `Removing an enum value will cause existing queries that use this enum value to error.`,
|
|
474
574
|
},
|
|
475
575
|
type: exports.ChangeType.EnumValueRemoved,
|
|
476
|
-
message: `Enum value '${value.name}' ${value
|
|
576
|
+
message: `Enum value '${value.name}' ${isDeprecated(value) ? '(deprecated) ' : ''}was removed from enum '${oldEnum.name}'`,
|
|
477
577
|
path: [oldEnum.name, value.name].join('.'),
|
|
478
578
|
};
|
|
479
579
|
}
|
|
@@ -556,7 +656,7 @@ function changesInEnum(oldEnum, newEnum, addChange) {
|
|
|
556
656
|
addChange(enumValueDeprecationReasonChanged(newEnum, oldValue, newValue));
|
|
557
657
|
}
|
|
558
658
|
}
|
|
559
|
-
}
|
|
659
|
+
},
|
|
560
660
|
});
|
|
561
661
|
}
|
|
562
662
|
|
|
@@ -596,6 +696,70 @@ function changesInUnion(oldUnion, newUnion, addChange) {
|
|
|
596
696
|
});
|
|
597
697
|
}
|
|
598
698
|
|
|
699
|
+
function compareTwoStrings(str1, str2) {
|
|
700
|
+
if (!str1.length && !str2.length)
|
|
701
|
+
return 1;
|
|
702
|
+
if (!str1.length || !str2.length)
|
|
703
|
+
return 0;
|
|
704
|
+
if (str1.toUpperCase() === str2.toUpperCase())
|
|
705
|
+
return 1;
|
|
706
|
+
if (str1.length === 1 && str2.length === 1)
|
|
707
|
+
return 0;
|
|
708
|
+
const pairs1 = wordLetterPairs(str1);
|
|
709
|
+
const pairs2 = wordLetterPairs(str2);
|
|
710
|
+
const union = pairs1.length + pairs2.length;
|
|
711
|
+
let intersection = 0;
|
|
712
|
+
pairs1.forEach((pair1) => {
|
|
713
|
+
for (let i = 0, pair2; (pair2 = pairs2[i]); i++) {
|
|
714
|
+
if (pair1 !== pair2)
|
|
715
|
+
continue;
|
|
716
|
+
intersection++;
|
|
717
|
+
pairs2.splice(i, 1);
|
|
718
|
+
break;
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
return (intersection * 2) / union;
|
|
722
|
+
}
|
|
723
|
+
function findBestMatch(mainString, targetStrings) {
|
|
724
|
+
if (!areArgsValid(mainString, targetStrings))
|
|
725
|
+
throw new Error('Bad arguments: First argument should be a string, second should be an array of strings');
|
|
726
|
+
const ratings = targetStrings.map((target) => ({
|
|
727
|
+
target,
|
|
728
|
+
rating: compareTwoStrings(mainString, target.value),
|
|
729
|
+
}));
|
|
730
|
+
const bestMatch = Array.from(ratings).sort((a, b) => b.rating - a.rating)[0];
|
|
731
|
+
return { ratings, bestMatch };
|
|
732
|
+
}
|
|
733
|
+
function flattenDeep(arr) {
|
|
734
|
+
return Array.isArray(arr)
|
|
735
|
+
? arr.reduce((a, b) => a.concat(flattenDeep(b)), [])
|
|
736
|
+
: [arr];
|
|
737
|
+
}
|
|
738
|
+
function areArgsValid(mainString, targetStrings) {
|
|
739
|
+
if (typeof mainString !== 'string')
|
|
740
|
+
return false;
|
|
741
|
+
if (!Array.isArray(targetStrings))
|
|
742
|
+
return false;
|
|
743
|
+
if (!targetStrings.length)
|
|
744
|
+
return false;
|
|
745
|
+
if (targetStrings.find((s) => typeof s.value !== 'string'))
|
|
746
|
+
return false;
|
|
747
|
+
return true;
|
|
748
|
+
}
|
|
749
|
+
function letterPairs(str) {
|
|
750
|
+
const pairs = [];
|
|
751
|
+
for (let i = 0, max = str.length - 1; i < max; i++)
|
|
752
|
+
pairs[i] = str.substring(i, i + 2);
|
|
753
|
+
return pairs;
|
|
754
|
+
}
|
|
755
|
+
function wordLetterPairs(str) {
|
|
756
|
+
const pairs = str.toUpperCase().split(' ').map(letterPairs);
|
|
757
|
+
return flattenDeep(pairs);
|
|
758
|
+
}
|
|
759
|
+
function safeString(obj) {
|
|
760
|
+
return inspect(obj).replace(/\[Object\: null prototype\] /g, '').replace(/(^')|('$)/g, '');
|
|
761
|
+
}
|
|
762
|
+
|
|
599
763
|
function inputFieldRemoved(input, field) {
|
|
600
764
|
return {
|
|
601
765
|
criticality: {
|
|
@@ -603,7 +767,7 @@ function inputFieldRemoved(input, field) {
|
|
|
603
767
|
reason: 'Removing an input field will cause existing queries that use this input field to error.',
|
|
604
768
|
},
|
|
605
769
|
type: exports.ChangeType.InputFieldRemoved,
|
|
606
|
-
message: `Input field '${field.name}' was removed from input object type '${input.name}'`,
|
|
770
|
+
message: `Input field '${field.name}' ${isDeprecated(field) ? '(deprecated) ' : ''}was removed from input object type '${input.name}'`,
|
|
607
771
|
path: [input.name, field.name].join('.'),
|
|
608
772
|
};
|
|
609
773
|
}
|
|
@@ -656,10 +820,10 @@ function inputFieldDefaultValueChanged(input, oldField, newField) {
|
|
|
656
820
|
return {
|
|
657
821
|
criticality: {
|
|
658
822
|
level: exports.CriticalityLevel.Dangerous,
|
|
659
|
-
reason: 'Changing the default value for an argument may change the runtime
|
|
823
|
+
reason: 'Changing the default value for an argument may change the runtime behavior of a field if it was never provided.',
|
|
660
824
|
},
|
|
661
825
|
type: exports.ChangeType.InputFieldDefaultValueChanged,
|
|
662
|
-
message: `Input field '${input.name}.${oldField.name}' default value changed from '${oldField.defaultValue}' to '${newField.defaultValue}'`,
|
|
826
|
+
message: `Input field '${input.name}.${oldField.name}' default value changed from '${safeString(oldField.defaultValue)}' to '${safeString(newField.defaultValue)}'`,
|
|
663
827
|
path: [input.name, oldField.name].join('.'),
|
|
664
828
|
};
|
|
665
829
|
}
|
|
@@ -757,7 +921,7 @@ function fieldRemoved(type, field) {
|
|
|
757
921
|
: `Removing a field is a breaking change. It is preferable to deprecate the field before removing it.`,
|
|
758
922
|
},
|
|
759
923
|
type: exports.ChangeType.FieldRemoved,
|
|
760
|
-
message: `Field '${field.name}' ${field
|
|
924
|
+
message: `Field '${field.name}' ${isDeprecated(field) ? '(deprecated) ' : ''}was removed from ${entity} '${type.name}'`,
|
|
761
925
|
path: [type.name, field.name].join('.'),
|
|
762
926
|
};
|
|
763
927
|
}
|
|
@@ -865,8 +1029,10 @@ function fieldTypeChanged(type, oldField, newField) {
|
|
|
865
1029
|
};
|
|
866
1030
|
}
|
|
867
1031
|
function fieldArgumentAdded(type, field, arg) {
|
|
1032
|
+
const isBreaking = graphql.isNonNullType(arg.type) && typeof arg.defaultValue === 'undefined';
|
|
1033
|
+
const defaultValueMsg = typeof arg.defaultValue !== 'undefined' ? ' (with default value) ' : ' ';
|
|
868
1034
|
return {
|
|
869
|
-
criticality:
|
|
1035
|
+
criticality: isBreaking
|
|
870
1036
|
? {
|
|
871
1037
|
level: exports.CriticalityLevel.Breaking,
|
|
872
1038
|
reason: `Adding a required argument to an existing field is a breaking change because it will cause existing uses of this field to error.`,
|
|
@@ -876,7 +1042,7 @@ function fieldArgumentAdded(type, field, arg) {
|
|
|
876
1042
|
reason: `Adding a new argument to an existing field may involve a change in resolve function logic that potentially may cause some side effects.`,
|
|
877
1043
|
},
|
|
878
1044
|
type: exports.ChangeType.FieldArgumentAdded,
|
|
879
|
-
message: `Argument '${arg.name}: ${arg.type}'
|
|
1045
|
+
message: `Argument '${arg.name}: ${arg.type}'${defaultValueMsg}added to field '${type.name}.${field.name}'`,
|
|
880
1046
|
path: [type.name, field.name, arg.name].join('.'),
|
|
881
1047
|
};
|
|
882
1048
|
}
|
|
@@ -910,8 +1076,8 @@ function fieldArgumentDefaultChanged(type, field, oldArg, newArg) {
|
|
|
910
1076
|
},
|
|
911
1077
|
type: exports.ChangeType.FieldArgumentDefaultChanged,
|
|
912
1078
|
message: typeof oldArg.defaultValue === 'undefined'
|
|
913
|
-
? `Default value '${newArg.defaultValue}' was added to argument '${newArg.name}' on field '${type.name}.${field.name}'`
|
|
914
|
-
: `Default value for argument '${newArg.name}' on field '${type.name}.${field.name}' changed from '${oldArg.defaultValue}' to '${newArg.defaultValue}'`,
|
|
1079
|
+
? `Default value '${safeString(newArg.defaultValue)}' was added to argument '${newArg.name}' on field '${type.name}.${field.name}'`
|
|
1080
|
+
: `Default value for argument '${newArg.name}' on field '${type.name}.${field.name}' changed from '${safeString(oldArg.defaultValue)}' to '${safeString(newArg.defaultValue)}'`,
|
|
915
1081
|
path: [type.name, field.name, oldArg.name].join('.'),
|
|
916
1082
|
};
|
|
917
1083
|
}
|
|
@@ -966,8 +1132,8 @@ function changesInField(type, oldField, newField, addChange) {
|
|
|
966
1132
|
addChange(fieldDescriptionChanged(type, oldField, newField));
|
|
967
1133
|
}
|
|
968
1134
|
}
|
|
969
|
-
if (isNotEqual(oldField
|
|
970
|
-
if (newField
|
|
1135
|
+
if (isNotEqual(isDeprecated(oldField), isDeprecated(newField))) {
|
|
1136
|
+
if (isDeprecated(newField)) {
|
|
971
1137
|
addChange(fieldDeprecationAdded(type, newField));
|
|
972
1138
|
}
|
|
973
1139
|
else {
|
|
@@ -997,7 +1163,7 @@ function changesInField(type, oldField, newField, addChange) {
|
|
|
997
1163
|
},
|
|
998
1164
|
onMutual(arg) {
|
|
999
1165
|
changesInArgument(type, oldField, arg.oldVersion, arg.newVersion, addChange);
|
|
1000
|
-
}
|
|
1166
|
+
},
|
|
1001
1167
|
});
|
|
1002
1168
|
}
|
|
1003
1169
|
|
|
@@ -1108,17 +1274,21 @@ function diffSchema(oldSchema, newSchema) {
|
|
|
1108
1274
|
return changes;
|
|
1109
1275
|
}
|
|
1110
1276
|
function changesInSchema(oldSchema, newSchema, addChange) {
|
|
1277
|
+
var _a, _b, _c, _d, _e, _f;
|
|
1278
|
+
const defaultNames = {
|
|
1279
|
+
query: 'Query',
|
|
1280
|
+
mutation: 'Mutation',
|
|
1281
|
+
subscription: 'Subscription',
|
|
1282
|
+
};
|
|
1111
1283
|
const oldRoot = {
|
|
1112
|
-
query: (oldSchema.getQueryType() || {}).name,
|
|
1113
|
-
mutation: (oldSchema.getMutationType() || {}).name,
|
|
1114
|
-
subscription: (oldSchema.getSubscriptionType() || {})
|
|
1115
|
-
.name,
|
|
1284
|
+
query: (_a = (oldSchema.getQueryType() || {}).name) !== null && _a !== void 0 ? _a : defaultNames.query,
|
|
1285
|
+
mutation: (_b = (oldSchema.getMutationType() || {}).name) !== null && _b !== void 0 ? _b : defaultNames.mutation,
|
|
1286
|
+
subscription: (_c = (oldSchema.getSubscriptionType() || {}).name) !== null && _c !== void 0 ? _c : defaultNames.subscription,
|
|
1116
1287
|
};
|
|
1117
1288
|
const newRoot = {
|
|
1118
|
-
query: (newSchema.getQueryType() || {}).name,
|
|
1119
|
-
mutation: (newSchema.getMutationType() || {}).name,
|
|
1120
|
-
subscription: (newSchema.getSubscriptionType() || {})
|
|
1121
|
-
.name,
|
|
1289
|
+
query: (_d = (newSchema.getQueryType() || {}).name) !== null && _d !== void 0 ? _d : defaultNames.query,
|
|
1290
|
+
mutation: (_e = (newSchema.getMutationType() || {}).name) !== null && _e !== void 0 ? _e : defaultNames.mutation,
|
|
1291
|
+
subscription: (_f = (newSchema.getSubscriptionType() || {}).name) !== null && _f !== void 0 ? _f : defaultNames.subscription,
|
|
1122
1292
|
};
|
|
1123
1293
|
if (isNotEqual(oldRoot.query, newRoot.query)) {
|
|
1124
1294
|
addChange(schemaQueryTypeChanged(oldSchema, newSchema));
|
|
@@ -1185,7 +1355,7 @@ const suppressRemovalOfDeprecatedField = ({ changes, oldSchema, }) => {
|
|
|
1185
1355
|
const type = oldSchema.getType(typeName);
|
|
1186
1356
|
if (graphql.isObjectType(type) || graphql.isInterfaceType(type)) {
|
|
1187
1357
|
const field = type.getFields()[fieldName];
|
|
1188
|
-
if (field
|
|
1358
|
+
if (isDeprecated(field)) {
|
|
1189
1359
|
return Object.assign(Object.assign({}, change), { criticality: Object.assign(Object.assign({}, change.criticality), { level: exports.CriticalityLevel.Dangerous }) });
|
|
1190
1360
|
}
|
|
1191
1361
|
}
|
|
@@ -1197,7 +1367,19 @@ const suppressRemovalOfDeprecatedField = ({ changes, oldSchema, }) => {
|
|
|
1197
1367
|
const type = oldSchema.getType(enumName);
|
|
1198
1368
|
if (graphql.isEnumType(type)) {
|
|
1199
1369
|
const item = type.getValue(enumItem);
|
|
1200
|
-
if (item && item
|
|
1370
|
+
if (item && isDeprecated(item)) {
|
|
1371
|
+
return Object.assign(Object.assign({}, change), { criticality: Object.assign(Object.assign({}, change.criticality), { level: exports.CriticalityLevel.Dangerous }) });
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
if (change.type === exports.ChangeType.InputFieldRemoved &&
|
|
1376
|
+
change.criticality.level === exports.CriticalityLevel.Breaking &&
|
|
1377
|
+
change.path) {
|
|
1378
|
+
const [inputName, inputItem] = parsePath(change.path);
|
|
1379
|
+
const type = oldSchema.getType(inputName);
|
|
1380
|
+
if (graphql.isInputObjectType(type)) {
|
|
1381
|
+
const item = type.getFields()[inputItem];
|
|
1382
|
+
if (item && isDeprecated(item)) {
|
|
1201
1383
|
return Object.assign(Object.assign({}, change), { criticality: Object.assign(Object.assign({}, change.criticality), { level: exports.CriticalityLevel.Dangerous }) });
|
|
1202
1384
|
}
|
|
1203
1385
|
}
|
|
@@ -1223,21 +1405,73 @@ const ignoreDescriptionChanges = ({ changes }) => {
|
|
|
1223
1405
|
return changes.filter((change) => descriptionChangeTypes.indexOf(change.type) === -1);
|
|
1224
1406
|
};
|
|
1225
1407
|
|
|
1408
|
+
const considerUsage = ({ changes, config, }) => tslib.__awaiter(void 0, void 0, void 0, function* () {
|
|
1409
|
+
if (!config) {
|
|
1410
|
+
throw new Error(`considerUsage rule is missing config`);
|
|
1411
|
+
}
|
|
1412
|
+
const collectedBreakingField = [];
|
|
1413
|
+
changes.forEach((change) => {
|
|
1414
|
+
if (change.criticality.level === exports.CriticalityLevel.Breaking && change.path) {
|
|
1415
|
+
const [typeName, fieldName, argumentName] = parsePath(change.path);
|
|
1416
|
+
collectedBreakingField.push({
|
|
1417
|
+
type: typeName,
|
|
1418
|
+
field: fieldName,
|
|
1419
|
+
argument: argumentName,
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1422
|
+
});
|
|
1423
|
+
// True if safe to break, false otherwise
|
|
1424
|
+
const usageList = yield config.checkUsage(collectedBreakingField);
|
|
1425
|
+
// turns an array of booleans into an array of `Type.Field` strings
|
|
1426
|
+
// includes only those that are safe to break the api
|
|
1427
|
+
const suppressedPaths = collectedBreakingField
|
|
1428
|
+
.filter((_, i) => usageList[i] === true)
|
|
1429
|
+
.map(({ type, field, argument }) => [type, field, argument].filter(Boolean).join('.'));
|
|
1430
|
+
return changes.map((change) => {
|
|
1431
|
+
// Turns those "safe to break" changes into "dangerous"
|
|
1432
|
+
if (change.criticality.level === exports.CriticalityLevel.Breaking &&
|
|
1433
|
+
change.path &&
|
|
1434
|
+
suppressedPaths.some((p) => change.path.startsWith(p))) {
|
|
1435
|
+
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
|
+
}
|
|
1437
|
+
return change;
|
|
1438
|
+
});
|
|
1439
|
+
});
|
|
1440
|
+
|
|
1441
|
+
const safeUnreachable = ({ changes, oldSchema }) => {
|
|
1442
|
+
const reachable = getReachableTypes(oldSchema);
|
|
1443
|
+
return changes.map((change) => {
|
|
1444
|
+
if (change.criticality.level === exports.CriticalityLevel.Breaking && change.path) {
|
|
1445
|
+
const [typeName] = parsePath(change.path);
|
|
1446
|
+
if (!reachable.has(typeName)) {
|
|
1447
|
+
return Object.assign(Object.assign({}, change), { criticality: Object.assign(Object.assign({}, change.criticality), { level: exports.CriticalityLevel.NonBreaking }), message: 'Unreachable from root' });
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
return change;
|
|
1451
|
+
});
|
|
1452
|
+
};
|
|
1453
|
+
|
|
1226
1454
|
const rules = /*#__PURE__*/Object.freeze({
|
|
1227
1455
|
__proto__: null,
|
|
1228
1456
|
dangerousBreaking: dangerousBreaking,
|
|
1229
1457
|
suppressRemovalOfDeprecatedField: suppressRemovalOfDeprecatedField,
|
|
1230
|
-
ignoreDescriptionChanges: ignoreDescriptionChanges
|
|
1458
|
+
ignoreDescriptionChanges: ignoreDescriptionChanges,
|
|
1459
|
+
considerUsage: considerUsage,
|
|
1460
|
+
safeUnreachable: safeUnreachable
|
|
1231
1461
|
});
|
|
1232
1462
|
|
|
1233
1463
|
const DiffRule = rules;
|
|
1234
|
-
function diff(oldSchema, newSchema, rules = []) {
|
|
1464
|
+
function diff(oldSchema, newSchema, rules = [], config) {
|
|
1235
1465
|
const changes = diffSchema(oldSchema, newSchema);
|
|
1236
|
-
return rules.reduce((prev, rule) =>
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1466
|
+
return rules.reduce((prev, rule) => tslib.__awaiter(this, void 0, void 0, function* () {
|
|
1467
|
+
const prevChanges = yield prev;
|
|
1468
|
+
return rule({
|
|
1469
|
+
changes: prevChanges,
|
|
1470
|
+
oldSchema,
|
|
1471
|
+
newSchema,
|
|
1472
|
+
config,
|
|
1473
|
+
});
|
|
1474
|
+
}), Promise.resolve(changes));
|
|
1241
1475
|
}
|
|
1242
1476
|
|
|
1243
1477
|
function readDocument(source) {
|
|
@@ -1401,7 +1635,7 @@ function validate(schema, sources, options) {
|
|
|
1401
1635
|
.filter((doc) => doc.hasOperations)
|
|
1402
1636
|
.forEach((doc) => {
|
|
1403
1637
|
const docWithOperations = {
|
|
1404
|
-
kind:
|
|
1638
|
+
kind: graphql.Kind.DOCUMENT,
|
|
1405
1639
|
definitions: doc.operations.map((d) => d.node),
|
|
1406
1640
|
};
|
|
1407
1641
|
const extractedFragments = (extractFragments(graphql.print(docWithOperations)) || [])
|
|
@@ -1412,7 +1646,7 @@ function validate(schema, sources, options) {
|
|
|
1412
1646
|
// remove duplicates
|
|
1413
1647
|
.filter((def, i, all) => all.findIndex((item) => item.name.value === def.name.value) === i);
|
|
1414
1648
|
const merged = {
|
|
1415
|
-
kind:
|
|
1649
|
+
kind: graphql.Kind.DOCUMENT,
|
|
1416
1650
|
definitions: [...docWithOperations.definitions, ...extractedFragments],
|
|
1417
1651
|
};
|
|
1418
1652
|
let transformedSchema = config.apollo
|
|
@@ -1475,67 +1709,6 @@ function sumLengths(...arrays) {
|
|
|
1475
1709
|
return arrays.reduce((sum, { length }) => sum + length, 0);
|
|
1476
1710
|
}
|
|
1477
1711
|
|
|
1478
|
-
function compareTwoStrings(str1, str2) {
|
|
1479
|
-
if (!str1.length && !str2.length)
|
|
1480
|
-
return 1;
|
|
1481
|
-
if (!str1.length || !str2.length)
|
|
1482
|
-
return 0;
|
|
1483
|
-
if (str1.toUpperCase() === str2.toUpperCase())
|
|
1484
|
-
return 1;
|
|
1485
|
-
if (str1.length === 1 && str2.length === 1)
|
|
1486
|
-
return 0;
|
|
1487
|
-
const pairs1 = wordLetterPairs(str1);
|
|
1488
|
-
const pairs2 = wordLetterPairs(str2);
|
|
1489
|
-
const union = pairs1.length + pairs2.length;
|
|
1490
|
-
let intersection = 0;
|
|
1491
|
-
pairs1.forEach((pair1) => {
|
|
1492
|
-
for (let i = 0, pair2; (pair2 = pairs2[i]); i++) {
|
|
1493
|
-
if (pair1 !== pair2)
|
|
1494
|
-
continue;
|
|
1495
|
-
intersection++;
|
|
1496
|
-
pairs2.splice(i, 1);
|
|
1497
|
-
break;
|
|
1498
|
-
}
|
|
1499
|
-
});
|
|
1500
|
-
return (intersection * 2) / union;
|
|
1501
|
-
}
|
|
1502
|
-
function findBestMatch(mainString, targetStrings) {
|
|
1503
|
-
if (!areArgsValid(mainString, targetStrings))
|
|
1504
|
-
throw new Error('Bad arguments: First argument should be a string, second should be an array of strings');
|
|
1505
|
-
const ratings = targetStrings.map((target) => ({
|
|
1506
|
-
target,
|
|
1507
|
-
rating: compareTwoStrings(mainString, target.value),
|
|
1508
|
-
}));
|
|
1509
|
-
const bestMatch = Array.from(ratings).sort((a, b) => b.rating - a.rating)[0];
|
|
1510
|
-
return { ratings, bestMatch };
|
|
1511
|
-
}
|
|
1512
|
-
function flattenDeep(arr) {
|
|
1513
|
-
return Array.isArray(arr)
|
|
1514
|
-
? arr.reduce((a, b) => a.concat(flattenDeep(b)), [])
|
|
1515
|
-
: [arr];
|
|
1516
|
-
}
|
|
1517
|
-
function areArgsValid(mainString, targetStrings) {
|
|
1518
|
-
if (typeof mainString !== 'string')
|
|
1519
|
-
return false;
|
|
1520
|
-
if (!Array.isArray(targetStrings))
|
|
1521
|
-
return false;
|
|
1522
|
-
if (!targetStrings.length)
|
|
1523
|
-
return false;
|
|
1524
|
-
if (targetStrings.find((s) => typeof s.value !== 'string'))
|
|
1525
|
-
return false;
|
|
1526
|
-
return true;
|
|
1527
|
-
}
|
|
1528
|
-
function letterPairs(str) {
|
|
1529
|
-
const pairs = [];
|
|
1530
|
-
for (let i = 0, max = str.length - 1; i < max; i++)
|
|
1531
|
-
pairs[i] = str.substring(i, i + 2);
|
|
1532
|
-
return pairs;
|
|
1533
|
-
}
|
|
1534
|
-
function wordLetterPairs(str) {
|
|
1535
|
-
const pairs = str.toUpperCase().split(' ').map(letterPairs);
|
|
1536
|
-
return flattenDeep(pairs);
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
1712
|
function similar(schema, typeName, threshold = 0.4) {
|
|
1540
1713
|
const typeMap = schema.getTypeMap();
|
|
1541
1714
|
const targets = Object.keys(schema.getTypeMap())
|
|
@@ -1680,4 +1853,3 @@ exports.diff = diff;
|
|
|
1680
1853
|
exports.getTypePrefix = getTypePrefix;
|
|
1681
1854
|
exports.similar = similar;
|
|
1682
1855
|
exports.validate = validate;
|
|
1683
|
-
//# sourceMappingURL=index.cjs.js.map
|