-
Notifications
You must be signed in to change notification settings - Fork 2k
add mergeAST from GraphiQL, refactor using visit() [WIP] #1948
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
Conversation
023aa09
to
f78ec10
Compare
f78ec10
to
19a8337
Compare
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 🤔 |
src/utilities/mergeAST.js
Outdated
// Collect the existing FragmentDefinition nodes. | ||
fragmentDefinitions[node.name.value] = node; | ||
}, | ||
}); |
There was a problem hiding this comment.
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, '')); | ||
}); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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.
src/utilities/mergeAST.js
Outdated
|
||
export function mergeAST(ast: DocumentNode): DocumentNode { | ||
const fragmentDefinitions: { [name: string]: FragmentDefinitionNode } = {}; | ||
const inlinedFragments: { [name: string]: FragmentSpreadNode } = {}; |
There was a problem hiding this comment.
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.
src/utilities/mergeAST.js
Outdated
* Given a document AST, merge all fragment definitions into the | ||
* provided operations using inline fragments. | ||
*/ | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove empty line
src/utilities/mergeAST.js
Outdated
return visit(ast, { | ||
FragmentSpread(node) { | ||
// No repeats! | ||
if (inlinedFragments[node.name.value]) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 :)
I think I know the reason, I will fix this. |
19a8337
to
5d3b34a
Compare
8199f23
to
8d1d14c
Compare
8d1d14c
to
997ead9
Compare
There was a problem hiding this 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(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not Set
?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 — Map
s and Set
s are well tested and perfectly tuned for purpose. Misusing objects as hash maps, well, it's only relique. Polyfilling is not needed today.
@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', | ||
}; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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.
disabling until I've done more research |
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 |
@benjie I think it would make sense to test it in GraphiQL to refine API and implementation and latter move to |
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
'svisit
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'svisit()
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.