I have a page that uses GraphQL Relay connection which fetches drafts.
query {
connections {
drafts(first: 10) {
edges {
node {
... on Draft {
id
}
}
}
}
}
}
In this page, I also create draft through CreateDraftMutation.
mutation {
createDraft(input: {
clientMutationId: "1"
content: "content"
}) {
draft {
id
content
}
}
}
After this mutation, I want Relay to add the created draft into its store. The best candidate for mutation config is RANGE_ADD, which is documented as following:
https://facebook.github.io/relay/docs/guides-mutations.html
RANGE_ADD
Given a parent, a connection, and the name of the newly created edge in the response payload Relay will add the node to the store and attach it to the connection according to the range behavior specified.
Arguments
parentName: string
The field name in the response that represents the parent of the connection
parentID: string
The DataID of the parent node that contains the connection
connectionName: string
The field name in the response that represents the connection
edgeName: string
The field name in the response that represents the newly created edge
rangeBehaviors: {[call: string]: GraphQLMutatorConstants.RANGE_OPERATIONS}
A map between printed, dot-separated GraphQL calls in alphabetical order, and the behavior we want Relay to exhibit when adding the new edge to connections under the influence of those calls. Behaviors can be one of 'append', 'ignore', 'prepend', 'refetch', or 'remove'.
The example from the documentation goes as the following:
class IntroduceShipMutation extends Relay.Mutation {
// This mutation declares a dependency on the faction
// into which this ship is to be introduced.
static fragments = {
faction: () => Relay.QL`fragment on Faction { id }`,
};
// Introducing a ship will add it to a faction's fleet, so we
// specify the faction's ships connection as part of the fat query.
getFatQuery() {
return Relay.QL`
fragment on IntroduceShipPayload {
faction { ships },
newShipEdge,
}
`;
}
getConfigs() {
return [{
type: 'RANGE_ADD',
parentName: 'faction',
parentID: this.props.faction.id,
connectionName: 'ships',
edgeName: 'newShipEdge',
rangeBehaviors: {
// When the ships connection is not under the influence
// of any call, append the ship to the end of the connection
'': 'append',
// Prepend the ship, wherever the connection is sorted by age
'orderby(newest)': 'prepend',
},
}];
}
/* ... */
}
If the parent is as obvious as faction, this is a piece of cake, but I've been having hard time identifying parentName and parentID if it came directly from query connections.
How do I do this?
Edit:
This is how query was exported.
export default new GraphQLObjectType({
name: 'Query',
fields: () => ({
node: nodeField,
viewer: {
type: viewerType,
resolve: () => ({}),
},
connections: {
type: new GraphQLObjectType({
name: 'Connections',
which in return is used in the relay container
export default Relay.createContainer(MakeRequestPage, {
fragments: {
connections: () => Relay.QL`
fragment on Connections {
I've been having hard time identifying parentName and parentID if it
came directly from query connections.
faction and ships in the example from Relay documentation are exactly the same as connections and drafts in your case. Each GraphQLObject has an ID,so does your connections object. Therefore, for your mutation, parentName is connctions and parentID is the ID of connections.
query {
connections {
id
drafts(first: 10) {
edges {
node {
... on Draft {
id
}
}
}
}
}
}
By the way, I guessconnections and drafts are terms from your application domain. Otherwise, connections confuses with GraphQL connection type.
Related
I have a nested component in my app.
At the top of the page, I have a query like
const REPOSITORY_PAGE_QUERY = gql`
query RepositoryPageQuery($name: String!, $owner: String!) {
repository(name: $name, owner: $owner) {
...RepositoryDetailsFragment
}
}
${REPOSITORY_DETAILS_FRAGMENT}
`;
RepositoryDetailsFragment then includes
// list of branches
refs(first: 2, refPrefix: "refs/heads/") {
...BranchesFragment
}
and finally
fragment BranchesFragment on RefConnection {
totalCount
pageInfo {
...PageInfoFragment
}
edges {
node {
id
name
}
}
}
${PAGE_INFO_FRAGMENT}
Obviously, I am not happy, because I need to pass BranchesFragment info around 3 levels deep.
Instead, it would be great if I could read it from the cache directly in my BranchesList component.
I tried to use
client.cache.readFragment({
fragment: BRANCHES_FRAGMENT,
fragmentName: "BranchesFragment"
});
But the problem is that this fragment does not have any id. Is there any way to deal with it and get the fragment info?
Alright, I suddenly came to the solution. Maybe it could be useful for others.
Imagine we have a hierarchy of query -> fragments and components -> subcomponents like this:
RootPageComponent
query
query RepositoryPageQuery(
$name: String!
$owner: String!
$count: Int!
$branchSearchStr: String!
) {
repository(name: $name, owner: $owner) {
...RepositoryDetailsFragment
}
}
${REPOSITORY_DETAILS_FRAGMENT}
component returns the following
<RepositoryDetails repository={data.repository} />
RepositoryDetails
Has a fragment
fragment RepositoryDetailsFragment on Repository {
name
descriptionHTML
defaultBranchRef {
id
name
}
# the branches repository has
refs(first: $count, refPrefix: "refs/heads/", query: $branchSearchStr) {
...BranchesFragment
}
}
${BRANCHES_FRAGMENT}
and returns <BranchesList /> component.
So, instead of passing branch.info from RootPage to RepositoryDetails and then to BranchesList;
You can do the following in BranchesList
const client = useApolloClient();
client.cache.readFragment({
fragment: BRANCHES_FRAGMENT,
fragmentName: "BranchesFragment",
id: "RefConnection:{}" // note this {} - apollow cache adds it when no id is present for the object
})
IMPORTANT!
Make sure to also update type policy for the field and set keyArgs to []
So in this particular case:
RefConnection: {
keyFields: []
...
}
This will give the same result, but you won't have to pass props to nested components and instead can read from cache directly (just like one would do using redux)
When I'm making a request to my backend through a mutation like that:
mutation{
resetPasswordByToken(token:"my-token"){
id
}
}
I'm getting a response in such format:
{
"data": {
"resetPasswordByToken": {
"id": 3
}
}
}
And that wrapper object named the same as the mutation seems somewhat awkward (and at least redundant) to me. Is there a way to get rid of that wrapper to make the returning result a bit cleaner?
This is how I define the mutation now:
export const ResetPasswordByTokenMutation = {
type: UserType,
description: 'Sets a new password and sends an informing email with the password generated',
args: {
token: { type: new GraphQLNonNull(GraphQLString) },
captcha: { type: GraphQLString },
},
resolve: async (root, args, request) => {
const ip = getRequestIp(request);
const user = await Auth.resetPasswordByToken(ip, args);
return user.toJSON();
}
};
In a word: No.
resetPasswordByToken is not a "wrapper object", but simply a field you've defined in your schema that resolves to an object (in this case, a UserType). While it's common to request just one field on your mutation type at a time, it's possible to request any number of fields:
mutation {
resetPasswordByToken(token:"my-token"){
id
}
someOtherMutation {
# some fields here
}
andYetAnotherMutation {
# some other fields here
}
}
If we were to flatten the structure of the response like you suggest, we would not be able to distinguish between the data returned by one mutation from another. We likewise need to nest all of this inside data to keep our actual data separate from any returned errors (which appear in a separate errors entry).
I am new to Relay, and I am having problems making it work with a GraphQL server.
I have adapted the Tea sample from the relay homepage to the SWAPI relay service. I cloned swapi-graphql, and added cors to the express server. I tested the link with this code:
var query = `
{
allFilms {
edges {
node {
id
}
}
}
}
`
fetch("http://localhost:50515/graphiql?query=" + query)
.then(response=>response.json())
.then(json=>console.log(json))
I got a response from the server, I saw some network action, it worked! I can communicate with the graphiql service.
Next, I created a query that was structured similar to the TeaStoreQuery. I tested it, and it returned the expected results.
query AllFilmQuery {
allFilms {
...filmListFragment
}
}
fragment filmListFragment on FilmsConnection {
edges {
node {
...filmFragment
}
}
}
fragment filmFragment on Film {
id
title
releaseDate
}
HOW DO YOU MAKE THIS WORK WITH RELAY??
I cannot figure out how to use Relay to query the server. Here is the code that I adapted from the Tea sample.
import { render } from 'react-dom'
import {
RootContainer,
createContainer,
Route,
injectNetworkLayer
} from 'react-relay'
// React component for each star wars film
const Film = ({ id, title, releaseDate }) =>
<li key={id}>
{title} (<em>{releaseDate}</em>)
</li>
// Relay container for each film
const FilmContainer = createContainer(Film, {
fragments: {
film: () => Relay.QL`
fragment on Film {
id,
title,
releaseDate
}
`
}
})
// React component for listing films
const FilmList = ({ films=[] }) =>
<ul>
{films.map(
film => <Film {...film} />
)}
</ul>
// Relay container for Listing all Films
const FilmListContainer = createContainer(FilmList, {
fragments: {
films: () => Relay.QL`
fragment on FilmsConnection {
edges {
node {
${ Film.getFragment('film') }
}
}
}
`
}
})
// The Home Route
class FilmHomeRoute extends Route {
static routeName = 'Home'
static queries = {
allFilms: (Component) => Relay.QL`
query AllFilmQuery {
allFilms {
${Component.getFragment('allFilms')}
}
}
`
}
}
// Is this how you setup a network layer
// I am using CORS, and I testing the graphql service with fetch
// The graphql service works but Relay never seems to try to connect
Relay.injectNetworkLayer(
new Relay.DefaultNetworkLayer('http://localhost:50515/graphiql')
)
render(
<RootContainer
Component={FilmListContainer}
route={new FilmHomeRoute()}
/>,
document.getElementById('react-container')
)
When I run this sample (source | output) I do not see any attempts at making network requests. I do see an error "Cannot render map of null". It seems like it cannot map the allfilms data.
What am I doing wrong?
According to section 5.1 of this document, the "Relay Object Identification Specification":
Relay‐compliant servers may expose root fields that are not plural identifying root fields; the Relay client will just be unable to use those fields as root fields in its queries.
Based on this specification, Relay can not query plural fields at the root of a query unless the field takes a list of arguments that exactly maps to the results. That means the allFilms field cannot be used with Relay. You can read more about the limitations in this other StackOverflow answer: How to get RelayJS to understand that a response from GraphQL is an array of items, not just a single item
If you would like to have a GraphQL schema with root fields that return arrays, you might want to use a different GraphQL client, since Relay is particularly restrictive in what kinds of schemas it works with. graphql.org has a list: http://graphql.org/code/#graphql-clients
I don't use Relay container, because I'd like to have more control over components. Instead of it I use HOC + Relay.Store.forceFetch, that fetches any given query with variables. So I have the following query:
query {
root {
search(filter: $filter) {
selectors {
_id,
data {
title,
status
}
},
selectorGroups {
_id,
data {
title,
}
}
}
}
}
Then I have to do some mutation on selector type.
export default class ChangeStatusMutation extends Relay.Mutation {
getMutation() {
return Relay.QL`mutation {selectors_status_mutation}`;
}
getVariables() {
return {
id: this.props.id,
status: this.props.status
};
}
getFatQuery() {
return Relay.QL`
fragment on selectors_status_mutationPayload{
result {
data {
status
}
}
}
`;
}
static fragments = {
result: () => Relay.QL`
fragment on selector {
_id,
data {
title,
status
}
}`,
};
getOptimisticResponse() {
return {
result: {
_id: this.props.id,
data: {
status: this.props.status
}
}
};
}
getConfigs() {
return [{
type: 'FIELDS_CHANGE',
fieldIDs: {
result: this.props.id
},
}];
}
}
Call mutation in component:
const mutation = new ChangeStatusMutation({id, status, result: selector});
Relay.Store.commitUpdate(mutation);
After mutation commitment selector in Relay storage is not changed. I guess that's because of empty Tracked Fragment Query and mutation performs without any fields:
ChangeStatusMutation($input_0:selectors_statusInput!) {
selectors_status_mutation(input:$input_0) {
clientMutationId
}
}
But the modifying selector was already fetched by Relay, and I pass it to the mutation with props. So Relay knows the type, that should be changed, how to find the item and which fields should be replaced. But can not intersect. What's wrong?
So, you're definitely a bit "off the ranch" here by avoiding Relay container, but I think this should still work...
Relay performs the query intersection by looking up the node indicated by your FIELDS_CHANGE config. In this case, your fieldIDs points it at the result node with ID this.props.id.
Are you sure you have a node with that ID in your store? I'm noticing that in your forceFetch query you fetch some kind of alternative _id but not actually fetching id. Relay requires an id field to be present on anything that you later want to refetch or use the declarative mutation API on...
I'd start by checking the query you're sending to fetch whatever this result type is. I don't see you fetching that anywhere in your question description, so I'm just assuming that maybe you aren't fetching that right now?
How to write outputFields, getFatQuery, getConfigs for create new item and update items list
Please take a look gist or live
Questions are
getFatQuery() {
return Relay.QL`
???
`;
}
getConfigs() {
return [???];
}
outputFields: {
???
},
The outputFields in your schema make up the GraphQL type CreateActivityPayload that will be generated from your schema.js file. A mutation is like a regular query, but with side effects. In outputFields you get to decide what's queryable. Since your store is the only thing in your app that can change as a result of this mutation, we can start with that.
outputFields: {
store: {
type: storeType,
resolve: () => store,
},
}
The fat query operates on these output fields. Here you tell Relay what could possibly change as a result of this mutation. Adding an activity could change the following fields:
getFatQuery() {
return Relay.QL`
fragment on CreateActivityPayload #relay(pattern: true) {
store {
activities
}
}
`;
}
Finally, the config tells Relay what to do with the query when it gets it, or even if it needs to be made at all. Here, you're looking to update a field after creating a new activity. Use the FIELDS_CHANGE config to tell Relay to update your store.
getConfigs() {
return [{
type: 'FIELDS_CHANGE',
fieldsIDs: {
store: this.props.storeId,
},
}];
}
See more: https://facebook.github.io/relay/docs/guides-mutations.html