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

Is it possible to use graphql-tag to combine two predefined queries? #169

Closed
maxdarque opened this issue Apr 15, 2018 · 16 comments
Closed
Labels
feature New addition or enhancement to existing solutions question

Comments

@maxdarque
Copy link

maxdarque commented Apr 15, 2018

Is it possible to use graphql-tag to combine two predefined queries? The goal is to pass them into <Query/>. I say this because I only want to define each query once and re-use them across React components. i.e.

const bookQuery = gql`
  query ($bookId: ID!) {
    book(id: $bookId) {
      id
      author
    }
  }
`
const carQuery = gql`
  query ($carId: ID!) {
    car(id: $carId) {
      id
      name
    }
  }

Would it be possible to combine both into something like?

const oneQuery = gql`
  query ($bookId: ID! $carId: ID!) {
    book(id: $bookId) {
      id
      author
    }
    car(id: $carId) {
      id
      name
    }
  }
@jnwng
Copy link
Contributor

jnwng commented May 28, 2018

@maxdarque i'm not sure i fully understand the question here. the declaration you made for oneQuery is a valid GraphQL document, and when it goes to a GraphQL server, will come back like

{ 
  book: {
    id: '',
    author: ''
  },
  car: {
    id: '',
    name: ''
  }
}

now, that being said, are you asking whether or not gql could do the work for you to combine two separately defined queries? e.g., gql.combine([bookQuery, carQuery]) would emit oneQuery? generally, i think this is an interesting idea, although there's a lot of things to consider here:

  1. what do we name the query? do we just concatenate the two names?
  2. what happens when we have variable collisions? do we automagically overwrite the variable names ourselves?

i could use a more concrete use-case to figure out why combining them manually or using fragments doesn't solve this issue for you. e.g., could you define your queries as fragments instead?

fragment BookFragment on Root {
  book { ... }
}
fragment CarFragment on Root {
  car { ... }
}
query { ...BookFragment, CarFragment }

the downside here is that when you have parameterized fragments, the parent needs to know about those parameters, but maybe this spec might be useful for you: graphql/graphql-spec#204 (there's experimental support for parameterized fragments in graphql-tag)

let me know what you think! im going to close this for now just to get my workload under control.

@jnwng jnwng closed this as completed May 28, 2018
@jnwng jnwng added question feature New addition or enhancement to existing solutions labels May 28, 2018
@PinkyJie
Copy link

I also faced the same problem here, I think this is a very practical requirement, it makes sense sometimes we want to combine the query together, but as @jnwng mentioned above, there are a lot of potential problems to consider while doing this.

So what's the best practice for programmatically combine different queries together? The point is we want to reuse the predefined queries as much as possible.

@PinkyJie
Copy link

PinkyJie commented Aug 28, 2018

I had a workaround here:

const bookQuery = `
    book(id: $bookId) {
      id
      author
    }
`
const carQuery = `
    car(id: $carId) {
      id
      name
    }
`

const oneQuery = gql(String.raw`
  query ($bookId: ID!, $carId: ID!) {
    ${bookQuery}
    ${carQuery}
  }
`)

I know it's pretty hacky, if you have graphql syntax highlight plugin, this kind of lose the hightlight for the first two queries, but it actually make you reuse the predefined queries somehow.

@maxdarque
Copy link
Author

@jnwng

now, that being said, are you asking whether or not gql could do the work for you to combine two separately defined queries?

Yes this is what I was referring to. Rather than defining queries inside each React component, I like to centralize them in my api folder. This way, when there's a change, it reduces my surface area. I also have a preference for fragments for similar reasons.

Maybe the best approach would be use fragments in the fashion outlined below until there is mature support for parameterized fragments.

const fieldsOnBook = gql`
  fragment fieldsOnBook on Book {
    id
    author
  }
`;

const fieldsOnCar = gql`
  fragment fieldsOnCar on Car {
    id
    name
  }
`;

const bookQuery = gql`
  query ($bookId: ID!) {
    book(id: $bookId) {
      .... fieldsOnBook
    }
  }
 ${fieldsOnBook}
`
const carQuery = gql`
  query ($carId: ID!) {
    car(id: $carId) {
      ... fieldsOnCar
    }
  }
 ${fieldsOnCar}
`

const oneQuery = gql`
  query ($bookId: ID! $carId: ID!) {
    book(id: $bookId) {
      .... fieldsOnBook
    }
    car(id: $carId) {
      ... fieldsOnCar
    }
  }
  ${fieldsOnBook}
  ${fieldsOnCar}
`

@digeomel
Copy link

If the goal for combining queries is to minimize the number of HTTP requests, then you might want to look into Apollo query batching:
https://blog.apollographql.com/query-batching-in-apollo-63acfd859862

@vdzundza
Copy link

#169 (comment)

I'm doing in the same way. What's about build the queries conditionally?

@blocka
Copy link

blocka commented Jun 23, 2019

I have potential use case.

Imagine a scenario with nested routes. The parent component needs to load data, but then doesn't display it's children until after it finishes. You wind up with cascading requests, and cascading spinners. It would be cool if the two requests could be merged (I supposed there could be a link similar to the batching link that would be the hook for this). Especially if both components (parent and child) are making the same query with the same arguments (think the parent shows some summary info about an entity while the children show more details.

@kevin-krug
Copy link

kevin-krug commented Oct 22, 2019

the requested feature is now possible thanks to parameterized fragments 🎉 https://graphql.org/learn/queries/#using-variables-inside-fragments

assuming book and car query are fields of the root query type named "RootQueryType", the above graphql-tag example #169 (comment) could be implemented as follows:

const bookFragment = gql`fragment BookFragment on RootQueryType {
    book(id: $bookId) {
      id
      author
    }
  }
`;
const carFragment = gql`fragment CarFragment on RootQueryType {
    car(id: $carId) {
      id
      name
    }
  }
`;

const oneQuery = gql`
  query ($bookId: ID! $carId: ID!) {
    ...BookFragment
    ...CarFragment  
  }
  ${ bookFragment }
  ${ carFragment }
`;

@jhd124
Copy link

jhd124 commented Nov 22, 2019

The return value of gql is just an object, you can modify it.
Just inject one query into another
Say you have a query:

const serverInfoQuery = gql`
    query serverInfoQuery{
        serverInfo{
            ready: true
        }
    }
`;

And another query:

const userInfoQuery = gql`
    query userInfoQuery{
        userInfo{
            name: "Jack"
        }
    }
`;

To combine them into one:

const serverQueryInj = serverInfoQuery.definitions[0].selectionSet.selections[0];
const userQuerySelections = _.get(userInfoQuery, "definitions[0].selectionSet.selections");
userQuerySelections.push(serverQueryInj);

Then the userInfoQuery is what you want

@Sunsvision
Copy link

Sunsvision commented Dec 24, 2019

I created simple combine method that uses gql ready objects, extracts them and convert in one new combined gql

static combineQueries(...queries) {
    let inner = '';
    let name = '';
    let vars = '';

    queries.map((q) => {
        // extract variables in one string
        q.definitions[0].variableDefinitions.map((v) => {
            vars += vars ? ',' : '';
            vars += '$' + v.variable.name.value + ':' + v.type.name.value;
        });
        // combine name
        name += q.definitions[0].name.value;
        // extract inner body without external query()
        const body = q.loc.source.body.replace(/query(.)+?{/, '').trim();
        inner += inner ? ',' : '';
        inner += body.slice(0, body.length - 1);
    });

    name += 'Combined';

    return gql('query ' +  name + '(' + vars + ') {' + inner + '}');
}

@domasx2
Copy link

domasx2 commented Jul 4, 2020

Hi! I created a lib for this: https://github.com/domasx2/graphql-combine-query

import combineQuery from 'graphql-combine-query'

import gql from 'graphql-tag'

const fooQuery = gql`
  query FooQuery($foo: String!) {
    getFoo(foo: $foo)
  }
`

const barQuery = gql`
  query BarQuery($bar: String!) {
    getBar(bar: $bar)
  }
`

const { document, variables } = combineQuery('FooBarQuery')
  .add(fooQuery, { foo: 'some value' })
  .add(barQuery, { bar: 'another value' })

console.log(variables)
// { foo: 'some value', bar: 'another value' }

print(document)
/*
query FooBarQuery($foo: String!, $bar: String!) {
   getFoo(foo: $foo)
   getBar(bar: $bar)
}
*/

@digeomel
Copy link

digeomel commented Jul 4, 2020

Although the approaches mentioned above may work, I think a more efficient way to do it, would be to combine the queries (and I'm talking only about queries, not mutations, because I don't see the value there) at the tree level. We have a similar situation as the one described by @blocka , multiple components loading different parts of data for the same object at more or less the same time, but we would like each component to manage its own data. So, instead of having the parent component load data for all its children and pass it on to them, which is the approach we're following now, each component will try to load each own slice of the data more or less at the same time as its siblings.

So, imagine you have these 2 queries:

query ($bookId: ID!) {
    book(id: $bookId) {
        author
        title
        isbn
    }
}

query ($bookId: ID!) {
    book(id: $bookId) {
        isbn
        pages
        tags {
            tag
        }
    }
}

where $bookId is the same, instead of getting:

query ($bookId_1: ID!, $bookId_2: ID!) {
    book_1: book(id: $bookId_1) {
        author
        title
        isbn
    }
    book_2: book(id: $bookId_2) {
        isbn
        pages
        tags {
            tag
        }
    }
}

Which I believe is what the approaches above return, we would get:

query ($bookId: ID!) {
    book(id: $bookId) {
        author
        title
        isbn
        pages
        tags {
            tag
        }
    }
}

Depending on the underlying implementation, the previous solution may result it two separate queries to the database for the same record, while the last one is only one. It would be far more complicated to implement, having to compare variables at multiple levels, but wouldn't it be better? I think given that the gql method returns a tree-like structure anyway, it should nevertheless be feasible.

@ziggy6792
Copy link

Hi! I created a lib for this: https://github.com/domasx2/graphql-combine-query

import comineQuery from 'graphql-combine-query'

import gql from 'graphql-tag'

const fooQuery = gql`
  query FooQuery($foo: String!) {
    getFoo(foo: $foo)
  }
`

const barQuery = gql`
  query BarQuery($bar: String!) {
    getBar(bar: $bar)
  }
`

const { document, variables } = combineQuery('FooBarQuery')
  .add(fooQuery, { foo: 'some value' })
  .add(barQuery, { bar: 'another value' })

console.log(variables)
// { foo: 'some value', bar: 'another value' }

print(document)
/*
query FooBarQuery($foo: String!, $bar: String!) {
   getFoo(foo: $foo)
   getBar(bar: $bar)
}
*/

Does this work with mutations too? Would it be possible to get it working with mutations?

@domasx2
Copy link

domasx2 commented Sep 18, 2020

Hi! I created a lib for this: https://github.com/domasx2/graphql-combine-query

Does this work with mutations too? Would it be possible to get it working with mutations?

It does work with mutations!

@TamirCode
Copy link

Hi! I created a lib for this: https://github.com/domasx2/graphql-combine-query

import combineQuery from 'graphql-combine-query'

import gql from 'graphql-tag'

const fooQuery = gql`
  query FooQuery($foo: String!) {
    getFoo(foo: $foo)
  }
`

const barQuery = gql`
  query BarQuery($bar: String!) {
    getBar(bar: $bar)
  }
`

const { document, variables } = combineQuery('FooBarQuery')
  .add(fooQuery, { foo: 'some value' })
  .add(barQuery, { bar: 'another value' })

console.log(variables)
// { foo: 'some value', bar: 'another value' }

print(document)
/*
query FooBarQuery($foo: String!, $bar: String!) {
   getFoo(foo: $foo)
   getBar(bar: $bar)
}
*/

absolute legend

@boompikachu
Copy link

Is it possible to use graphql-tag to combine two predefined queries? The goal is to pass them into <Query/>. I say this because I only want to define each query once and re-use them across React components. i.e.

const bookQuery = gql`
  query ($bookId: ID!) {
    book(id: $bookId) {
      id
      author
    }
  }
`
const carQuery = gql`
  query ($carId: ID!) {
    car(id: $carId) {
      id
      name
    }
  }

Would it be possible to combine both into something like?

const oneQuery = gql`
  query ($bookId: ID! $carId: ID!) {
    book(id: $bookId) {
      id
      author
    }
    car(id: $carId) {
      id
      name
    }
  }

Regarding the oneQuery, if only the $bookId variables change, both book and car get refetch. Is there a way to make only book field change while keeping the car the same? Separating the queries work but is there a way to do it in combined query?

For example

 const oneQuery = gql`
   query ($bookId: ID! $carId: ID!) {
     book(id: $bookId) {
       id
       author
     }
     car(id: $carId) {
       id
       name
     }
     trains { // since trains will remain the same even when variables change, refetching this field every time the variables changed is not ideal
       id
       name
    }
   }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature New addition or enhancement to existing solutions question
Projects
None yet
Development

No branches or pull requests