KeystoneJS relationships, how can I connect using an array of ids - graphql

I am using the new version Keystone Next and I am trying to connect multiple items at once using an array of ids. It seems connect supports that, accepting an array of objects.
const FINISH_VOCABULARY_QUIZ_MUTATION = gql`
mutation FINISH_VOCABULARY_QUIZ_MUTATION(
$userId: ID!
$wordId: ID!
) {
updateUser(id: $userId, data: {
wrongAnswers: {
connect: [{id: "idblabla"}, {id: "idblabla2"}]
}
}) {
id
}
}`;
But what I just can't seem to figure out is how do I pass this array of ids as a variable to my mutation.
I understand that I would need to create a new type? The documentation is still unfinished, so there is nothing on that yet.
I have also tried using string interpolation to form my query, but it seems that it's not a thing in GraphQl.

This is more of a GraphQL question than a KeystoneJS but one but to head to the right direction here you'd need to change your query to something like below:
const FINISH_VOCABULARY_QUIZ_MUTATION = gql`
mutation FINISH_VOCABULARY_QUIZ_MUTATION(
$userId: ID!,
$ids: [UserWhereUniqueInput!]!
) {
updateUser(id: $userId, data: {
wrongAnswers: {
connect: $ids
}
}) {
id
}
}`;
And then map your array of ids to an array of objects with id fields.

There is a better method:
const FINISH_VOCABULARY_QUIZ_MUTATION = gql`
mutation FINISH_VOCABULARY_QUIZ_MUTATION(
$userId: ID!,
$data: SomeAPIDefinedMutationUniqueInput
) {
updateUser(id: $userId, data: $data)
id
}
}`;
This way you:
don't have to define types for internal arguments ($wordsIdWrong: [WordWhereUniqueInput]);
can reuse/share this mutation - import it from some, common for queries, place (dir) - just call it with different data variables;
easier for reading/maintenance;
PS. To be honest, there should be some specific [to quizes] mutation (don't use userUpdate for that), with user (or better quiz) id defined within SomeAPIDefinedUniqueInput.

Related

Can Apollo read partial fragments from cache?

I have a simple mutation editPerson. It changes the name and/or description of a person specified by an id.
I use this little snippet to call the mutator from React components:
function useEditPerson(variables) {
const gqlClient = useGQLClient();
const personFragment = gql`fragment useEditPerson__person on Person {
id
name
description
}`;
return useMutation(gql`
${personFragment}
mutation editPerson($id: ID!, $description: String, $name: String) {
editPerson(id: $id, description: $description, name: $name) {
...useEditPerson__person
}
}
`, {
variables,
optimisticResponse: vars => {
const person = gqlClient.readFragment({
id: vars.id,
fragment: personFragment,
});
return {
editPerson: {
__typename: "Person",
description: "",
name: "",
...person,
...vars,
},
};
},
});
}
This works well enough unless either the name or description for the indicated person hasn't yet been queried and does not exist in the cache; in this case person is null. This is expected from readFragment - any incomplete fragment does this.
The thing is I really need that data to avoid invariant errors - if they're not in the cache I'm totally okay using empty strings as default values, those values aren't displayed anywhere in the UI anyway.
Is there any way to read partial fragments from the cache? Is there a better way to get that data for the optimistic response?
I guess you use the snippet in the form that has all the data you need. So, you can pass the needed data to your useEditPerson hook through the arguments and then use in optimistic response, and then you won't need to use gqlClient.

Sorting results in AWS Amplify GraphQL without filtering

Provided a very simple model in graphql.schema, how would I perform a simple sort query?
type Todo #model
id: ID!
text: String!
}
Which generates the following in queries.js.
export const listTodos = /* GraphQL */ `
query ListTodos(
$filter: ModelTodoFilterInput
$limit: Int
$nextToken: String
) {
listTodos(filter: $filter, limit: $limit, nextToken: $nextToken) {
items {
id
text
}
nextToken
}
}
`;
I have found multiple sources pointing me in the direction of the #key directive. This similar question addresses that approach (GraphQL with AWS Amplify - how to enable sorting on query).
While that may seem promising and successfully generates new queries I can use, all the approaches I have tried require that I filter the data before sorting it. All I want to do is sort my todo results on a given column name, with a given sort direction (ASC/DESC).
This is how I would perform a simple (unsorted) query:
const todos = await API.graphql(graphqlOperation(listTodos));
I would be looking to do something along the lines of:
const todos = await API.graphql(graphqlOperation(listTodos, {sortField: "text", sortDirection: "ASC"} )).
Decorate your model with the #searchable directive, like so:
type Todo #model #searchable
{
id: ID!
text: String!
}
After that, you can query your data with sorting capabilities like below:
import { searchToDos } from '../graphql/queries';
import { API, graphqlOperation } from 'aws-amplify';
const toDoData = await API.graphql(graphqlOperation(searchToDos, {
sort: {
direction: 'asc',
field: 'text'
}
}));
console.log(toDoData.data.searchToDos.items);
For more information, see
https://github.com/aws-amplify/amplify-cli/issues/1851#issuecomment-545245633
https://docs.amplify.aws/cli/graphql-transformer/directives#searchable
Declaring #searchable incurs pointless extra server cost if all you need is straight forward sorting. It spins up an EBS and an OpenSearch that will be about $20 a month minumum.
Instead you need to use the #index directive.
As per the documentation here: https://docs.amplify.aws/guides/api-graphql/query-with-sorting/q/platform/js/
In your model, add the #index directive to one of the fields with a few parameters:
type Todo #model {
id: ID!
title: String!
type: String! #index(name: "todosByDate", queryField: "todosByDate", sortKeyFields: ["createdAt"])
createdAt: String!
}
By declaring the queryField and the sortKeyField you will now have a new query available to once you push your amplify config:
query todosByDate {
todosByDate(
type: "Todo"
sortDirection: ASC
) {
items {
id
title
createdAt
}
}
}
The field you declare this directive on can not be empty (notice the ! after the field name)
This is a much better way of doing it as opposed to #searchable, which is massively overkill.
I've accepted MTran's answer because it feels to me it is the nearest thing to an actual solution, but I've also decided to actually opt for a workaround. This way, I avoid adding a dependency to ElasticSearch.
I ended up adding a field to my schema and every single entry has the same value for that field. That way, I can filter by that value and still have the entire table of values, that I can then sort against.

How to generate the same graphql query with different fields

I'm using graphql-tag so i'm going to use that syntax.
Lets say I have this query:
const query = gql`
query user(id: String) {
user(id: $id) {
id
}
}
`
Whats the best patten to reuse that same query document node if on a different call I want the fields username and email in addition to id without having to rewrite the entire query again like:
const query = gql`
query user(id: String) {
user(id: $id) {
id
username
email
}
}
`
I'm using react-apollo on the frontend if that makes things anymore interesting.
Edit:
Just to clarify... something like this
const userIdFrag = gql`
fragment UserId on User {
id
}
`
const fullUserFrag = gql`
fragment FullUser on User {
id
username
email
}
`
const generateQuery = (documentNode) => {
return gql`
query user(id: String) {
user(id: $id) {
...documentNode
}
}
${documentNode}
`
}
const idQuery = generateQuery(userIdFrag);
const fullUserQuery = generateQuery(fullUserFrag);
(The above does work but give me errors from graphql in the console, which leads me to believe this is not something I should be doing)
Based on your comment the following should work:
const generateQuery = (documentNode, fragment) => {
return gql`
query user(id: String) {
user(id: $id) {
...${fragment}
}
}
${documentNode}
`
}
const idQuery = generateQuery(userIdFrag, 'UserId');
const fullUserQuery = generateQuery(fullUserFrag, 'FullUser');
Basically the fragment name used is the actual one that needs to be spread while the whole documentNode object is put at the end, after query's closing bracket
I am not the very expert on the topic, but here is what I have been able to find out. (if you see any mistakes in my assumptions, let me know).
I found this article that makes some good points against dynamically generating gql queries/mutations. It seems like you get some nice benefits with the static approach, although it's a bit more typing.
But, in case you do need to have dynamic fields, I haven't been able to find anything bad about using the #skip directive GraphQL provides. Here the docs ref.
For the case of using it in react-apollo they also have it in their docs.
So, your code can end up looking something like this:
const query = gql`
query user($id: String, $skipUserMeta: Boolean!) {
user(id: $id) {
id
username #skip(if: $skipUserMeta)
email #skip(if: $skipUserMeta)
}
}
`
You just pass the skipUserMeta as a variable alongside the id field.
NOTE: I actually found a video which talks about the exact same approach here

How to chain mutations in apollo-client while using a form component

Apollo-Client 2.0. I am using chained Mutation components. I am trying to pass a returned value from the first Mutation to the second Mutation. I execute the mutations when an onSubmit button is clicked on a form component. The returned value from first mutation is not being passed as one of the "variables" in second mutation
I reviewed solutions in two very similar posts: How to wrap GraphQL mutation in Apollo client mutation component in React and How to chain together Mutations in apollo client. I think my use of a form is adding some additional complexity to my solution. Although the passed value (competitionId) is visible in the handleOnSubmit function (if I console log after createCompetition() in handleOnSubmit), it is not getting passed as a variable in the second Mutation which is called in the handleOnSubmit. The result is a successful execution of the first Mutation and a 400 error on the second mutation: “errors”:[{“message”:“Variable \“$competitionId\” of required type \“ID!\” was not provided.” To be more specific, the value of CompetitionId DOES get passed to the second mutation after the first mutation runs, but it does not get passed as a "variables" to the createMatch function passed as an argument to the handleOnSubmit. It looks like the "variables" passed along with the createMatch function to the handleOnSubmit, only include the variables that are available when the submit button is clicked. The competitionId, is generated after the submit button is clicked and the first mutation returns it as a result.
handleOnSubmit = async(event, createCompetition, createMatch) => {
event.preventDefault();
await createCompetition();
await createMatch();
this.clearState();
this.props.history.push('/home');
}
render () {
const {location, name, action, caliber, rifleName, dateOf,competitionScore} = this.state;
const { matchNumber, targetNumber, relay, distanceToTarget, matchScore} = this.state;
return (
<div className="App">
<h2 className="App">Add Competition</h2>
<Mutation
mutation={CREATE_COMPETITION}
variables={{location, name, action, caliber, rifleName, dateOf, competitionScore}}
refetchQueries={() => [
{ query: GET_ALL_COMPETITIONS, variables:{name: name}}
]}
update={this.updateCache}>
{(createCompetition, {data, loading, error}) => {
if(loading) return <div>loading competition...</div>
if(error) return <div>error: {error}</div>
let competitionId;
if(data) {
competitionId = data.createCompetition._id;
}
return (
<Mutation
mutation={CREATE_MATCH}
variables={{competitionId, matchNumber, targetNumber, distanceToTarget, matchScore}}>
{(createMatch, {_, loading, error}) => {
if(loading) return <div>loading match...</div>
return (
<form
className="form"
onSubmit={event => this.handleOnSubmit (event, createCompetition, createMatch)}>
<label> remaining form deleted for brevity
I expected the value of the CompetitionId to be passed as a variable to the createMatch function called in the handleOnSubmit method. It is not provided.
Seems what you needs is nested mutations :thinkingface;
Q: Are you using prisma?
Well, in GraphQL you can create nodes by a single mutation, this is pretty simple if your Types are related, so I assume this is your case.
And should looks something like this:
datamodel.graphql
type Competition {
id: ID! #unique
name: String!
match: Match! #relation(name: "CompetitionMatch")
}
type Match {
id: ID! #unique
name: String!
campetition: Competition! #relation(name: "CompetitionMatch")
}
So, now in your schema.graphql should looks like this:
type Mutation {
createCompetition (name: String! match: MatchInput): Competition
}
input MatchInput {
name: String!
}
and now when you call your createCompetition mutation, you have to send the match data, like so:
mutation createCompetition (
name: 'Loremp competition'
match: { name: 'child match'}
) {
id
name
match {
id
name
}
}
Ref: https://www.graph.cool/docs/reference/graphql-api/mutation-api-ol0yuoz6go/#nested-create-mutations
Hope this help!
regards
Where the if (data) is, is where you should return the 2nd mutation

Pass through GraphQL variables to second function in an elegant manner

I'm working with GraphQL and having some trouble finding the best way to pipe variables from the query to the result.
I have a schema like so:
type Fragment {
# The id of the fragment
id: String!
# The key of the fragment
key: String!
# The type of component
component_type: String!
# The params used to build the fragment
params: JSON
# Component data
data: JSON
children: [JSON]
items: [JSON]
}
The fragment is meant as a "cms" fragment. I want to pass some query data through to another backend after this resolves.
My query looks like this:
query getFragmentsWithItems($keys: [String!]!
$platform: PlatformType
$version: String
$userInfo: UserInput
$userId: Int
) {
fragmentsWithItems(keys: $keys, platform: $platform, version: $version, userInfo: $userInfo, userId: $userId) {
key
data
children
params
items
}
}
Here's the problem: I have some query data in the data field from the Fragment. That data is not available until that Fragment has resolved. I want to take that data and send it to a different backend. I want to do this with GraphQL, and I was hoping to do something like:
Fragment: () => {
async query(obj, args, context, info, {modles}) => {
const items = await models.getItems(obj.query_string);
}
}
But I need the user_info and user_id that I passed to the original query. Apparently that is only accessible from the info argument which is not meant to be used.
The other path I've taken is to have a manual resolver that does something like so:
const resolveFI = ({ keys, platform, version, userInfo, userId, models }) => {
if (!keys || !keys.length) {
return Promise.resolve(null);
}
return models.release.get({ platform, version }).then(release =>
Promise.all(
keys.map(key =>
models.fragments.get({
key,
platform,
version,
release: release.id
})
)
).then(data => {
const promises = [];
data.rows.forEach(r => {
if (r.data.query_data) {
const d = {
// Can just ignore
filters: r.data.query_data.filters || {},
user_info: userInfo,
user_id: userId
};
promises.push(
new Promise(resolve => {
resolve(
models.itemSearch.get(d).then(i => ({ items: i.items, ...r }))
);
})
);
}
...etc other backends
This works, however a manual promise chain seems to defeat the purpose of using GraphQL.
The last thing I tried was making items a non-scalar type, something like:
type Fragment {
items: ItemSearchResult(user_info: UserInput) etc
But since I can't pipe the actual result from Fragment to the ItemSearchResult that doesn't work.
I realize this is pretty long-winded so I'm open to edits or clarifying.
I'm looking to see if I've missed a better approach or if I should just bag it and have the client apps do the item query after they get the Fragment data back.
It's not that you're not supposed to use info -- it's just a tremendous pain in the butt to use ;) In all seriousness, it's meant to be used for optimization and more advanced use cases, so you shouldn't hesitate to use it if a better solution doesn't present itself. There are libraries out there (like this one) that you can use to parse the object more easily.
That said, there's a couple of ways I imagine you could handle this:
1.) Inside your query resolver(s)
getFragmentsWithItems: async (obj, args, ctx, info) => {
const fragments = await howeverYouDoThat()
const backendCalls = fragments.map(fragment => {
// extract whatever data you need from the fragment
return asyncCallToBackEnd()
})
await backendCalls
return fragments
}
Unfortunately, if you have a lot of different queries returning fragments, you'll end up with redundancy.
2.) Inside the resolver for an existing field (or an additional one) on the Fragment type.
If you go this route, and you need args passed to the query field, you can extract them using the info. Alternatively, you can also mutate the context object inside your query resolver and attach those arguments to it. Then, all resolvers "below" the query resolver (like the resolvers for your Fragment fields) can access those arguments through the context.
3.) Apollo Server lets you define a formatResponse function when configuring its middleware. This essentially provides a hook to do whatever you want with the response before it's returned to the client. You could parse the response inside that function and make the calls to the other backend from there.

Resources