How to update all records in a collection using graphql - graphql

I'm using Graph.cool graphql as a service and am wondering how to do a mass update to the collection, similar to a SQL update.
In my case I need to update the suffix of a url, in the imageUrl column of my database. I need to swap out a {someid}_sm.jpg to {someid}_lg.jpg
How do I do that with a graphql mutation? I don't want to reload the entire dataset again and am looking for a way to do it that doesn't involve manually interating through the entire list with a graphql client.
mutation {
updatePost() // what goes here?
}

Migration script
The best approach is indeed to use a migration script that combines multiple mutations so only one HTTP request is sent to the GraphQL backend.
Consider this schema:
type Image {
id: ID!
name: String!
}
We can include the same mutation multiple times in one request with GraphQL aliases:
mutation {
first: updateImage(id: "first-id", name: "01_lg.jpg") {
id
name
}
second: updateImage(id: "second-id", name: "02_lg.jpg") {
id
name
}
}
We'll make use of this mechanism in our migration script. I'll describe it with Lokka and Node, however you can choose whatever language and GraphQL client you prefer.
First, we query all existing images to obtain their id and name:
const queryImages = async() => {
const result = await client.query(`{
images: allImages {
id
name
}
}`)
return result.images
}
Then we replace the names accordingly and construct one big request including the necessary updateImage mutations with a different GraphQL alias for each.
If your image names might contain the string sm in the {someid} part mentioned in your question, this script will break! In that case, please adjust accordingly.
const migrateImages = async(images) => {
// beware! if your ids contain the string 'sm', adjust the string replacement accordingly!
const updateMutations = _.chain(images)
.map(image => ({ id: image.id, name: image.name.replace('sm', 'lg')}))
.map(image => `
${image.id}: updateImage(id: "${image.id}", name: "${image.name}") {
id
name
}`)
.value()
.join('\n')
const result = await client.mutate(`{
${updateMutations}
}`)
console.log(`Updated ${Object.keys(result).length} images`)
console.log(result)
}
That's it. If you have to update thousands of images, batching the mutations in say groups of a hundred might be better than to batch all of them in one request. Note that mutations run sequentially on the GraphQL server.
Running the migration
Currently, I suggest the following workflow for running the migration:
Clone your project
Run the migration script on your cloned project
Verify that the migration ran successfully. Double check :)
Run the migration on your original project
You can find the code and further instructions here.
While this approach is great for migrations that are as straightforward as in your example, it's not perfect for all situations. We're already thinking about creating an integrated experience for this use case, such as an interactive migration right in your Graphcool project, with simulated migrations, checks and more. If you have suggestions, let me know in Slack.

Related

Should a query in Apollo Client look for the results cached by different queries before making a network request?

I'm trying to figure out how queries in Apollo Client are supposed to interact with the cache.
Specifically, I want to know if we run a query that fetches all todos:
todos {
title
completed
}
And then later we run a query that fetches a single todo that was already fetched by the todos query and requests the exact same fields:
todo(id: $id) {
title
completed
}
Should the second query a) fetch the data from the cache, or b) make a network request?
My assumption was that it would be case A. This is based on this quote from an official Apollo blog post:
https://www.apollographql.com/blog/demystifying-cache-normalization/
For example, if we were to:
Perform a GetAllTodos query, normalizing and caching all todos from a backend
Call GetTodoById on a todo that we had already retrieved with GetAllTodos
...then Apollo Client could just reach into the cache and get the object directly without making another request.
However, in my app I kept getting case B, it was always making an additional network request even though I had already requested all the data in a different query.
I assumed that I was doing something wrong, so I checked out this Apollo Full-stack Tutorial repo (https://github.com/apollographql/fullstack-tutorial) and updated the LaunchDetails query to only request the same data that was already requested in the GetLaunchList query. This replicated the same scenario I detailed above with the todos.
The queries now look like this:
export const GET_LAUNCHES = gql`
query GetLaunchList($after: String) {
launches(after: $after) {
cursor
hasMore
launches {
...LaunchTile
}
}
}
${LAUNCH_TILE_DATA}
`;
export const GET_LAUNCH_DETAILS = gql`
query LaunchDetails($launchId: ID!) {
launch(id: $launchId) {
...LaunchTile
}
}
${LAUNCH_TILE_DATA}
`;
I ran the application, and found that a new network request was made for the LaunchDetails query, even though all the required data was already in the cache after the GetLaunchList query was run.
I haven't been able to find any answer to this in the documentation, and the results I'm seeing from the example tutorial app seem to be at odds with the quote from the blog piece above.
Is it the case that a query will only look to the cache if the query has already been run before? Can it not fetch cached data if that data was cached by a different query? Am I missing something?
Please see this better (in my opinion) answer here:
https://stackoverflow.com/a/66053242/6423036
Copying directly from that answer, credit to the author:
This functionality exists, but it's hard to find if you don't know what you're looking for. In Apollo Client v2 you're looking for cache redirect functionality, in Apollo Client v3 this is replaced by type policies / field read policies (v3 docs).
Apollo doesn't 'know' your GraphQL schema and that makes it easy to set up and work with in day-to-day usage. However, this implies that given some query (e.g. getBooks) it doesn't know what the result type is going to be upfront. It does know it afterwards, as long as the __typename's are enabled. This is the default behaviour and is needed for normalized caching.
Let's assume you have a getBooks query that fetches a list of Books. If you inspect the cache after this request is finished using Apollo devtools, you should find the books in the cache using the Book:123 key in which Book is the typename and 123 is the id. If it exists (and is queried!) the id field is used as identifier for the cache. If your id field has another name, you can use the typePolicies of the cache to inform Apollo InMemoryCache about this field.
If you've set this up and you run a getBook query afterwards, using some id as input, you will not get any cached data. The reason is as described before: Apollo doesn't know upfront which type this query is going to return.
So in Apollo v2 you would use a cacheRedirect to 'redirect' Apollo to the right cache:
cacheRedirects: {
Query: {
getBook(_, args, { getCacheKey }) {
return getCacheKey({
__typename: 'Book',
id: args.id,
});
}
},
},
(args.id should be replaced by another identifier if you have specified another key in the typePolicy)
When using Apollo v3, you need a typepolicy / field read policy:
typePolicies: {
Query: {
fields: {
getBook(_, { args, toReference }) {
return toReference({
__typename: 'Book',
id: args.id,
});
}
}
}
}
the query will make a network query.
todo(id: $id) {
title
completed
}
Apollo cache isn't very smart. It is just storage. You need to read/write for more complicated operations manually.
The reason for this is Apollo doesn't know about your schema and data structure. It doesn't know that todo(id: $id) will do DB search by, so it can't optimize to look in the cache.
If you don't want a second fetch, you have to implement your data fetch structure with fragment:
try {
return client.readFragment({
id: 'Todo:5', // The value of the to-do item's unique identifier
fragment: gql`
fragment TodoFragment on Todo {
id
title
completed
}
`,
});
} catch(_e) { // if no fragment is found there will be an error
client.query(QUERY, variables: { id: 5})
}
The way Apollo cache is that if you do two queries:
load todos
todos {
id
title
completed
}
load single todo
todo(id: $id) {
id
title
completed
}
If you list a list of todos and load the second one - it will update the todo data.

How are arguments added to GraphQL, do they need to be defined before?

Hi Everyone I am just trying to learn graphql as I am using Gatsby. I want to know does each field in graphql take an argument or does it need to be defined somehow before. So for example if you visit this link graphql search results
https://graphql.org/swapi-graphql?query=%7B%0A%09allPeople%20%7B%0A%09%20%20people%20%7B%0A%09%20%20%20%20id%0A%20%20%20%20%20%20name%0A%20%20%20%20%20%20birthYear%0A%20%20%20%20%20%20eyeColor%0A%09%20%20%7D%0A%09%7D%0A%7D%0A
If i wanted to limit people by eye color how would I do that. In the docs it seems easy as you would just do something like people(eyecolor: 'brown') but that doesn't seem possible. Am I missing something? I basically want to do a SQL style search for all people where eye color is brown.
Thanks.
Arguments need to be defined in the schema and implemented in the resolver. If you're consuming a 3rd party API (like the link you provided), you're limited to their schema. You can tell by looking at their schema (by clicking Docs on the right side of the page) which fields take arguments. For example, person takes id and personID arguments:
people doesn't take any arguments, as seen in the schema:
If you're building your own schema, you can add arguments to any field, and when you implement the resolver for that field you can use the arguments for logic in that resolver.
If you're working with a schema that you don't control, you'll have to add filtering on the frontend:
const {people} = data.allPeople;
const brownEyedPeople = people.filter(({eyeColor}) => eyeColor === 'brown');
When you start developing in Gatsby and actually pull your data into Gatsby, there will be a filter query option that automatically becomes available in the query arguments.
https://www.gatsbyjs.org/docs/graphql-reference/#filter
You can expect to be able to filter your people by eyeColor by using the below query:
{
allPeople(filter: { eyeColor: { eq: "brown" } }) {
edges {
node {
id
name
birthYear
eyeColor
}
}
}
}

Call the same GraphQL mutation action many times in one http request [duplicate]

I have a mutation:
const createSomethingMutation = gql`
mutation($data: SomethingCreateInput!) {
createSomething(data: $data) {
something {
id
name
}
}
}
`;
How do I create many Somethings in one request? Do I need to create a new Mutation on my GraphQL server like this:
mutation {
addManySomethings(data: [SomethingCreateInput]): [Something]
}
Or is there a way to use the one existing createSomethingMutation from Apollo Client multiple times with different arguments in one request?
You can in fact do this using aliases, and separate variables for each alias:
const createSomethingMutation = gql`
mutation($dataA: SomethingCreateInput!) {
createA: createSomething(data: $dataA) {
something {
id
name
}
}
createB: createSomething(data: $dataB) {
something {
id
name
}
}
}
`;
You can see more examples of aliases in the spec.
Then you just need to provide a variables object with two properties -- dataA and dataB. Things can get pretty messy if you need the number of mutations to be dynamic, though. Generally, in cases like this it's probably easier (and more efficient) to just expose a single mutation to handle creating/updating one or more instances of a model.
If you're trying to reduce the number of network requests from the client to server, you could also look into query batching.
It's not possible so easily.
Because the mutation has one consistent name and graphql will not allow to have the same operation multiple times in one query. So for this to work Apollo would have to map the mutations into aliases and then even map the variables data into some unknown iterable form, which i highly doubt it does.

Unable to combine local and remote data in a single GraphQL query (Next.js + Apollo)

The setup:
My basic setup is a Next.js app querying data from a GraphQL API.
I am fetching an array of objects from the API and am able to display that array on the client.
I want to be able to filter the data based on Enum values that are defined in the API schema. I am able to pass these values programmatically and the data is correctly updated.
I want those filters to be persistent when a user leaves the page & come back. I was originally planning to use Redux, but then I read about apollo-link-state and the ability to store local (client) state into the Apollo store, so I set out to use that instead. So far, so good.
The problem:
When I try to combine the local query and the remote query into a single one, I get the following error: networkError: TypeError: Cannot read property 'some' of undefined
My query looks like this:
const GET_COMBINED = gql`
{
items {
id
details
}
filters #client
}
`
And I use it inside a component like this:
export default const Items = () => (
<Query query={GET_COMBINED}>
{({ loading, error, data: { items, filters } }) => {
...do stuff...
}}
</Query>
)
IF however, I run the queries separately, like the following:
const GET_ITEMS = gql`
{
items {
id
details
}
}
`
const GET_FILTERS = gql`
{
filters #client
}
`
And nest the queries inside the component:
export default const Items = () => (
<Query query={GET_ITEMS}>
{({ loading, error, data: { items } }) => {
return (
<Query query={GET_FILTERS}>
{({ data: { filters } }) => {
...do stuff...
}}
</Query>
)
}}
</Query>
)
Then it works as intended!
But it seems far from optimal to nest queries like this when a single query would - in theory, at least - do the job. And I truly don't understand why the combined query won't work.
I've stripped my app to its bare bones trying to understand, but the gist of it is, whenever I try to combine fetching local & remote data into a single query, it fails miserably, while in isolation both work just fine.
Is the problem coming from SSR/Next? Am I doing it wrong? Thanks in advance for your help!
Edit 2 - additional details
The error is triggered by react-apollo's getDataFromTree, however even when I choose to skip the query during SSR (by passing the ssr: false prop to the Query component), the combined query still fails. Besides, both the remote AND local queries work server-side when run separately. I am puzzled.
I've put together a small repo based on NextJS's with-apollo example that reproduces the problem here: https://github.com/jaxxeh/next-with-apollo-local
Once the app is running, clicking on the Posts (combined) link straight away will trigger an error, while Posts (split) link will display the data as intended.
Once the data has been loaded, the Posts (combined) will show data, but the attempt to load extra data will trigger an error. Reloading (i.e. server-rendering) the page will also trigger an error. Checkboxes will be functional and their state preserved across the app.
The Posts (split) page will fully function as intended. You can load extra post data, reload the page and set checkboxes.
So there is clearly an issue with the combined query, be it on the server-side (error on reload) or the client-side (unable to display additional posts). Direct writes to the local state (which bypass the query altogether) do work, however.
I've removed the Apollo init code for brevity & clarity, it is available on the repo linked above. Thank you.
Add an empty object as your resolver map to the config you pass to withClientState:
const stateLink = withClientState({
cache,
defaults: {
filters: ['A', 'B', 'C', 'D']
},
resolvers: {},
typedefs: `
type Query {
filters: [String!]!
}
`,
})
There's a related issue here. Would be great if the constructor threw some kind of error if the option was missing or if the docs were clearer about it.

How to load the graphql queries from the server without defining it in the front end?

Now let's say we are using a REST API. I have one endpoint like this: /homeNewsFeed. This API will give us a response like this:
[
{
blockTitle: 'News',
type: 'list',
api: 'http://localhost/news'
},
{
blockTitle: 'Photos',
type: 'gallery',
api: 'http://localhost/gallery'
}
]
Now after getting this we go through the array and call the respective endpoints to load the data. My question is, how to do this in GraphQL? Normally we define the query in the front end code. Without doing that, how to let the server decide what to send?
The main reason to do this is. Imagine we have a mobile app. We need to push new blocks to this news feed without sending an app update. But each item can have their own query.
Normally we define the query in the front end code. Without doing that, how to let the server decide what to send?
Per the spec, a GraphQL execution request must include two things: 1) a schema; and 2) a document containing an operation definition. The operation definition determines what operation (which query or mutation) to execute as well as the format of the response. There are work arounds and exceptions (I'll discuss some below), but, in general, if specifying the shape of the response on the client-side is undesirable or somehow not possible, you should carefully consider whether GraphQL is the right solution for your needs.
That aside, GraphQL lends itself more to a single request, not a series of structured requests like your existing REST API requires. So the response would look more like this:
[
{
title: 'News',
content: [
...
],
},
{
title: 'Photos',
content: [
...
],
}
]
and the corresponding query might look like this:
query HomePageContent {
blocks {
title
content {
# additional fields
}
}
}
Now the question becomes how do differentiate between different kinds of content. This is normally solved by utilizing an interface or union to aggregate multiple types into a single abstract type. The exact structure of your schema will depend on the data you're sending, but here's an example:
interface BlockContentItem {
id: ID!
url: String!
}
type Story implements BlockContentItem {
id: ID!
url: String!
author: String!
title: String!
}
type Image implement BlockContentItem {
id: ID!
url: String!
alt: String!
}
type Block {
title: String!
content: [BlockContentItem!]!
}
type Query {
blocks: [Block!]!
}
You can now query blocks like this:
query HomePageContent {
blocks {
title
content {
# these fields apply to all BlockContentItems
__typename
id
url
# then we use inline fragments to specify type-specific fields
... on Image {
alt
}
... on Story {
author
title
}
}
}
}
Using inline fragments like this ensures type-specific fields are only returned for instances of those types. I included __typename to identify what type a given object is, which may be helpful to the client app (clients like Apollo automatically include this field anyway).
Of course, there is still the issue of what happens when you want to add a new block. If the block's content fits an existing type, no sweat. But what happens when you anticipate you will need a different type in the future, but can't design around that right now?
Typically, that sort of change would require both a schema change on the server and a query change on the client. And in most cases, this will probably be fine because if you're getting data in a different structure, you will have to update your client app anyway. Otherwise, your app won't know how to render the new data structure correctly.
But let's say we want to future-proof our schema anyway. Here's two ways you could go about doing it.
Instead of specifying an interface for content, just utilize a custom JSON scalar. This will effectively throw the response validation out the window, but it will allow you to return whatever you want for the content of a given block.
Abstract out whatever fields might be needed in the future into some kind of value-key type. For example:
.
type MetaItem {
key: String!
value: String!
}
type Block {
title: String!
meta: [MetaItem!]!
# other common fields
}
There's any number of other workarounds, some better than others depending on the kind of data you're working with. But hopefully that gives you some idea how to address the scenario you describe in a GraphQL context.

Resources