Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add mergeAST from GraphiQL, refactor using visit() [WIP] #1948

Closed
wants to merge 1 commit into from

Conversation

acao
Copy link
Member

@acao acao commented Jun 4, 2019

Accomplishes graphql/graphiql#836, originally introduced via graphql/graphiql#762 earlier this year, mergeAST is a handy utility for in-lining fragments for complex queries.

As per @IvanGoncharov's suggestion, I refactored it to use language/visitor's visit function. Good thing I'd been working with AST and visitors on another project using antlr4 earlier this year, or I would've been so lost here, haha. That said, GraphQL's visit() is so powerful, I'm blown away at how simple this was.

Thank you @ramonsaboya for the inspiration, and some solid unit tests, and an excellent idea all around.

I'm wondering if a Set would be a more appropriate data structure here?

This is a WIP, just learning about visit() and this is just a learning excercise for me.
Not a huge priority to any part of the ecosystem currently, and a lot of important edge cases are still broken here.

@acao
Copy link
Member Author

acao commented Jun 4, 2019

I found a failing case that wasn't in the original unit tests:

      query Test1 {
        ...Fragment4
        ...Fragment5
      }
  
      fragment Fragment5 on Test1 {
        ...Fragment4
      }

      fragment Fragment4 on Test1 {
        id
      }

does not give me

 query Test1 {
        ...on Test1 {
          id
        }
        ...on Test1 {
          ...on Test1 {
            id
          }
        }
      }

instead, I get:

 query Test1 {
        ...on Test1 {
          id
        }
        ...on Test1 {
          id
        }
      }

this works in the original! hmmm 🤔

// Collect the existing FragmentDefinition nodes.
fragmentDefinitions[node.name.value] = node;
},
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need visit here, simple for of over definitions would be enough.

it(fixture.desc, () => {
const result = print(mergeAST(parse(fixture.query))).replace(/\s/g, '');
expect(result).to.equal(fixture.mergedQuery.replace(/\s/g, ''));
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't use fixtures please unroll it here.
Use dedent to remove indentation from strings.
And defined expectMerged wrapper to not repeat print(mergeAST(parse

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please write it like this:

function expectInlined(query) {
  return expect(print(inlineNamedFragments(parse(query))));
}

describe('inlineNamedFragments', () => {
  it('does not modify query with no fragments', () => {
    expectInlined(dedent`
      {
        id
      }
    `).to.equal(dedent`
      {
        id
      }
    `);
  });

import { expect } from 'chai';
import { describe, it } from 'mocha';

import { parse, print } from '../../index';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use direct imports from appropriate files.


export function mergeAST(ast: DocumentNode): DocumentNode {
const fragmentDefinitions: { [name: string]: FragmentDefinitionNode } = {};
const inlinedFragments: { [name: string]: FragmentSpreadNode } = {};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use Object.create(null) here and use ObjMap for types.

* Given a document AST, merge all fragment definitions into the
* provided operations using inline fragments.
*/

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove empty line

return visit(ast, {
FragmentSpread(node) {
// No repeats!
if (inlinedFragments[node.name.value]) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is incorrect check, same fragment can be used in multiple places

{
  person: {
    ...ProfileInfo
    friend: {
      ...ProfileInfo
    }
  }
}

Also please cover in tests.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

covered in unit tests now :)

@IvanGoncharov
Copy link
Member

I found a failing case that wasn't in the original unit tests:

I think I know the reason, I will fix this.

@IvanGoncharov IvanGoncharov added the PR: feature 🚀 requires increase of "minor" version number label Jun 4, 2019
@acao acao force-pushed the add-refactor-mergeAST branch 5 times, most recently from 8199f23 to 8d1d14c Compare June 4, 2019 10:15
Copy link
Contributor

@langpavel langpavel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this work well with aliases and experimental fragment variables?

array: $ReadOnlyArray<any>,
iteratee: (item: any) => any,
) {
const FilteredMap = new Map();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not Set?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Set makes a lot more sense

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Map, Set should be used only for non-string keys, Object.create(null) is perfectly fine for working with string keys.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@IvanGoncharov Set is much better primitive for checking presence/uniqueness than Object.create(null), both by math sense and by the fact — Maps and Sets are well tested and perfectly tuned for purpose. Misusing objects as hash maps, well, it's only relique. Polyfilling is not needed today.

@acao
Copy link
Member Author

acao commented Jun 4, 2019

@langpavel good questions!

do you mean like this:

fragment UserFriends ($first: Int!, $after: Int) on User {
  name
  avatar
}

query {
   ...UserFriends(first: 100, after: null)
}

I don't see why it wouldn't, since this would still require visiting the same node types, but I can add some test cases to be sure

return {
...fragmentDefinitions[node.name.value],
kind: 'InlineFragment',
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please review type definitions before you use them or convert them into each other.
Fragment definition has way more fields than inline fragments.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for sure, adding more test cases, it can't be this simple

...node,
selections: uniqueBy(
node.selections,
selection => selection.name.value,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You also removing duplicated fields, including fields with different aliases, args, etc.

@acao acao changed the title add mergeAST from GraphiQL, refactor using visit() add mergeAST from GraphiQL, refactor using visit() [WIP] Jun 4, 2019
@acao
Copy link
Member Author

acao commented Jun 4, 2019

disabling until I've done more research

@acao acao closed this Jun 4, 2019
@benjie
Copy link
Member

benjie commented May 21, 2020

I have a more thorough implementation of this here: graphql/graphiql#1542

Let me know if you're interested into a PR to GraphQL.js

@IvanGoncharov
Copy link
Member

@benjie I think it would make sense to test it in GraphiQL to refine API and implementation and latter move to graphql-js.
Also, the relay compiler supports this and several similar transformations so we can learn from them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
PR: feature 🚀 requires increase of "minor" version number
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants