Closed yungsters closed 8 years ago
@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.
Do you have any example code as to how the viewer
root field would look like on the server side (nodejs)?
@vincentriemer take a look at the todo app's schema for an example of setting up a root viewer
field.
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 GraphQLObjectType
s with fields in them that can be other GraphQLObjectType
s. However, it seems that the first level GraphQLObjectType
s 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.
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!
:+1:
Just ran into this. Would love to see root fields with multiple arguments!
One problem with the viewer
approach is that Relay seems not to let you specify multiple levels of nesting at the root query level:
const AppContainer = Relay.createContainer(App, {
fragments: {
projects: () => Relay.QL`
fragment on Project {
name
isMyProject
}
`
}
});
const AppQueries = {
projects: () => Relay.QL`
query {
viewer {
projects
}
}
`
};
ReactDOM.render(
<RelayRouter history={history}>
<Route path="/" component={AppContainer} queries={AppQueries} />
</RelayRouter>,
mountNode
);
Is there a workaround for making that work? Or should my root query always just be statically set to query { viewer }
?
@ForbesLindesay There's no technical reason we couldn't allow nesting in the root query. The only thing that would have to change is GraphQLFragmentPointer's createForRoot method, which makes a simplifying assumption that the fragment is a direct child - any interest in sending a PR? ;-)
Otherwise, yes, most apps end up with mostly viewer/node root queries.
@steveluscher has there been any progress on this? Is there anyway I can help? Right now, we're basically using JSON.stringify()
to send in multiple arguments via Relay to a root query. It's a very hacky solution, but seems to be working. Would love to be able to remove this cruft from our project sooner rather than later ;-)
The right solution seems like it would be to fix this issue. If you have pointers on what to do though I can try and produce a pull request. I'm not really clear on the details of what code does what.
@RavenHursT: Indeed, though at one time I was moving full steam on this project, it's been moved to the backburner. I laid some of the groundwork (we moved from storing an association between ‘fieldNames’ and ‘rootCallIds’ to one between a field's ‘storage key’ (a hash of its name and it sorted arguments) and its ‘identifying argument value.’ Next steps include:
rootCallMap
such that you can express associations between an identifying argument value (or EMPTY
if there's no identifier) and one or more data IDs ({[identifyingArgValue: string]: DataID | Array<DataID>}
)forEachRootCallArg
, rename it to forEachIdentifyingArgValue
, and teach it to store arrays of DataID
when the field in question is plural.forEachRootCallArg
assumes that root calls will have one argument and one argument only, and that any argument that appears is the identifying one. Tear down this assumption by creating a first-class GraphQL type GraphQLIdentifier
that you can use to ‘tag’ an argument as being the ‘identifying’ one in an identifying root field. Configure the printer to print metadata that calls out the identifying argument. Teach RelayRootQuery#getIdentifyingArg
to use this metadata to return the identifying argument (right now it just returns the first argument, if any).The first two tasks should enable plural non-identifying root fields. The third one should unlock non-identifying arguments on root fields.
@yungsters If I were to embrace the origins of Relay, and build a system of my own that has a similar "root call" named viewer
, is it fair to say that viewer
is sort of like the current active user's window into the nested data fields?
{
viewer {
accounts {
edges { node { id, username } }
}
}
}
If I were currently signed in as a some user, the accounts
field would take my session and authorization into account, on the back-end, and only provide me with the items I should be able to access, or any other properties that may change based on who I am?
How would this system work without the requirement of a user being signed in, but having the ability to create an account and sign in? With the accounts
connection being under the viewer
, it's not possible to mutate accounts
and so there's a chicken/egg problem there.
I'm trying to figure out how to embrace this design, and also build a user auth system entirely on top of GraphQL / Relay.
@ryancole You have the description of viewer
correct. For example, at Facebook, viewer
contains a news_feed
connection field that is always the current viewer's news feed. Another notable field that viewer
has is the actor
field which allows querying the current user's information.
As far as I know, our internal GraphQL endpoint was not originally designed for usage without authentication. However, the data model backing viewer
does support the notion of an "unauthenticated user". More recently, we've augmented the GraphQL endpoint to support fetching fields for unauthenticated users, but off the top of my head, I do not know what is and is not accessible.
What I imagine to be common with most applications that deal with authentication (and which is certainly true for Facebook) is for the entire client store to be reset when authenticating as a user. This is useful for a variety of reasons including isolation of concerns (e.g. modules that can assume there is a logged in user vs. those that do not make this assumption) as well as privacy and security (i.e. the simplest way to ensure that there is no cross-contamination or leaking of a user's private information is to start with a clean slate).
For example, most of the Facebook mobile apps built using React Native and Relay implement authentication without Relay, but — upon obtaining an access token — switch to relying on Relay for all user client data management.
This would certainly complicate things in a system where a user can authenticate using multiple accounts (e.g. something like an operating system such as Windows or OS X where a user can be logged in as multiple user simultaneously). But if you do not need this degree of control, I would try to keep things simple. Hope this makes sense / helps!
With the accounts connection being under the viewer, it's not possible to mutate accounts and so there's a chicken/egg problem there.
Is this actually true? I believe that if your viewer
has a global ID (i.e. it's a Node), it will work just fine. Maybe not though, I haven't actually tried it.
Just ran into similar issues to @ForbesLindesay on a side project I was working on.
My work-around was to create a self-pointer to my root type on my root type:
var rootType = new GraphQLObjectType({
name: 'Root',
fields: () => ({
root: {
type: new GraphQLNonNull(rootType),
resolve: () => ({})
},
...restOfRootType
}),
});
export default new GraphQLSchema({query: rootType});
and then all of my routes use the same RootQuery
:
export default {
root: () => Relay.QL`
query {
root
}
`,
};
Instead of then creating a deeply nested query to pass to my route, I just create a wrapper component:
function wrapWithC(component) {
class CWrapper extends React.Component {
render() {
return React.createElement(
component,
{c: this.props.root.a.b.c},
this.props.children
);
}
}
return Relay.createContainer(CWrapper, {
initialVariables: {
a: 'a',
b: 'b',
c: 'c',
},
fragments: {
root: () => Relay.QL`
fragment on Root {
a(aVar: $a) {
b(bVar: $b) {
c(cVar: $c) {
${component.getFragment('c')}
}
}
}
}
`,
},
});
}
return <Route
path={path}
component={wrapWithC(ComponentThatTakesAC)}
queries={RootQuery}
/>;
This has ended up working reasonably well for me; I end up not really using the root query functionality to drill down to the object in my schema, and use the wrappers to do that instead.
@dschafer thanks for sharing! :+1:
The ability to have nesting in root queries per @ForbesLindesay's comment would be great. Would make composition and refactoring easier.
There's no technical reason we couldn't allow nesting in the root query. — @josephsavona
Really? Doesn't there have to be at most one leaf in a root query? Otherwise, how would Relay know at which leaf to anchor fragments?
Doesn't there have to be at most one leaf in a root query?
Yes, but Relay could do a recursive search for fragment references (throwing if there is not exactly one of them).
Note: We've recently made progress on this with #894 and #895 - these allow root field arguments to be any type including enums and input objects. I've updated the description with next steps.
I wish Relay would support routes like this one (using slugs instead of IDs):
{
path: '/:username/products/:slug',
queries: {
viewer: () => Relay.QL`query { viewer }`,
product: () => Relay.QL`query { product(username: $username, slug: $slug) }`
},
component: Product
}
or, even better this:
{
path: '/:username/products/:slug',
query: () => Relay.QL`query {
viewer,
product(username: $username, slug: $slug)
}`,
component: Product
}
That is supposed to match a URL such as /koistya/products/t-shirt
.
If you have a tip for me how to make it work, I would greatly appreciate that!
Have you tried using https://github.com/relay-tools/react-router-relay ?
Nope. But I think my problem is not the router itself (a custom one works great for me), but the lack of the support of multiple arguments in root queries. Also, I'd like to find a way to make Realy send a single query request. So I could define my routes as per example above (a single query
request with multiple top-level fields in it).
@koistya somewere Jimmy Jia wrote that to make single request for multiple queries - it is the task of Network Layer. Such NL does not exist yet, but I found https://github.com/eyston/relay-composite-network-layer This is a lit bit different thing, but may be useful as an example for writing such NL.
BTW when connection is root query, you will have different logic with infinite scrolling list. You should pass first
arg to router (then it should be passed to query), nor this.props.relay.setVariables({first: incValue})
. And I had some problem with sorting (it was problem with multiple arguments in root queries).
So I come back to single root, aka Viewer
, and happy again like an elephant.
@steveluscher do you know when this will ship?
@mattecapu We aren't actively working on this. I'd recommend checking out @dschafer's comment for a good workaround.
@josephsavona thank you, can I know why this isn't a priority? I was pretty baffled to discover Relay isn't capable yet to do this kind of things as they seem very natural. As of @dschafer's comment, it looks like a workaround for the multiple-parameters issue, while I need to fetch an array
Comp1 = Relay.createContainer(Comp1, {
fragments: {
comp1: () => Relay.QL`
fragment on Comp1 @relay(plural: true) {
${childComp2.getFragment('comp2')}
}`
}
});
export const Comp1Queries = {
comp1: () => Relay.QL`
query { comp1s(id: $comp1ID) }`
};
But I get
RelayOSSNodeInterface: Expected payload for root field
comp1s
to be a single non-array result, instead received an array with 1 results.
And I'm not really grasping the viewer
workaround.
FWIW, since the workaround is so simple (add a "viewer"-esque field on your root query), I'm pretty OK that the Relay team is focusing on other stuff 😁
On Mon, Apr 11, 2016, 10:35 AM Matteo Capucci notifications@github.com wrote:
@josephsavona https://github.com/josephsavona thank you, can I know why this isn't a priority? I was pretty baffled to discover Relay isn't capable yet to do this kind of things as they seem very natural.
— You are receiving this because you commented. Reply to this email directly or view it on GitHub https://github.com/facebook/relay/issues/112#issuecomment-208376966
@mattecapu:
can I know why this isn't a priority?
Prioritization is tough, because there are always a lot more things that we would like to do than we have immediate capacity for. Even articulating why decisions are made the way they are can be tough. Perhaps the best way to get insight into what we're working on and why is to take a look at our meeting notes. Let us know if you'd like to know more about any of the things that we're actively working on mentioned in there.
@NevilleS the issue is that this thread is perhaps the only documentation of this "feature", so it's a bit disappointing to dive into Relay and then stumble upon this obstacle. Moreover the viewer
design pattern is not explained anywhere (at least I can't find an explanation, I'll be happy to be wrong on this!) and as a beginner I scratched my head looking at the example and thinking "Why this?".
In fact I'd be happy to understand how to work around this for now.
@wincent I may sound pedant/unrespectful but actually I'm a great fan of the whole React/Relay/GraphQL thing and I understand the last two are still a pretty novel technology so it's fair to have some issues! I want you (and the rest of the team) to know I really appreciate your work and effort in doing this! OSS is hard and you're doing fine anyway :smile: The question was more a matter of understanding, because the queries I wrote seemed straightforward to me and clashing with this issue made me wonder if I really understood how Relay is supposed to work or I'm missing something.
Okay building on @dschafer's solution I managed to work around this. For now is just an ad-hock hack but I hope to encapsulate this logic in a general-purpose wrapper.
I edited the code above in the following way:
const RootQuery = new GraphQLObjectType({
name: 'RootQuery',
description: 'Root query',
fields: () => ({
// HACK to make queries at root level
rootHack: {
type: new GraphQLNonNull(RootQuery),
resolve: () => ({})
},
...restOfRootType
});
// ...
export default new GraphQLSchema({
query: RootQuery
});
class Comp1 extends React.Component {
render() {
// comp1s, in my case, is an array (its type is GraphQLList(comp1))
return (
<Comp2 comp1={this.props.comp1query.comp1s} />
);
}
}
Comp1 = Relay.createContainer(Comp1, {
initialVariables: {
// this is then changed by react-router based on a URL parameter with the same name
comp1ID: 0
},
fragments: {
comp1query: () => Relay.QL`
fragment on RootQuery {
comp1s(id: $comp1ID) {
${childComp2.getFragment('comp2')}
}
}`
}
});
// this will be the same for every page
const Comp1Queries = {
comp1: () => Relay.QL`
query { rootHack }`
};
My react-router
routes
<Router>
<Route path={"/comp1/:comp1ID"}
component={comp1} queries={Comp1Queries}
prepareParams={({ comp1ID }) => ({ comp1ID: parseInt(comp1ID) })} />
</Router>
Hope to help someone with this
Do I understand correctly that this issues covers just root fields? I was wondering if there are any existing or planned options how to tell relay that some argument is identifying or that its the graphQL id. Therefore Relay could just fetch additional data.
It would make fetching some additional information very convenient, because if I want get (on demand) some additional information in some component I need to do it in fragment. I don't see how I could make it with root fields which are always defined along with root container&component.
any existing or planned options how to tell relay that some argument is identifying or that its the graphQL id
Can you clarify how this would be different than node
?
It would make fetching some additional information very convenient,
Hmm. Can you clarify your goal here? There are two options for fetching data in a component, whichever is more convenient: use setVariables
/forceFetch
or manually construct a query and fetch it. The second option is easy to overlook, but you can always create a root query without a route and fetch it:
const query = Relay.createQuery(Relay.QL`query Foo { ... }`, variables);
RelayStore.primeCache({query}, readyState => {
if (readyState.error) {
console.error(readyState.error);
} else if (readyState.done) {
// data is ready
}
});
Note that this method will write the results of the query into the cache, and all affected components will re-render if necessary (i.e. you don't need to call setState
or anything in the callback above).
@josephsavona I apologize for vague description what I had in mind. Example should clear it up.
export default Relay.createContainer(ComponentA, {
initialVariables: {
formatIds: [],
},
fragments: {
viewer: () => Relay.QL`
fragment on Viewer {
formats (ids: $formatIds) {
id
name
code
}
}
`
}
})
This is the on demand scenario when I want to get metadata for formats using setVariables
. My use case is that I have some general informations for all formats (from initial 'static' query) but need to get some extra informations on demand on different places in the app and wanted to reduce over-fetching. So I know the ids for formats and want to get extra fields on-demand for few of them.
But afaik it is not possible to tell Relay that I am gonna use list of actual graphQL ids. Therefore it re-fetches format for any new combination of ids.
So lets say at first I would do:
1) setVariables({formatIds: [id1, id2]})
, relay fetches formats id1 and id2 with all required fields in this fragment instead of just missing fields.
2) setVariables({formatIds: [id3, id2]})
, relay fetches formats with id3 and id2 instead of just missing fields for id3
node
field is the closest thing, but I can't use it for list of ids and use it in component fragment to get data from container.
It might even be nice to have a concept of nodes()
field which would require either a type
or ids
param but also would allow for some additional optional filters so this could be used for fetching all nodes of the given type, fetch all nodes given the list of ids and have some additional customer filters (e.g. I want only nodes of a type that include a title)
Looking something like this:
const nodesQueryConfig = {
queries: {
nodes: () => {
return Relay.QL`query { nodes(ids: $ids) }`
}
},
name: 'nodesQueryConfig',
params: {
ids: [id1, id2]
}
};
Or of course wrapped in the viewer.
aahh @jardakotesovec have you seen the @relay(plural: true)
option yet? That would allow you to ask for multiple ids and return you all these elements.
node field is the closest thing, but I can't use it for list of ids and use it in component fragment to get data from container.
Relay supports plural identifying root fields which work exactly as you're describing. If you implement a nodes(ids: [ID!]): [Node]
field in the schema, Relay understands that each of the id arguments corresponds to the specific Node
with that id. If you fetch nodes(ids: [1,2])
and then nodes(ids: [2,3])
, Relay will know that id 2 has already been fetched, and diff this to only fetch node(id: 3)
.
@josephsavona
Note that this method will write the results of the query into the cache, and all affected components will re-render if necessary (i.e. you don't need to call setState or anything in the callback above).
I actually tried that, I built a query to fetch some additional fields on a node and sent it to the node interface.
The query built by Relay looks correct (only the missing fields are asked), but the result is never merged into the cache.
The only way I found to get the updated data is Relay.Store.readQuery(query)[0];
Any idea what I'm doing wrong ?
@kamek-pf the public data-fetching APIs always merge data into the cache. If you're not seeing an update one of two things could be happening:
Either way, can you open a new issue for this? This is tangential to the OP.
@josephsavona I am aware of the plural identifying root field, but I have two issues there
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 fieldSo, 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.
@LegNeato The node
field needs to be on Query
, alongside your root
node.
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.
@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) }
`
})
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...
}
},
`,
},
});
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!
@maletor That extra level of indirection isn't necessary. It's sufficient to do:
query {
viewer {
rootConnection
multipleArgs(a: 1, b: 2)
}
}
@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.
@yungsters maybe you can comment on this?
This is working fine on Relay Modern
Problem
Relay currently only knows how to handle these three types of root fields:
empire
queries{id: "123", ...}
ship(id: "456")
queries{id: "456", ...}
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:
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 toviewer
.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
, andusernames
. 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 theme
root field and the node with ID of 123. Now, if we ever encounter another query forme
, 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 forme
.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 fornode(id: "123")
andnode(id: "456")
even though we may never have independently queried for either before. (We would also be able to fulfillme
if the association from above was established.)Next Steps
id
of the response. Currently all root arguments are assumed to be identifying.