store.getRootField(...) returns null - reactjs

I call an api to add a new request:
import { commitMutation, graphql } from "react-relay";
import { REQUEST_STATUS_NEW } from "../../../constants";
const mutation = graphql`
mutation CreateRequestMutation($input: CreateRequestInput!) {
createRequest(input: $input) {
request {
id
tid
title
description
price
commission
value
expirationDate
createdAt
completionDate
multipleResponders
draft
status
requestProposals
type {
id
name
}
industry {
id
name
}
applications {
id
}
myApplication {
id
}
}
}
}
`;
let tempId = 0;
function sharedUpdater(store, request) {
const root = store.getRoot();
const newRequests = root
.getLinkedRecords("requests", { own: true })
.filter(r => r);
if (!newRequests.find(m => m.getValue("id") === request.getValue("id"))) {
newRequests.push(request);
}
root.setLinkedRecords(newRequests, "requests", { own: true });
}
export const commit = (environment, input) => {
tempId += 1;
return commitMutation(environment, {
mutation,
variables: { input },
updater: store => {
const payload = store.getRootField("createRequest");
console.log('payload: ', payload)
const request = payload.getLinkedRecord("request");
sharedUpdater(store, request);
}
});
};
But each time I call it, store.getRootField return me null. I cant really understand where is the problem where I can investigate further. Any help appreciated. Seems like server doesnt have any issues at their side. How can I debug this?

Related

How to implement Authorization with Custom Directives in apollo with graphql-tools/utils?

I know that Apollo 2 allowed custom directives by extending the class "SchemaDirectiveVisitor." However, I am using apollo 3 and I know that the way to achieve this now is by using graphql-tools/utils and graphql-tools/schema.
In my index.js I have the following code:
const serverServer = async () => {
app.use(AuthMiddleware);
app.use(
cors({
origin: 'mydomain',
})
);
let schema = makeExecutableSchema({
typeDefs: [typeDefsLibrary, typeDefsDynamicContent, userTypeDefs],
resolvers: {
Query,
Mutation,
Article,
Blog,
Podcast,
SermonNotes,
Sermon,
// dynamic Content
Friday,
Thursday,
// Post Content
Commentary,
Quote,
Thought,
UserContent_SermonNotes,
// User Content
User,
All_Posts,
},
});
schema = AuthorizationDirective(schema, 'auth');
const apolloServer = new ApolloServer({
schema,
context: ({ req }) => {
const { isAuth, user } = req;
return {
req,
isAuth,
user,
};
},
});
await apolloServer.start();
apolloServer.applyMiddleware({ app: app, path: '/api' });
app.listen(process.env.PORT, () => {
console.log(`listening on port 4000`);
});
};
serverServer();
then on my schema file I have:
directive #auth(requires: [RoleName] ) on OBJECT | FIELD_DEFINITION
enum RoleName {
SUPERADMIN
ADMIN
}
type Commentary #auth(requires: [SUPERADMIN, ADMIN]) {
ID: ID
USER_ID: ID
VERSE_ID: String
body: String
category_tags: String
referenced_verses: String
verse_citation: String
created_date: String
posted_on: String
creator(avatarOnly: Boolean): User
comments(showComment: Boolean): [Commentary_Comment]
approvals: [Commentary_Approval]
total_count: Int
}
and this is my custom directive code:
const { mapSchema, getDirective, MapperKind } = require('#graphql-tools/utils');
const { defaultFieldResolver } = require('graphql');
const { ApolloError } = require('apollo-server-express');
//const { logging } = require('../../helpers');
module.exports.AuthorizationDirective = (schema, directiveName) => {
return mapSchema(schema, {
[MapperKind.FIELD]: (fieldConfig, _fieldName, typeName) => {
const authDirective = getDirective(schema, fieldConfig, directiveName);
console.log('auth Directive line 10: ', authDirective);
if (authDirective && authDirective.length) {
const requiredRoles = authDirective[0].requires;
if (requiredRoles && requiredRoles.length) {
const { resolve = defaultFieldResolver } = fieldConfig;
fieldConfig.resolve = function (source, args, context, info) {
if (requiredRoles.includes('PUBLIC')) {
console.log(
`==> ${context.code || 'ANONYMOUS'} ACCESSING PUBLIC RESOLVER: ${
info.fieldName
}`
);
//logging(context, info.fieldName, args);
return resolve(source, args, context, info);
}
if (!requiredRoles.includes(context.code)) {
throw new ApolloError('NOT AUTHORIZED', 'NO_AUTH');
}
console.log(`==> ${context.code} ACCESSING PRIVATE RESOLVER: ${info.fieldName}`);
//logging(context, info.fieldName, args);
return resolve(source, args, context, info);
};
return fieldConfig;
}
}
},
});
};
But is not working. It seems like it is not even calling the Custom Directive. As you see I have a "console.log('auth Directive line 10: ', authDirective);" on my schema directive function that return "undefined."
I know this post is so ling but I hope someone can help!
Thanks in advance!
Below is the code worked for me
I have used [MapperKind.OBJECT_FIELD]: not [MapperKind.FIELD]:
I have referred this from #graphql-tools ->
https://www.graphql-tools.com/docs/schema-directives#enforcing-access-permissions
`
const { mapSchema, getDirective, MapperKind } = require('#graphql-tools/utils');
const { defaultFieldResolver } = require('graphql');
const HasRoleDirective = (schema, directiveName) => {
return mapSchema(schema, {
// Executes once for each object field in the schems
[MapperKind.OBJECT_FIELD]: (fieldConfig, _fieldName, typeName) => {
// Check whether this field has the specified directive
const authDirective = getDirective(schema, fieldConfig, directiveName);
if (authDirective && authDirective.length) {
const requiredRoles = authDirective[0].requires;
// console.log("requiredRoles: ", requiredRoles);
if (requiredRoles && requiredRoles.length) {
// Get this field's original resolver
const { resolve = defaultFieldResolver } = fieldConfig;
// Replace the original resolver with function that "first" calls
fieldConfig.resolve = function (source, args, context, info) {
// console.log("Context Directive: ", context);
const { currentUser } = context;
if(!currentUser) throw new Error("Not Authenticated");
const { type } = currentUser['userInfo']
const isAuthorized = hasRole(type, requiredRoles);
if(!isAuthorized) throw new Error("You Have Not Enough Permissions!")
//logging(context, info.fieldName, args);
return resolve(source, args, context, info);
};
return fieldConfig;
}
}
}
})
}
`

How to wait for the end of an action with useDispatch to move on?

I currently have a real problem. I want to redirect my user to the right conversation or publication when they press a notification.
All the code works, but I have the same problem all the time: the redirection happens before the action is completed, which results in a nice error telling me that the item is "null".
If I redirect to a publication with a new comment, it shows the publication, but the comments load one or two seconds after being redirected.
How is it possible to wait for the end of an action before redirecting?
Thanks a lot
My action (with Redux Thunk)
export const fetchPublications = token => {
return async dispatch => {
await axios
.get(`/articles?token=${token}`)
.then(response => {
const articles = response.data.articles;
const groups = response.data.groups;
const groupPosts = response.data.groupPosts;
const comments = response.data.comments;
const loadedArticles = [];
const loadedGroups = [];
const loadedGroupPosts = [];
const loadedComments = [];
for (const key in articles) {
loadedArticles.push(
new Article(
articles[key].id,
articles[key].title,
articles[key].content,
articles[key].description,
articles[key].cover,
articles[key].dateCreation,
articles[key].creatorPhoto,
articles[key].creatorFirstName,
articles[key].creatorLastName,
articles[key].creatorId,
articles[key].slug,
articles[key].isOnline,
articles[key].isForPro,
'article',
),
);
}
for (const key in groups) {
loadedGroups.push(
new Group(
groups[key].id,
groups[key].name,
groups[key].icon,
groups[key].cover,
groups[key].description,
groups[key].isPublic,
groups[key].isOnInvitation,
groups[key].dateCreation,
groups[key].slug,
groups[key].safeMode,
groups[key].isOnTeam,
groups[key].role,
groups[key].isWaitingValidation,
'group',
),
);
}
for (const key in groupPosts) {
loadedGroupPosts.push(
new GroupPost(
groupPosts[key].id,
groupPosts[key].content,
groupPosts[key].dateCreation,
groupPosts[key].lastModification,
groupPosts[key].creatorPhoto,
groupPosts[key].creatorFirstName,
groupPosts[key].creatorLastName,
groupPosts[key].creatorId,
groupPosts[key].onGroupId,
groupPosts[key].groupName,
groupPosts[key].groupIcon,
'groupPost',
groupPosts[key].liked,
groupPosts[key].likesCounter,
groupPosts[key].commentsCounter,
),
);
}
for (const key in comments) {
loadedComments.push(
new Comment(
comments[key].id,
comments[key].content,
comments[key].dateCreation,
comments[key].lastModification,
comments[key].creatorPhoto,
comments[key].creatorFirstName,
comments[key].creatorLastName,
comments[key].creatorId,
comments[key].onPostId,
),
);
}
dispatch({
type: FETCH_PUBLICATIONS,
articles: loadedArticles,
groups: loadedGroups,
groupPosts: loadedGroupPosts,
comments: loadedComments,
});
})
.catch(error => {
console.log(error);
throw new Error('Une erreur est survenue.');
});
};
};
My notification handler
const handleNotificationResponse = async response => {
if (response.actionIdentifier === 'expo.modules.notifications.actions.DEFAULT') {
try {
if (response.notification.request.content.data.discussionId) {
if (isAuth) {
const discussionId =
response.notification.request.content.data.discussionId;
dispatch(messengerActions.fetchMessenger(userToken));
const item = messages.filter(
message => message.id == discussionId,
);
navigationRef.current?.navigate('MessengerApp', {
screen: 'Discussion',
params: { item: item[0] },
});
}
} else if (response.notification.request.content.data.groupPostId) {
if (isAuth) {
const groupPostId =
response.notification.request.content.data.groupPostId;
dispatch(newsfeedActions.fetchPublications(userToken));
const item = groupPosts.filter(
groupPost => groupPost.id == groupPostId,
);
navigationRef.current?.navigate('App', {
screen: 'Comments',
params: {
item: item[0],
},
});
}
}
} catch (err) {}
} else {
}
};

Apollo Client delete Item from cache

Hy I'm using the Apollo Client with React. I query the posts with many different variables. So I have one post in different "caches". Now I want to delete a post. So I need to delete this specific post from all "caches".
const client = new ApolloClient({
link: errorLink.concat(authLink.concat(httpLink)),
cache: new InMemoryCache()
});
Postquery:
export const POSTS = gql`
query posts(
$after: String
$orderBy: PostOrderByInput
$tags: JSONObject
$search: String
$orderByTime: Int
) {
posts(
after: $after
orderBy: $orderBy
tags: $tags
search: $search
orderByTime: $orderByTime
) {
id
title
...
}
}
`;
I tried it with the cache.modify(), which is undefined in my mutation([https://www.apollographql.com/docs/react/caching/cache-interaction/#cachemodify][1])
const [deletePost] = useMutation(DELETE_POST, {
onError: (er) => {
console.log(er);
},
update(cache, data) {
console.log(cache.modify())//UNDEFINED!!!
cache.modify({
id: cache.identify(thread), //identify is UNDEFINED + what is thread
fields: {
posts(existingPosts = []) {
return existingPosts.filter(
postRef => idToRemove !== readField('id', postRef)
);
}
}
})
}
});
I also used the useApolloClient() with the same result.
THX for any help.
Instead of using cache.modify you can use cache.evict, which makes the code much shorter:
deletePost({
variables: { id },
update(cache) {
const normalizedId = cache.identify({ id, __typename: 'Post' });
cache.evict({ id: normalizedId });
cache.gc();
}
});
this option worked for me
const GET_TASKS = gql`
query tasks($listID: String!) {
tasks(listID: $listID) {
_id
title
sort
}
}
`;
const REMOVE_TASK = gql`
mutation removeTask($_id: String) {
removeTask(_id: $_id) {
_id
}
}
`;
const Tasks = () => {
const { loading, error, data } = useQuery(GET_TASKS, {
variables: { listID: '1' },
});
сonst [removeTask] = useMutation(REMOVE_TASK);
const handleRemoveItem = _id => {
removeTask({
variables: { _id },
update(cache) {
cache.modify({
fields: {
tasks(existingTaskRefs, { readField }) {
return existingTaskRefs.filter(
taskRef => _id !== readField('_id', taskRef),
);
},
},
});
},
});
};
return (...);
};
You can pass your updater to the useMutation or to the deletePost. It should be easier with deletePost since it probably knows what it tries to delete:
deletePost({
variables: { idToRemove },
update(cache) {
cache.modify({
fields: {
posts(existingPosts = []) {
return existingPosts.filter(
postRef => idToRemove !== readField('id', postRef)
);
},
},
});
},
});
You should change variables to match your mutation. This should work since posts is at top level of your query. With deeper fields you'll need a way to get the id of the parent object. readQuery or a chain of readField from the top might help you with that.

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
}
}
}

graphql: how to handle prev page, next page, last page and first page properly?

For frontend I am using React + Relay. I have some connection at the backend that could be queried like:
query Query {
node(id: 123456) {
teams(first: 10) {
node {
id
name
}
page_info {
start_cursor
end_cursor
}
}
}
}
So for the traditional approach, I can use skip PAGE_SIZE * curr limit PAGE_SIZE to query for next page, prev page and first page and last page (In fact I can query for random page)
But how should I implement the frontend to make these requests elegantly?
#Junchao, what Vincent said is correct. Also, you must have a re-fetch query and send refetchVariables with your first value updated. I will try to provide you an example:
export default createRefetchContainer(
TeamsComponent,
{
query: graphql`
fragment TeamsComponent_query on Query
#argumentDefinitions(
first: { type: Int }
last: { type: Int }
before: { type: String }
after: { type: String }
) {
teams(
id: { type: "ID!" }
first: { type: Int }
last: { type: Int }
before: { type: String }
after: { type: String }
) #connection(key: "TeamsComponent_teams", filters: []) {
count
pageInfo {
endCursor
hasNextPage
}
edges {
node {
id
name
}
}
}
}
`,
},
graphql`
query TeamsComponent(
$after: String
$before: String
$first: Int
$last: Int
) {
...TeamsComponent_query
#arguments(
first: $first
last: $last
after: $after
before: $before
)
}
`,
);
I tried to build an example based on your code. This is basically the idea. The bottom query is the re-fetch one. Alongside with that, you must trigger this re-fetch somehow by calling this.props.relay.refetch passing your renderVaribles. Take a deep looker into the docs about this.
Hope is helps :)
UPDATE:
Just to add something, you could have a handleLoadMore function with something like this:
handleLoadMore = () => {
const { relay, connection } = this.props;
const { isFetching } = this.state;
if (!connection) return;
const { edges, pageInfo } = connection;
if (!pageInfo.hasNextPage) return;
const total = edges.length + TOTAL_REFETCH_ITEMS;
const fragmentRenderVariables = this.getRenderVariables() || {};
const renderVariables = { first: total, ...fragmentRenderVariables };
if (isFetching) {
// do not loadMore if it is still loading more or searching
return;
}
this.setState({
isFetching: true,
});
const refetchVariables = fragmentVariables => ({
first: TOTAL_REFETCH_ITEMS,
after: pageInfo.endCursor,
});
relay.refetch(
refetchVariables,
null,
() => {
this.setState({ isFetching: false });
},
{
force: false,
},
);
};
UPDATE 2:
For going backwards, you could have something like:
loadPageBackwardsVars = () => {
const { connection, quantityPerPage } = this.props;
const { quantity } = getFormatedQuery(location);
const { endCursorOffset, startCursorOffset } = connection;
const firstItem = connection.edges.slice(startCursorOffset, endCursorOffset)[0].cursor;
const refetchVariables = fragmentVariables => ({
...fragmentVariables,
...this.getFragmentVariables(),
last: parseInt(quantity || quantityPerPage, 10) || 10,
first: null,
before: firstItem,
});
return refetchVariables;
};

Resources