Skip to content

Commit 81b20f5

Browse files
tgandrewseddeee888
andauthored
Make the fragment matcher deterministic (#10214)
* Make the fragment matcher deterministic This exposes a new plugin option `deterministic` that when enabled will ensure that the fragment matcher will always return the same result for the same schema, no matter the order it is in the schema. It does this by sorting the results so that they are always in the same order. The default is `false` to maintain backwards compatibility. This fixes #7367 * Add missing changeset --------- Co-authored-by: Eddy Nguyen <[email protected]>
1 parent 62dd4c7 commit 81b20f5

File tree

3 files changed

+96
-6
lines changed

3 files changed

+96
-6
lines changed

Diff for: .changeset/witty-tables-join.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@graphql-codegen/fragment-matcher': minor
3+
---
4+
5+
Add new flag to make the fragment matcher results deterministic

Diff for: packages/plugins/other/fragment-matcher/src/index.ts

+23-6
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,11 @@ export interface FragmentMatcherConfig {
109109
*/
110110
useExplicitTyping?: boolean;
111111
federation?: boolean;
112+
/**
113+
* @description When enabled sorts the fragment types lexicographically. This is useful for deterministic output.
114+
* @default false
115+
*/
116+
deterministic?: boolean;
112117
}
113118

114119
const extensions = {
@@ -128,6 +133,7 @@ export const plugin: PluginFunction = async (
128133
federation: false,
129134
apolloClientVersion: 3,
130135
useExplicitTyping: false,
136+
deterministic: false,
131137
...pluginConfig,
132138
};
133139

@@ -157,23 +163,34 @@ export const plugin: PluginFunction = async (
157163
throw new Error(`Plugin "fragment-matcher" couldn't introspect the schema`);
158164
}
159165

160-
const filterUnionAndInterfaceTypes = type => type.kind === 'UNION' || type.kind === 'INTERFACE';
166+
const sortStringsLexicographically = (a: string, b: string) => {
167+
if (!config.deterministic) {
168+
return 0;
169+
}
170+
return a.localeCompare(b);
171+
};
172+
173+
const unionAndInterfaceTypes = introspection.data.__schema.types
174+
.filter(type => type.kind === 'UNION' || type.kind === 'INTERFACE')
175+
.sort((a, b) => sortStringsLexicographically(a.name, b.name));
176+
161177
const createPossibleTypesCollection = (acc, type) => {
162-
return { ...acc, [type.name]: type.possibleTypes.map(possibleType => possibleType.name) };
178+
return {
179+
...acc,
180+
[type.name]: type.possibleTypes.map(possibleType => possibleType.name).sort(sortStringsLexicographically),
181+
};
163182
};
164183

165184
const filteredData: IntrospectionResultData | PossibleTypesResultData =
166185
apolloClientVersion === 2
167186
? {
168187
__schema: {
169188
...introspection.data.__schema,
170-
types: introspection.data.__schema.types.filter(type => type.kind === 'UNION' || type.kind === 'INTERFACE'),
189+
types: unionAndInterfaceTypes,
171190
},
172191
}
173192
: {
174-
possibleTypes: introspection.data.__schema.types
175-
.filter(filterUnionAndInterfaceTypes)
176-
.reduce(createPossibleTypesCollection, {}),
193+
possibleTypes: unionAndInterfaceTypes.reduce(createPossibleTypesCollection, {}),
177194
};
178195

179196
const content = JSON.stringify(filteredData, null, 2);

Diff for: packages/plugins/other/fragment-matcher/tests/plugin.spec.ts

+68
Original file line numberDiff line numberDiff line change
@@ -444,4 +444,72 @@ describe('Fragment Matcher Plugin', () => {
444444

445445
expect(content).toEqual(introspection);
446446
});
447+
it('should create the result deterministically when configured to', async () => {
448+
const complexSchema = buildASTSchema(gql`
449+
type Droid {
450+
model: String
451+
}
452+
453+
type Character {
454+
name: String
455+
}
456+
457+
type Jedi {
458+
side: String
459+
}
460+
461+
union People = Jedi | Droid | Character
462+
union People2 = Droid | Jedi | Character
463+
464+
type Query {
465+
allPeople: [People]
466+
}
467+
`);
468+
469+
const reorderedComplexSchema = buildASTSchema(gql`
470+
type Droid {
471+
model: String
472+
}
473+
474+
type Character {
475+
name: String
476+
}
477+
478+
type Jedi {
479+
side: String
480+
}
481+
482+
union People2 = Droid | Jedi | Character
483+
union People = Jedi | Droid | Character
484+
485+
type Query {
486+
allPeople: [People]
487+
}
488+
`);
489+
490+
const contentA = await plugin(
491+
complexSchema,
492+
[],
493+
{
494+
apolloClientVersion: 2,
495+
deterministic: true,
496+
},
497+
{
498+
outputFile: 'foo.json',
499+
}
500+
);
501+
const contentB = await plugin(
502+
reorderedComplexSchema,
503+
[],
504+
{
505+
apolloClientVersion: 2,
506+
deterministic: true,
507+
},
508+
{
509+
outputFile: 'foo.json',
510+
}
511+
);
512+
513+
expect(contentA).toEqual(contentB);
514+
});
447515
});

0 commit comments

Comments
 (0)