Apollo client specific updatequery for all items? - reactjs

I have two models with the following relation:
Groups has many
Links
So basically my initial query is getAllGroups, which fetches all groups and its links.
I have a mutation which simply creates a link, what is the most efficient way to update my UI?
Currently I have:
export const withCreateLink = graphql(createLink, {
props({ownProps, mutate}) {
return {
createLink(url, description, group) {
return mutate({
variables: {
url,
description,
group
},
updateQueries: {
getAllGroups: (prev, {mutationResult}) => {
const newLink = mutationResult.data.createLink;
return update(prev, {
allGroups: {
links: {
$push: [newLink]
}
}
})
}
}
})
}
}
}
});
Basically I only want to fetch the group which has the link added to it, but I need to specify which group that is. How do I go about this?

I already figured it out, while creating other mutations this way i've found some different approaches to tackle this:
Option 1:
If you have access to the index of the current model that's in your cache, you can pass it as a prop and then use it inside of your query to target the one that it has been added to:
updateQueries: {
getAllGroups: (prev, {mutationResult}) => {
const newLink = mutationResult.data.createLink;
return update(prev, {
allGroups: {
[groupIndex]:
links: {
$push: [newLink]
}
}
})
}
}
Option 2: Using update instead of updateQueries, so you can read the result of the root query, map over it and then append the newly inserted data. After this you can write it to the cache.
update: (proxy, mutationResult) => {
const query = getAllGroups;
const data = proxy.readQuery({query});
data.allGroups.map((groupData) => {
if(groupData.id == group)
groupData.links.push(mutationResult.data.createLink);
});
proxy.writeQuery({
query,
data
})
}

Related

Is there a way to check nested resources in react-admin's matchSuggestion?

The goal is to make an AutocompleteInput check for the filter value not only in the suggestion list directly, but also in the suggestions' references to different resources.
Specifically, say a Quote has a reference to a Contact and to an Address, and the user enters 'abc' in the input. Now, a Quote whose address contains 'abc' should also be displayed in the suggestion list.
The most elegant way would be to use the useGetOne hook like in the following code snippet but you can't call that hook from outside a React component.
const matchAnyNested = (filter, value) => {
if (matchAnyField(filter, value)) return true;
const { data: contact } = useGetOne('contacts', value.contact_id);
if (matchAnyField(filter, contact)) return true;
const { data: account } = useGetOne('accounts', contact.account_id);
if (matchAnyField(filter, account)) return true;
for (let item of value.part_items) {
const part = useGetOne('parts', item.part_id);
if (matchAnyField(filter, part)) return true;
}
return false;
};
[...]
<AutocompleteInput ... matchSuggestion={matchAnyNested} />
Is there a way to fetch records from within the matchSuggestion function or some other way to validate suggestions based on nested records ? Thanks for any help
Because of the React rules of hooks, this doesn't seem to be possible. I ended up implementing this filtering functionality in the backend.
The useGetOne hook, just like other dataProvider hooks, accepts an enabled option. The example from the react-admin documentation shows its usage:
// fetch posts
const { ids, data: posts, loading: isLoading } = useGetList(
'posts',
{ page: 1, perPage: 20 },
{ field: 'name', order: 'ASC' },
{}
);
// then fetch categories for these posts
const { data: categories, loading: isLoadingCategories } = useGetMany(
'categories',
ids.map(id=> posts[id].category_id),
// run only if the first query returns non-empty result
{ enabled: ids.length > 0 }
);
It applies to your case:
const matchAnyNested = (filter, value) => {
const { data: contact } = useGetOne(
'contacts',
value.contact_id,
{ enabled: !matchAnyField(filter, value) }
);
const { data: account } = useGetOne(
'accounts',
contact.account_id,
{ enabled: !matchAnyField(filter, contact) }
);
// ...
};
This won't solve your problem in the loop, though, because of the rules of hooks.
If you do need that loop, your best bet is to use the useDataProvider hook to call the dataProvider directly:
const matchAnyNested = async (filter, value) => {
const dataProvider = useDataProvider();
if (matchAnyField(filter, value)) return true;
const { data: contact } = await dataProvider.getOne('contacts', { id: value.contact_id });
if (matchAnyField(filter, contact)) return true;
const { data: account } = await dataProvider.getOne('accounts', { id: contact.account_id });
if (matchAnyField(filter, account)) return true;
for (let item of value.part_items) {
const part = await dataProvider.getOne('parts', { id: item.part_id });
if (matchAnyField(filter, part)) return true;
}
return false;
};

Cache data may be lost when replacing the my field of a Query object

this my code
const NewVerificationCode = () => {
const { loading, error, data = {}, refetch } = useQuery(CONFIRMATION_CODE, {
skip: true,
onError: (err) => {},
});
console.log(loading, error, data);
if (loading || error) {
return <ErrorLoadingHandler {...{ loading, error }} />;
}
return (
<form
onSubmit={(e) => {
refetch();
e.preventDefault();
}}
>
<div>
<button type="submit" className="signUpbutton">
{"Send the message again"}
</button>
</div>
</form>
);
};
const CONFIRMATION_CODE = gql`
query {
my {
sendNewTokenForConfirmation
}
}
`;
when i make a request i get a warning
Cache data may be lost when replacing the my field of a Query object.
To address this problem (which is not a bug in Apollo Client), either ensure all >objects of type My have IDs, or define a custom merge function for the Query.my >field, so InMemoryCache can safely merge these objects
existing:
{"__typename":"My","getUser{"__typename":"User","email":"shakizriker0022#gmail.com"}}
incoming: {"__typename":"My","sendNewTokenForConfirmation":"SUCCESS"}
For more information about these options, please refer to the documentation:
I followed the links.
I read the documentation and realized that the problem is in the apollo client cache (typePolicies).
But how should I solve this problem I just can't figure out.
What should i write in typePolicies to get rid of the warning ?.
You may need to return an id for Apollo to uniquely identify that object in the cache.
I think this issue is similar to yours:
link
const CONFIRMATION_CODE = gql`
query {
my {
id
sendNewTokenForConfirmation
}
}
`;
Every object should return id, _id or alternatively a custom id field (https://www.apollographql.com/docs/react/caching/cache-configuration) for automatic merges to function.
const cache = new InMemoryCache({
typePolicies: {
Product: {
keyFields: ["custom-id-field"],
},
},
});
(!)People often forget that cache operations require the same variables used for the initial query (!)
Mutation Sample (adding a purchase):
update(cache, { data: NewPurchase } }) => {
let productCache = cache.readQuery({
query: productQuery,
variables: {
id: productId
}
})
cache.writeQuery({
query: productQuery,
variables: { id: productId },
data: {
Product: {
...productCache.Product,
purchases: productCache.Product.purchases.concat(NewPurchase)
}
}
});
};
}
(Important: Each Purchase also requires it's own individual id that needs to be returned)

Why does react Apollo on update subscription returns the already updated value as prev?

I'm using apollo for a react project and with subscription on the creation, update and deletion of object. I use the subscribeToMore functionality to start the subscriptions. The prev value of the updateQuery callback is the correct value for the creation and deletion subscription. But for the update subscription the prev value already contains the updated value. Overall this is really nice, as I don't need to add my custom implementation on how to update the object and can just return prev, but I don't understand why this happens. From my understanding the previous value should be returned. Is this just backed in functionality of apollo or is this some weird bug?
Here is the component which implements the subscriptions:
import { useEffect } from 'react';
import { useQuery } from '#apollo/react-hooks';
import * as Entities from './entities';
import * as Queries from './queries';
interface IAppointmentData {
appointments: Entities.IAppointment[];
error: boolean;
loading: boolean;
}
function getAppointmentsFromData(data) {
return (data && data.appointments) || [];
}
export function useAllAppointments(): IAppointmentData {
const initialResult = useQuery(Queries.GET_APPOINTMENTS);
const { data, error, loading, subscribeToMore } = initialResult;
useEffect(() => {
const unsubscribeNewAppointments = subscribeToMore({
document: Queries.NEW_APPOINTMENTS_SUB,
variables: {},
updateQuery: (prev, { subscriptionData }) => {
if (!subscriptionData.data) {
return prev;
}
const { newAppointment } = subscriptionData.data;
return Object.assign({}, prev, {
appointments: [...prev.appointments, newAppointment],
});
},
});
const unsubscribeUpdateAppointment = subscribeToMore({
document: Queries.UPDATE_APPOINTMENTS_SUB,
variables: {},
updateQuery: (prev, { subscriptionData }) => {
return prev
},
});
const unsubscribeDeleteAppointments = subscribeToMore({
document: Queries.DELETE_APPOINTMENTS_SUB,
variables: {},
updateQuery: (prev, { subscriptionData }) => {
if (!subscriptionData.data) {
return prev;
}
const { deleteAppointment: {
id: deletedAppointmentId
} } = subscriptionData.data;
return Object.assign({}, prev, {
appointments: prev.appointments.filter(item => item.id !== deletedAppointmentId),
});
},
});
return function unsubscribe() {
unsubscribeNewAppointments()
unsubscribeDeleteAppointments()
unsubscribeUpdateAppointment()
}
}, [subscribeToMore]);
return {
appointments: getAppointmentsFromData(data),
error: !!error,
loading,
};
}
And these are my graphql queries / subscriptions:
import { gql } from 'apollo-boost';
export const GET_APPOINTMENTS = gql`
{
appointments {
id
responsibleCustomer {
id
user {
id
firstName
lastName
}
}
companions {
id
user {
id
firstName
lastName
}
}
time {
plannedStart
plannedEnd
}
type
}
}
`;
export const NEW_APPOINTMENTS_SUB = gql`
subscription newAppointment {
newAppointment {
id
responsibleCustomer {
id
user {
id
firstName
lastName
}
}
companions {
id
user {
id
firstName
lastName
}
}
time {
plannedStart
plannedEnd
}
type
}
}
`;
export const UPDATE_APPOINTMENTS_SUB = gql`
subscription updateAppointment {
updateAppointment {
id
responsibleCustomer {
id
user {
id
firstName
lastName
}
}
companions {
id
user {
id
firstName
lastName
}
}
time {
plannedStart
plannedEnd
}
type
}
}
`;
export const DELETE_APPOINTMENTS_SUB = gql`
subscription deleteAppointment {
deleteAppointment {
id
}
}
`;
According to the official Apollo documentation
Note that the updateQuery callback must return an object of the same
shape as the initial query data, otherwise the new data won't be
merged.
After playing around for a bit it seems that if the id you are returning is the same as in your cache and the object has the same shape, the update happens automatically.
But if you actually follow the CommentsPage example in the link I provided you will see that the id that is returned is new, that is why they explicity assign the object with the new data.
if (!subscriptionData.data) return prev;
const newFeedItem = subscriptionData.data.commentAdded;
return Object.assign({}, prev, {
entry: {
comments: [newFeedItem, ...prev.entry.comments]
}
});
Ive tested this with my own message app Im working on, when receiving new messages with new id I have to return the merged data myself. But when im updating the chatRooms to display which chatRoom should be at the top of my screen (which one has the newest message), then my update happens automatically and I just return prev.
If however you want a work around to explicitly check the data before updating it you could try this workaround I found on GitHub. You will just need to use a reverse lookup id or just a different variable other than id.
This is what I would do in your example to achieve this:
updateAppointment {
// was id
idUpdateAppointment // im sure updateAppointmentId should also work... i think
responsibleCustomer {
id
user {
id
firstName
lastName
}
}
}

Apollo update after a mutation isn't triggering a rerender

I am having troubles with a mutation in graphQL apollo. When a page loads, it will run a query lectureResponseQuery and if the query == null a mutation fetchLectureResponseMutation is run to create a new document. This mutation returns the new result and I do an update to the query and I expect that the component will re-render with the new data, but it doesn't. Does anyone know why that is? Thanks!
#graphql(fetchLectureResponseMutation, {
options: ownProps => ({
variables: { lectureName: ownProps.match.params.lectureName },
update: (proxy, { data: { fetchLectureResponse } }) => {
const data = proxy.readQuery({
query: lectureResponseQuery,
variables: { lectureName: ownProps.match.params.lectureName },
});
data.lectureResponse = fetchLectureResponse;
proxy.writeQuery({
query: lectureResponseQuery,
data,
});
},
}),
name: 'fetchLectureResponse',
})
#graphql(lectureResponseQuery, {
options: ownProps => ({
variables: { lectureName: ownProps.match.params.lectureName },
}),
})
class LecturePage extends React.PureComponent {
componentWillUpdate(nextProps) {
if (nextProps.data.lectureResponse === null) {
this.props.fetchLectureResponse();
}
}
render() {
const { data } = this.props;
if (data.loading || data.lectureResponse === null) {
return <Loading />;
}
return <LectureLayout lectureResponse={data.lectureResponse} />
}
}
For anyone looking into this issue in the future- the central issue is that I wanted to do a find OR create operation. This works much better when the query just returns the new object if it doesn't exist because then you only make 1 backend call which means that you don't have to synchronize the timings between a query and a mutation.
TLDR: Use a query for a findOrCreate operation!

Fragment cannot be spread here as objects of type "X" can never be of type "Y"

In this case, type "X" is Application and type "Y" is type "Node" - I can see why this is happening, but my understanding of Relay isn't enough to understand how to fix it. The query generated by Relay is
query {
node(id: $some_id) {
...F0
}
}
fragment F0 on Application {
...
}
I have a schema that looks like
query {
application {
/* kind of a generic endpoint for fetching lists, etc */
invites(token: $token) {
name
}
}
viewer { /* the current user */ }
}
I'm trying to fetch a specific invite from outside a session (viewer is null).
I've tried
const application = Relay.QL`query { application }`
...
<Route ... queries={{ application }}/>
...
Relay.createContainer(Component, {
initialValues: { token: null },
fragments: {
application: () => {
fragment on Application {
invites(token: $token) {
...
}
}
}
}
})
which gives me the error
fragment "F0" cannot be spread here as objects of type "Node" can never be of type "Application" - or something to that effect.
I'm a little confused, because if I were to write a raw query and run it through GraphQL directly
query {
application {
invites(token: "asdasdasd") {
edges {
node {
name
}
}
}
}
}
it gives me what I'm looking for...
In the backend, my graph is defined like
export const Application = new GraphQLObjectType({
name: 'Application',
fields: () => ({
id: {
type: GraphQLString,
resolve: () => 'APPLICATION_ID'
},
invites: {
type: InviteConnectionType,
args: connectionArgs,
resolve: (application, args) => {
...
}
}
})
})
export default new GraphQLSchema({
query: new GraphQLObjectType({
name: 'query',
fields: {
node: nodeField,
application: {
type: Application,
resolve: (root, args, ctx) => {
return Promise.resolve({})
}
}
}
})
I've been looking at questions like this and some issues on the Relay github, but it's not clear to me how I should implement nodeInterface.
edit: the short-long of the current nodeInterface code is
export const {
nodeInterface,
nodeField
} = nodeDefinitions(
(globalId) => {
const { type, id } = fromGlobalId(globalId)
return db[type].findById(id)
},
(obj) => {
const name = obj.$modelOptions.name.singular
return types[name]
}
)
Application is not a db model, however, just a generic interface to fetch data through. I've tried checking to see if type === 'Application', and returning null (although I see why that doesn't work), returning Application (the GraphQLObject), but that doesn't work... not really sure where to go from there.
You need to automatically generate an unique global id for a GraphQL
type that you want to refetch.
In nodeInterface you tell GraphQL
how to map the id to the corresponding GraphQL object.
By the given server-side object nodeInterface identifies the GraphQL type.
Below is simplified example how it may look like with Application:
// nodeInterface.
var {nodeInterface, nodeField} = nodeDefinitions(
(globalId) => {
var {type, id} = fromGlobalId(globalId);
// The mapping from globalId to actual object id and type.
console.log('globalId:', id);
console.log('type:', type);
if (type === 'Application') {
// getApplication is your db method to retrieve Application object.
// With id you could also retrieve a specific db object.
return getApplication();
} else {
return null;
}
},
(obj) => {
// Note that instanceof does an identity check on the prototype object, so it can be easily fooled.
if (obj instanceof Application) {
return ApplicationType;
} else {
return null;
}
},
);
// Application.
export const ApplicationType = new GraphQLObjectType({
name: 'Application',
fields: () => ({
// Auto-generated, globally defined id.
id: globalIdField('Application'),
_id: {
type: GraphQLString,
resolve: () => 'APPLICATION_ID'
},
invites: {
type: InviteConnectionType,
args: connectionArgs,
resolve: (application, args) => {
...
}
}
}),
// Declaring nodeInterface.
interfaces: [nodeInterface]
});
Note that during the initial fetch nodeInterface is not even executed, so if nodeInterface is returning nothing there won’t be errors at the initial fetch. If that doesn’t make sense or you’re still struggling you can post a link to the repo, I’ll look into it.
To give an update on this, I was on the right path.
The current nodeDefinitions I had just needed a little extra:
nodeDefinitions(
(globalId) => {
const { type, id } = fromGlobalId(globalId)
if (type === 'Application') {
return Promise.resolve(Application)
}
return db[type].findById(id)
},
(obj) => {
if (obj.$modelOptions) {
/* sequelize object */
const name = obj.$modelOptions.name.singular
return types[name]
} else if (obj.name === 'Application') {
return Application
}
return null
}
)
I'm not sure if this is the best way to do it, but it seems to do the trick. The gist is that, if the type of node that I want to be returned is Application, I return the GraphQL object - { ... name: "Application" ... } we'll use the name field from this in the next step (the second callback in nodeDefinitions) to just re-return Application. I think you could return a "custom" object or something else - it doesn't matter so long as you return something unique that you can define a mapping from to a GraphQLObject type (required for the second callback).

Resources