My query is failing in relay and I don't know why? - reactjs

I have this simple query which works fine in my Graphql but I cannot pass data using relay to components and I don't know why :(
{
todolist { // todolist returns array of objects of todo
id
text
done
}
}
this is my code in an attempt to pass data in components using relay:
class TodoList extends React.Component {
render() {
return <ul>
{this.props.todos.todolist.map((todo) => {
<Todo todo={todo} />
})}
</ul>;
}
}
export default Relay.createContainer(TodoList, {
fragments: {
todos: () => Relay.QL`
fragment on Query {
todolist {
id
text
done
}
}
`,
},
});
And lastly my schema
const Todo = new GraphQLObjectType({
name: 'Todo',
description: 'This contains list of todos which belong to its\' (Persons)users',
fields: () => {
return {
id: {
type: GraphQLInt,
resolve: (todo) => {
return todo.id;
}
},
text: {
type: GraphQLString,
resolve: (todo) => {
return todo.text;
}
},
done: {
type: GraphQLBoolean,
resolve: (todo) => {
return todo.done;
}
},
}
}
});
const Query = new GraphQLObjectType({
name: 'Query',
description: 'This is the root query',
fields: () => {
return {
todolist: {
type: new GraphQLList(Todo),
resolve: (root, args) => {
return Conn.models.todo.findAll({ where: args})
}
}
}
}
});
This code looks simple and I cannot see why this won't work and I have this error Uncaught TypeError: Cannot read property 'todolist' of undefined, but I configure todolist and I can query in my graphql, you can see the structure of the query is same, I don't know why this is not working?

todolist should be a connection type on Query. Also, your ids should be Relay global IDs. You will not have access to your objects' raw native id fields in Relay.
import {
connectionArgs,
connectionDefinitions,
globalIdField,
} from 'graphql-relay';
// I'm renaming Todo to TodoType
const TodoType = new GraphQLObjectType({
...,
fields: {
id: uidGlobalIdField('Todo'),
...
},
});
const {
connectionType: TodoConnection,
} = connectionDefinitions({ name: 'Todo', nodeType: TodoType });
// Also renaming Query to QueryType
const QueryType = new GraphQLObjectType({
...,
fields: {
id: globalIdField('Query', $queryId), // hard-code queryId if you only have one Query concept (Facebook thinks of this top level field as being a user, so the $queryId would be the user id in their world)
todos: { // Better than todoList; generally if it's plural in Relay it's assumed to be a connection or list
type: TodoConnection,
args: connectionArgs,
},
},
});
// Now, to be able to query off of QueryType
const viewerDefaultField = {
query: { // Normally this is called `viewer`, but `query` is ok (I think)
query: QueryType,
resolve: () => ({}),
description: 'The entry point into the graph',
}
};
export { viewerDefaultField };
The above is not fully complete (you'll likely also need to setup a node interface on one or more of your types, which will require node definitions), but it should answer your basic question and get you started.
It's a huge, huge pain to learn, but once you struggle through it it starts to make sense and you'll begin to love it over RESTful calls.

Related

Find element in an array of object with key array types

I would like to extract all active modules and actions with given roles.
getAvailableModulesAndActionsWithRoles(roles: string[]): IHubModule[] {
const modules = [...this.getModules()];
const activeModules = modules.filter(module => module.isEnabled);
activeModules.map(module => {
Object.keys(module.actions).map(action => {
roles.map(role => {
if (!module.actions[action]?.includes(role)) {
delete module.actions[action];
return;
}
});
});
});
return activeModules;
}
Here is how my module interface looks like
export interface IHubModule {
name: ModuleName;
isEnabled: boolean;
actions: Record<Topic, string[]>;
}
I passed an array of roles as an argument and I would like to extract all active actions for a given roles.
Here is how my test looks like
it('returns only active modules and actions for a list of roles', () => {
const moduleInfos: IHubModule[] = [
{
name: ModuleNameTest.TEST as any,
isEnabled: true,
actions: {
get: [BaseRole.STUDENT, BaseRole.EMPLOYEE],
post: [BaseRole.ADMIN, BaseRole.UNVERIFIED_GUEST],
update: [],
del: [BaseRole.ADMIN, BaseRole.UNVERIFIED_GUEST, BaseRole.STUDENT],
},
},
];
jest
.spyOn(hubService, 'getModules')
.mockImplementation(() => moduleInfos);
const results: IHubModule[] = [
{
name: ModuleNameTest.TEST as any,
isEnabled: true,
actions: {
post: [BaseRole.ADMIN, BaseRole.UNVERIFIED_GUEST],
del: [BaseRole.ADMIN, BaseRole.UNVERIFIED_GUEST, BaseRole.STUDENT],
},
},
{
name: ModuleNameTest.TEST as any,
isEnabled: true,
actions: {
get: [BaseRole.STUDENT, BaseRole.EMPLOYEE],
del: [BaseRole.ADMIN, BaseRole.UNVERIFIED_GUEST, BaseRole.STUDENT],
},
},
]
expect(
hubService.getAvailableModulesAndActionsWithRoles([
BaseRole.ADMIN,
BaseRole.STUDENT,
]),
).toStrictEqual(results);
});
This only work for one argument and I would like to get all module with available action for a given role. Is there a way to skip the action key instead of deleting it if the role is not found ? Because I needed on the next iteration.
I think that instead of modifying the original modules, you should create copies with the required content.
getAvailableModulesAndActionsWithRoles(roles: string[]): IHubModule[] {
const activeModules = this.getModules().filter(module => module.isEnabled);
// selecting modules corresponding to each action
return roles.flatMap(role => activeModules.map(
module => {
const actions = {};
Object.keys(module.actions)
.filter(key => Array.isArray(module.actions[key]) && module.actions[key].includes(role))
.forEach(key => {
actions[key] = module.actions[key];
});
return {...module, actions } as IHubModule;
}
));
//TODO We should also filter modules having empty actions
}

Apollo MockedProvider: “Failed to match x mocks for this query” where x is off by 1. Never sees the query I ask for

I’m experiencing the most bizarre Apollo behavior… in my tests, any time I issue a query for which I have a mock, Apollo magically can’t find it.
I have 5 mocks covering queries based on a collectionId and some pagination variables. When I have the component under test look up ID 834940, I get this:
Expected variables: {"page":1,"perPage":20,"collectionId":834940}
Failed to match 4 mocks for this query, which had the following variables:
{"collectionId":374276,"page":1,"perPage":20}
{"collectionId":112805,"page":1,"perPage":20}
{"collectionId":238350,"page":1,"perPage":20}
{"collectionId":-1,"page":1,"perPage":20}
Note that it says “4 mocks”. So it’s not seeing the mock I need, and the query fails. Make sense… until I change the component under test to look up ID 112805, which you can clearly see listed in the 4 mocks Apollo ostensibly knows about.
Expected variables: {"page":1,"perPage":20,"collectionId":112805}
Failed to match 4 mocks for this query, which had the following variables:
{"collectionId":374276,"page":1,"perPage":20}
{"collectionId":834940,"page":1,"perPage":20}
{"collectionId":238350,"page":1,"perPage":20}
{"collectionId":-1,"page":1,"perPage":20}
It can still only see 4 mocks, but the mocks it can see have changed. Now, all of a sudden, 112805 is missing in the mocks it sees. And now it CAN see the mock for 834940, the ID I queried for last time! So basically, whatever I ask for is the query it can’t resolve!!
Has anyone encountered this before?
Context
Collection.tsx:
A functional component with 3 different useQuery calls. Only the third one is failing:
import collectionQuery from './Collection.gql';
export type CollectionProps = {
className?: string;
collection: {
id: number;
name: string;
};
};
export function Collection( props: CollectionProps ) {
/* [useQuery #1] */
/* [useQuery #2] */
// useQuery #3
const {
data, error, loading, refetch,
} = useQuery<CollectionQuery, CollectionQueryVariables>( collectionQuery, {
skip: variables.collectionId === -1,
variables,
} );
return null;
}
queryMocks.ts:
import collectionQuery from './Collection.gql';
import { CollectionQuery_collection } from '../../../graphqlTypes/CollectionQuery';
const page = 1;
const perPage = 20;
export const firstCollection: CollectionQuery_collection = {
id: 374276,
name: 'First Collection',
};
export const secondCollection: CollectionQuery_collection = {
id: 834940,
name: 'Second Collection',
};
export const thirdCollection: CollectionQuery_collection = {
id: 112805,
name: 'Third Collection',
}
export const fourthCollection: CollectionQuery_collection = {
id: 238350,
name: 'Fourth Collection',
};
export const fifthCollection: CollectionQuery_collection = {
id: -1,
name 'Fifth Collection (Error)',
};
export const queryMocks: MockedResponse[] = [
{
request: {
query: collectionQuery,
variables: {
collectionId: firstCollection.id,
page,
perPage,
},
},
result: {
data: {
collection: firstCollection,
},
},
},
{
request: {
query: collectionQuery,
variables: {
collectionId: secondCollection.id,
page,
perPage,
},
},
result: {
data: {
collection: secondCollection,
},
},
},
{
request: {
query: collectionQuery,
variables: {
collectionId: thirdCollection.id,
page,
perPage,
},
},
result: {
data: {
collection: thirdCollection,
},
},
}, {
request: {
query: collectionQuery,
variables: {
collectionId: fourthCollection.id,
page,
perPage,
},
},
result: {
data: {
collection: fourthCollection,
},
},
}, {
request: {
query: collectionQuery,
variables: {
collectionId: fifthCollection.id,
page,
perPage,
},
},
result: {
data: {
collection: fifthCollection,
},
},
},
];
Collection.test.tsx:
import React from 'react';
import { MockedProvider } from '#apollo/client/testing';
import { MemoryRouter } from 'react-router';
import { secondCollection, queryMocks } from './queryMocks';
import { Collection } from './Collection';
it( 'Renders', () => {
render(
<MockedProvider mocks={ queryMocks } addTypename={ false }>
<MemoryRouter>
<Collection collection={ { id: secondCollection.id } } />
</MemoryRouter>
</MockedProvider>,
);
} );
The component is fetching collectionQuery twice, likely due to a state change. This could be from a useState set* call, or it could be from having multiple useQuerys. In the latter case, when one of the 3 queries resolves (with data changing from undefined to defined), it triggers a component re-render, which then calls the other query or queries again.
The reason why this breaks MockedProvider is because each mock can only be used to resolve a single query. So on the first matching API call, the mock is “spent”, reducing the queryMocks length from 5 to 4. Then, when the component re-renders and calls the same query again, Apollo can no longer find a matching mock. So to solve this, you have to either A.) refactor the component to only call the query once, or B.) add two of the same mock to the queryMocks array, like so:
queryMocks.ts:
const secondMock = {
request: {
query: collectionQuery,
variables: {
collectionId: secondCollection.id,
page,
perPage,
},
},
result: {
data: {
collection: secondCollection,
},
},
};
export queryMocks: MockedResponse[] = [
/* [mockOne] */
mockTwo,
mockTwo,
/* [mockThree, ..., mockFive] */
];

GraphQL: Adding subscription to schema

I am trying to setup subscriptions with GraphQL and Relay. I have the following mutation which adds a new team:
const createTeamMutation = mutationWithClientMutationId({
name: 'CreateTeam',
inputFields: {
ownerId: { type: new GraphQLNonNull(GraphQLInt) },
name: { type: new GraphQLNonNull(GraphQLString) },
},
outputFields: {
team: {
type: teamType,
resolve: (payload) => payload
}
},
mutateAndGetPayload: (team) => {
const { ownerId, name } = team;
// Using Sequalize ORM here..
return db.models.team.create(team)
.then.... etc
}
});
This works fine but I can't seem to figure out how to get my subscriptions working at least in GraphiQL. I have the following definition in my schema:
const GraphQLCreateTeamSubscription = subscriptionWithClientId({
name: 'CreateTeamSubscription',
outputFields: {
team: {
type: teamType,
resolve: (payload) => payload
}
},
subscribe: (input, context) => {
// What is meant to go here??
},
});
I am not sure how to build out the subscribe feature and can't seem to find enough documentation. When I run the following in GraphiQL,
subscription createFeedSubscription {
team {
id
ownerId
name
}
}
I get the following error:
Cannot query field team on type Subscription.
Thank you for all the help!

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).

Relay mutation on plain array is not working

I am having a hard time figuring out how to do mutations on plain array via relay.
I am trying to add a new tag to a post.
It does not get updated on client-side after being successfully added on the server-side.
I have to manually reload to see the new tag.
I have tried both REQUIRED_CHILDREN and this.props.relay.forceFetch(), but to no avail.
Also, tried FIELDS_CHANGE for post.
GraphQL Schema:
Post {
id: ID!
text: String!
tags: [Tag!]!
}
Tag {
id: ID!
name: String!
}
AddTagToPostMutation:
static fragments = {
post: () => Relay.QL`
fragment on Post {
id
tags
}
`,
}
getMutation() {
return Relay.QL`mutation { addTagToPost }`;
}
getVariables() {
return {
name: this.props.tag.name,
};
}
getFatQuery() {
return Relay.QL`
fragment on AddTagToPostMutationPayload {
tag {
id
name
}
post {
id
tags
}
}
`;
}
getConfigs() {
return [{
type: 'REQUIRED_CHILDREN',
children: [Relay.QL`
fragment on AddTagToPostMutationPayload {
tag {
id
name
}
post {
id
tags
}
}
`],
}];
}
getOptimisticResponse() {
return {
tag: {
name: this.props.tag.name,
},
post: {
id: this.props.post.id,
},
};
}
As freiksenet already pointed out, FIELDS_CHANGE should be used in getConfigs() function. I took your schema, implemented the GraphQL types, server-side and client-side mutation to add tag to a post. The client-side gets updated successfully. I'm going to just elaborate the solution in my answer.
First, check your server-side mutation. My implementation uses graphql and graphql-relay libraries and looks like below. Notice that the output of the server-side mutation is a post to which a tag has been added. This post is the one whose ID was provided as input.
const AddTagToPostMutation = mutationWithClientMutationId({
name: 'AddTagToPost',
inputFields: {
postId: { type: new GraphQLNonNull(GraphQLID) },
name: { type: new GraphQLNonNull(GraphQLString) },
},
outputFields: {
post: {
type: PostType,
resolve: ({id}) => getPost(id),
},
},
mutateAndGetPayload: ({postId, name}) => {
const id = fromGlobalId(postId).id;
addTagToPost(id, name);
return {id};
},
});
Using graphiql, you can test your mutation:
mutation {
addTagToPost(input:{
postId: "UG9zdDpwb3N0Mg=="
name:"a new tag name"
clientMutationId:"123244"}) {
post {
id
text
tags {
id
name
}
}
}
}
I added a field posts for all posts to the root query. Using graphiql, I first checked the post IDs and used one above.
Using react-relay, the client-side mutation code looks like below. It is passed a prop post whose ID is used as input variable in getVariables() function. In the getConfigs() function, we specify that post field has to be updated. The association between the payload field post and the passed prop post is established using FIELDS_CHANGE mutation type.
export default class AddTagToPostMutation extends Relay.Mutation {
getMutation() {
return Relay.QL`mutation{addTagToPost}`;
}
getVariables() {
return {
postId: this.props.post.id,
name: this.props.name,
};
}
getFatQuery() {
return Relay.QL`
fragment on AddTagToPostPayload {
post {
id,
tags {
id,
name,
}
}
}
`;
}
getConfigs() {
return [{
type: 'FIELDS_CHANGE',
fieldIDs: {
post: this.props.post.id,
},
}];
}
static fragments = {
post: () => Relay.QL`
fragment on Post {
id,
}
`,
};
}
The client-side mutation is invoked like this:
Relay.Store.commitUpdate(new AddTagToPostMutation({
post: postToModify,
name: tagName,
}));
I think you should just use FIELDS_CHANGE in such situations.
getConfigs() {
return [{
type: 'FIELDS_CHANGE',
fieldIDs: {post: this.props.post.id},
}];
}
getOptimisticResponse() {
return {
post: {
id: this.props.post.id,
tags: [...this.props.post.tags, this.props.tag],
},
};
}

Resources