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

Support Root Fields w/o Node Mapping #112

Closed
2 of 4 tasks
yungsters opened this issue Aug 18, 2015 · 53 comments
Closed
2 of 4 tasks

Support Root Fields w/o Node Mapping #112

yungsters opened this issue Aug 18, 2015 · 53 comments

Comments

@yungsters
Copy link
Contributor

Problem

Relay currently only knows how to handle these three types of root fields:

  • Root field with no arguments and queries a single node.
    • e.g. empire queries {id: "123", ...}
  • Root field with one argument and queries a single node.
    • e.g. ship(id: "456") queries {id: "456", ...}
  • Root field with one array argument and queries an array of nodes.
    • e.g. ships(ids: ["456", "789"]) queries [{id: "456", ...}, {id: "789", ...}]

However, it has become clear that Relay needs to support any kind of root field. For example:

  • Root field with multiple arguments.
  • Root field with no arguments and queries an array of nodes.
  • Root field that is a connection.

Workaround

For now, the unsupported use cases can be implemented by creating a "global node", commonly called the viewer. You can then add arbitrary fields to viewer.

static fragments = {
  viewer: () => Relay.QL`
    fragment on Viewer {
      # Can return an array of users.
      users
    }
  `
};

Rationale

Historically, GraphQL (as used internally at Facebook) did not have a root type or root fields. Instead, it had special "root calls" such as node, nodes, me, and usernames. Much of Relay was built on top of this assumption that the "root calls" return nodes.

For example, when we fetch me and get {id: "123", ...}, we record the association between the me root field and the node with ID of 123. Now, if we ever encounter another query for me, we can check our store for the node with ID of 123 and resolve the query without having to potentially re-fetch all of the fields we already have for me.

Another example, when we fetch nodes(ids: ["123", "456"]), we record the association between each argument and their respective response nodes. This allows us to fulfill queries for node(id: "123") and node(id: "456") even though we may never have independently queried for either before. (We would also be able to fulfill me if the association from above was established.)

Next Steps

  • Support literal enum and input object values (babel plugin: support input object literals #894)
  • Support arbitrary values in root calls (Non string identifying argument values #895)
  • Define a consistent method for annotating a root argument as "identifying". Identifying arguments have a 1:1 correspondence between the argument value and the id of the response. Currently all root arguments are assumed to be identifying.
  • Allow arbitrary root calls with or without identifying arguments.
@devknoll
Copy link
Contributor

@yungsters @josephsavona is it possible that this change would also allow us to remove the Relay-specific Input Objects on mutations? It seems super strange that GraphQL supports multiple arguments but then Relay forces them to all be wrapped up ;-)

Edit: would lose the ability to have a single variable in the document and stuff all the values into a variable as an input object though.

@vincentriemer
Copy link
Contributor

Do you have any example code as to how the viewer root field would look like on the server side (nodejs)?

@josephsavona
Copy link
Contributor

@vincentriemer take a look at the todo app's schema for an example of setting up a root viewer field.

@nickretallack
Copy link

This really confused me. This limitation should be spelled out more clearly in the docs and in relay-starter-kit.

There is already a root object: the GraphQLSchema. Its direct descendants can be GraphQLObjectTypes with fields in them that can be other GraphQLObjectTypes. However, it seems that the first level GraphQLObjectTypes are different from all others in that Relay refuses to query them in certain ways.

In the starter kit, the root object is queryType, and it defines a field viewer which is a userType. queryType even has a comment in it: // Add your own root fields here. But you don't want to add your own root fields here because they will have limited functionality. You really want to add them into the viewer. So does it make sense for the viewer to be a userType? I don't think it does.

I think the starter kit should change the userType into a generic wrapper type and move the comment into that type instead.

Btw, I really wanted to create a connection type at the root level. Requiring two levels of GraphQLObjectType in a row to get to normal functionality seems silly.

As for making things continue to work at Facebook, I don't know the specifics, but it seems like removing this restriction shouldn't negatively impact anything, right? You'd just need to make Relay's parser smart enough to parse queries that don't fall under this restriction. If you wanted to keep the restriction internally, you could write some sort of validation process to check if anyone is violating the restriction before submitting their code.

@steveluscher
Copy link
Contributor

Thanks for this, @nickretallack. Right now, I'm working on literally nothing else but enabling identifying/non-identifying singular/plural fields at the root, including connections. Hold tight!

@jardakotesovec
Copy link
Contributor

@josephsavona I am aware of the plural identifying root field, but I have two issues there

  • I need this field formats it on different places in the app inside components and I don't see how I could use them inside fragment, since its root field
  • And yes it allows to link keys with objects, but it does not know that its actual graphQL id, therefore if I get all formats (with some basic fields) at the beginning and want to get extra fields using root identyfing fields, it fetches all fields and creates additional mapping between id and object. This issue was mentioned here.

@LegNeato
Copy link

LegNeato commented Jul 20, 2016

So, with the viewer workaround, how do we query the node root field if everything goes through viewer? Is this what the additional "self-pointer to my root type" workaround is used for? So you can do something like:

fragment on Viewer {
  root {
    node(id: $whatever) {
    ...
  }
}

But doing that gives me:

You defined a `node(id: ID!)` field on type `Query`, but Relay requires the `node` field to be defined on the root type.

@mjtamlyn
Copy link

@LegNeato The node field needs to be on Query, alongside your root node.

@LegNeato
Copy link

I have it there but still not getting how to query it from client code as the client fragments all refer to viewer.

const queryType = new GraphQLObjectType({
  name: 'Query',
  fields: () => ({
    node: nodeField,
    viewer: {
      type: viewerType,
    },
  }),
});

I'll poke at it some more and take it to stackoverflow if I still don't get it.

@miracle2k
Copy link

miracle2k commented Jul 21, 2016

@LegNeato I used to have my own copy of node called object() below of client. I believe the reason I named it "object" is the error you posted above. The check for the location of "node" that Relay does is, I think, to dumb to understand that this is an additional node field. Try to rename it.

However, have you considered (I am doing this now instead myself) just using two root queries:

export const relayRoute = (queries, paramsGetter) => BaseComponent => {
  // Generate a route
  class GeneratedRoute extends Relay.Route {
    static queries = queries;
    static paramDefinitions = {};
    static routeName = 'GeneratedRoute';
  }

  // Return a HOC that injects a route generated by routeGetter.
  return withProps(ownerProps => {
    // Let user's function generate the route
    const params = paramsGetter(ownerProps);
    return {relayRoute: new GeneratedRoute(params)};
  })(BaseComponent);
}
const route = relayRoute({
client: () => Relay.QL`
    query { client }
`,
menu: () => Relay.QL`
    query { menu: node(id: $id) }
`
})

@LegNeato
Copy link

I ended up figuring it out, or at least something that works nicely. @miracle2k I think I can't just blindly use $id as some routes have multiple nested ids. Honestly didn't try it though.

In the schema definition:

// This is top-level / "root" node.
const queryType = new GraphQLObjectType({
  name: 'Query',
  fields: () => ({
    // The node query lets us query for any object with an id w/o having to manually define
    // queries under the viewer "global node". 
    node: nodeField,
   // This is the "global node" workaround mentioned above.
    viewer: {
      type: viewerType,
    },
  }),
});

export const Schema = new GraphQLSchema({
  query: queryType,
});

In the react-router definitions:

// This means when BarComponent is rendered, it will
// do a node query with the barID from the URL and query the node fields defined in
// the BarComponent component fragment named "node".
<Route path=":fooID/edit/:barID" component={BarComponent} queries={{
    viewer: () => Relay.QL`query { viewer }`,
    node: () => Relay.QL`query { node(id: $barID) }`,
 }} />

In the client component:

export default Relay.createContainer(BarComponent, {
  fragments: {
    // Define what we need to query from the viewer "global node".
    viewer: () => Relay.QL`
      fragment on Viewer {
        // Whatever fields...
      }
    `,
    // Define what we need to query from the node.
    node: () => Relay.QL`
      fragment on Node {
        id,
        ... on Bar {
            // Whatever fields...
        }
      },
    `,
  },
});

@wincent
Copy link
Contributor

wincent commented Sep 3, 2016

We're preparing Relay 2, which completely removes the restrictions and special casing around root fields. So I'm going to close this as we're unlikely to take any direct action on it, but people using Relay 1 will still be able to find this issue via search. Thanks to everybody who has participated in the thread!

@josephsavona
Copy link
Contributor

@maletor That extra level of indirection isn't necessary. It's sufficient to do:

query {
  viewer {
    rootConnection
    multipleArgs(a: 1, b: 2)
  }
}

@ermik
Copy link

ermik commented Mar 4, 2018

@wincent I ran into an issue recently where createRefetchContainer refetching a root query prop wouldn't subscribe to the store. The refetch() calls would use the component configuration fragment and the query correctly, successfully refetching data, but the component would never get initial query props nor would they update on refetch. Tests in a dedicated <QueryRenderer /> as well as in a more complex component tree has shown this to be an issue. But once the query field was moved on a second level (e.g. viewer or anything else) — the component behaves as expected.

Problematic code:

{
    query: graphql`
      fragment RelaySelectComponent_query on Query
        @argumentDefinitions(s: { type: "String", defaultValue: "beer" }) {
        products(search: $s) {
          edges {
            node {
              id
              gtin
              description
            }
          }
        }
      }
    `
  },
  graphql`
    # Refetch query to be fetched upon calling refetch.
    # Notice that we re-use our fragment and the shape of this query matches our fragment spec. $count: Int,
    query RelaySelectComponentRefetchQuery($s: String = "wine") {
      ...RelaySelectComponent_query @arguments(s: $s)
    }
}

I am not as experienced as one (i.e. me) would hope, so I can't make it into a definitive bug report, but I'm asking, respectfully, you guys take a look. I wasted good 20 work hours trying to find a solution.

@ermik
Copy link

ermik commented May 12, 2018

@yungsters maybe you can comment on this?

@sibelius
Copy link
Contributor

This is working fine on Relay Modern

@ermik
Copy link

ermik commented May 12, 2018

@sibelius the code snippet above was tested last in Relay 1.4.1 and was failing. I might be mistaken of course.

@Visva92
Copy link

Visva92 commented Mar 8, 2021

Problem

Relay currently only knows how to handle these three types of root fields:

  • Root field with no arguments and queries a single node.

    • e.g. empire queries {id: "123", ...}
  • Root field with one argument and queries a single node.

    • e.g. ship(id: "456") queries {id: "456", ...}
  • Root field with one array argument and queries an array of nodes.

    • e.g. ships(ids: ["456", "789"]) queries [{id: "456", ...}, {id: "789", ...}]

However, it has become clear that Relay needs to support any kind of root field. For example:

  • Root field with multiple arguments.
  • Root field with no arguments and queries an array of nodes.
  • Root field that is a connection.

Workaround

For now, the unsupported use cases can be implemented by creating a "global node", commonly called the viewer. You can then add arbitrary fields to viewer.

static fragments = {
  viewer: () => Relay.QL`
    fragment on Viewer {
      # Can return an array of users.
      users
    }
  `
};

Rationale

Historically, GraphQL (as used internally at Facebook) did not have a root type or root fields. Instead, it had special "root calls" such as node, nodes, me, and usernames. Much of Relay was built on top of this assumption that the "root calls" return nodes.

For example, when we fetch me and get {id: "123", ...}, we record the association between the me root field and the node with ID of 123. Now, if we ever encounter another query for me, we can check our store for the node with ID of 123 and resolve the query without having to potentially re-fetch all of the fields we already have for me.

Another example, when we fetch nodes(ids: ["123", "456"]), we record the association between each argument and their respective response nodes. This allows us to fulfill queries for node(id: "123") and node(id: "456") even though we may never have independently queried for either before. (We would also be able to fulfill me if the association from above was established.)

Next Steps

  • Support literal enum and input object values (babel plugin: support input object literals #894)
  • Support arbitrary values in root calls (Non string identifying argument values #895)
  • Define a consistent method for annotating a root argument as "identifying". Identifying arguments have a 1:1 correspondence between the argument value and the id of the response. Currently all root arguments are assumed to be identifying.
  • Allow arbitrary root calls with or without identifying arguments.

@Visva92 Visva92 mentioned this issue Mar 8, 2021
Closed
@facebook facebook deleted a comment from Visva92 Mar 10, 2021
@ghost
Copy link

ghost commented Aug 7, 2021

Thanks for the help and efforts, please remove all api's and reset my accounts and balance to zero (0). Since, I was bugged or hacked.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests