Skip to content

Commit

Permalink
Introduce dedupeFragments config flag for dealing with duplicate fr…
Browse files Browse the repository at this point in the history
…agments (#6018)

Co-authored-by: Dotan Simha <dotansimha@gmail.com>
  • Loading branch information
gilgardosh and dotansimha committed Jun 20, 2021
1 parent ecd205a commit cf1e5ab
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 3 deletions.
6 changes: 6 additions & 0 deletions .changeset/shiny-news-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@graphql-codegen/visitor-plugin-common': patch
'@graphql-codegen/typed-document-node': patch
---

Introduce new feature for removing duplicated fragments
10 changes: 10 additions & 0 deletions packages/plugins/other/visitor-plugin-common/src/base-visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export interface ParsedConfig {
fragmentImports: ImportDeclaration<FragmentImport>[];
immutableTypes: boolean;
useTypeImports: boolean;
dedupeFragments: boolean;
}

export interface RawConfig {
Expand Down Expand Up @@ -186,6 +187,14 @@ export interface RawConfig {
* @ignore
*/
globalNamespace?: boolean;
/**
* @description Removes fragment duplicants for reducing data transfer.
* It is done by removing sub-fragments imports from fragment definition
* Instead - import all of them are imported to the Operation node.
* @type boolean
* @default false
*/
dedupeFragments?: boolean;
}

export class BaseVisitor<TRawConfig extends RawConfig = RawConfig, TPluginConfig extends ParsedConfig = ParsedConfig> {
Expand All @@ -203,6 +212,7 @@ export class BaseVisitor<TRawConfig extends RawConfig = RawConfig, TPluginConfig
addTypename: !rawConfig.skipTypename,
nonOptionalTypename: !!rawConfig.nonOptionalTypename,
useTypeImports: !!rawConfig.useTypeImports,
dedupeFragments: !!rawConfig.dedupeFragments,
...((additionalConfig || {}) as any),
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,9 @@ export class ClientSideBaseVisitor<
}

protected _transformFragments(document: FragmentDefinitionNode | OperationDefinitionNode): string[] {
const includeNestedFragments = this.config.documentMode === DocumentMode.documentNode;
const includeNestedFragments =
this.config.documentMode === DocumentMode.documentNode ||
(this.config.dedupeFragments && document.kind === 'OperationDefinition');

return this._extractFragments(document, includeNestedFragments).map(document =>
this.getFragmentVariableName(document)
Expand Down Expand Up @@ -312,7 +314,7 @@ export class ClientSideBaseVisitor<
gqlObj = optimizeDocumentNode(gqlObj);
}

if (fragments.length > 0) {
if (fragments.length > 0 && (!this.config.dedupeFragments || node.kind === 'OperationDefinition')) {
const definitions = [
...gqlObj.definitions.map(t => JSON.stringify(t)),
...fragments.map(name => `...${name}.definitions`),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,170 @@
import { Types } from '@graphql-codegen/plugin-helpers';
import { buildSchema, parse } from 'graphql';
import { plugin } from '../src';

describe('TypeDocumentNode', () => {
describe('TypedDocumentNode', () => {
it('Should not output imports when there are no operations at all', async () => {
const result = (await plugin(null as any, [], {})) as Types.ComplexPluginOutput;
expect(result.content).toBe('');
expect(result.prepend.length).toBe(0);
});

it('Duplicated nested fragments handle (dedupeFragments=true)', async () => {
const schema = buildSchema(/* GraphQL */ `
schema {
query: Query
}
type Query {
jobs: [Job!]!
}
type Job {
id: ID!
recruiterName: String!
title: String!
}
`);

const ast = parse(/* GraphQL */ `
query GetJobs {
jobs {
...DataForPageA
...DataForPageB
...JobSimpleRecruiterData
}
}
fragment DataForPageA on Job {
id
...JobSimpleRecruiterData
}
fragment DataForPageB on Job {
title
...JobSimpleRecruiterData
}
fragment JobSimpleRecruiterData on Job {
recruiterName
}
`);

const res = (await plugin(
schema,
[{ location: '', document: ast }],
{ dedupeFragments: true },
{ outputFile: '' }
)) as Types.ComplexPluginOutput;

expect((res.content.match(/JobSimpleRecruiterDataFragmentDoc.definitions/g) || []).length).toBe(1);
});

it('Check with nested and recursive fragments handle (dedupeFragments=true)', async () => {
const schema = buildSchema(/* GraphQL */ `
type Query {
test: MyType
nested: MyOtherType
}
type MyOtherType {
myType: MyType!
myOtherTypeRecursive: MyOtherType!
}
type MyType {
foo: String!
}
`);

const ast = parse(/* GraphQL */ `
query test {
test {
...MyTypeFields
nested {
myOtherTypeRecursive {
myType {
...MyTypeFields
}
myOtherTypeRecursive {
...MyOtherTypeRecursiveFields
}
}
myType {
...MyTypeFields
}
}
}
}
fragment MyOtherTypeRecursiveFields on MyOtherType {
myType {
...MyTypeFields
}
}
fragment MyTypeFields on MyType {
foo
}
`);

const res = (await plugin(
schema,
[{ location: '', document: ast }],
{ dedupeFragments: true },
{ outputFile: '' }
)) as Types.ComplexPluginOutput;

expect((res.content.match(/MyTypeFieldsFragmentDoc.definitions/g) || []).length).toBe(1);
});

it('Ignore duplicated nested fragments handle (dedupeFragments=false)', async () => {
const schema = buildSchema(/* GraphQL */ `
schema {
query: Query
}
type Query {
jobs: [Job!]!
}
type Job {
id: ID!
recruiterName: String!
title: String!
}
`);

const ast = parse(/* GraphQL */ `
query GetJobs {
jobs {
...DataForPageA
...DataForPageB
}
}
fragment DataForPageA on Job {
id
...JobSimpleRecruiterData
}
fragment DataForPageB on Job {
title
...JobSimpleRecruiterData
}
fragment JobSimpleRecruiterData on Job {
recruiterName
}
`);

const res = (await plugin(
schema,
[{ location: '', document: ast }],
{ dedupeFragments: false },
{ outputFile: '' }
)) as Types.ComplexPluginOutput;

expect((res.content.match(/JobSimpleRecruiterDataFragmentDoc.definitions/g) || []).length).toBe(2);
});
});

0 comments on commit cf1e5ab

Please sign in to comment.