2

I want to bulk update list of entries with graphQL mutation in faunaDB. The input data is list of coronavirus cases from external source. It will be updated frequently. The mutation should update existing entries if the entry name is present in collectio and create new ones if not present.

Current GRAPHQL MUTATION

mutation UpdateList($data: ListInput!) {
  updateList(id: "260351229231628818", data: $data) {
    title
    cities {
      data {
        name
        infected
      }
    }
  }
}

GRAPHQL VARIABLES

{
  "data": {
    "title": "COVID-19",
    "cities": {
      "create": [
        {
          "id": 22,
          "name": "Warsaw",
          "location": {
            "create": {
              "lat": 52.229832,
              "lng": 21.011689
            }
          },
          "deaths": 0,
          "cured": 0,
          "infected": 37,
          "type": "ACTIVE",
          "created_timestamp": 1583671445,
          "last_modified_timestamp": 1584389018
        }
      ]
    }
  }
}

SCHEMA

type cityEntry {
  id: Int!
  name: String!
  deaths: Int!
  cured: Int!
  infected: Int!
  type: String!
  created_timestamp: Int!
  last_modified_timestamp: Int!
  location: LatLng!
  list: List
}

type LatLng {
  lat: Float!
  lng: Float!
}

type List {
  title: String!
  cities: [cityEntry] @relation
}

type Query {
  items: [cityEntry!]
  allCities: [cityEntry!]
  cityEntriesByDeathFlag(deaths: Int!): [cityEntry!]
  cityEntriesByCuredFlag(cured: Int!): [cityEntry!]
  allLists: [List!]
}

Everytime the mutation runs it creates new duplicates. What is the best way to update the list within single mutation?

user1276919
  • 550
  • 5
  • 25
  • Hi! Could you maybe share the schema (or a portion of the schema) in the answer? It looks like you might need an upsert mutation in order to do so, but unfortunately that is currently not available on FaunaDB. You might try creating the cities beforehand, and then using "connect" instead of "create" when creating that type from the mutation you shared. – lregnier Mar 18 '20 at 19:03
  • Thanks for the suggestion. I added schema details. The upsert mutation is exacly what i need - "If not present insert entries else update them". If it is not supported yet in fauna graphQL maybe it is possible Fauna Query Language. – user1276919 Mar 19 '20 at 08:38
  • It can definitely be implemented in FQL as a separate function. To implement custom resolvers you could take a look at: https://css-tricks.com/instant-graphql-backend-using-faunadb. I'll let Leo give you the details since he is he expert. In the meantime, what you are building seems interesting, if you want to talk about it with us or need help to smoothen your progress, please join http://community.fauna.com and talk to us – Brecht De Rooms Mar 19 '20 at 11:23
  • @BrechtDeRooms I asked the same question on FaunaDB community twice and did not get any reply. And here it seems there is no reply either. Also the docs do not show a single example how an argument is passed to the UDF from graphql using a resolver. – crisscross May 07 '20 at 22:26
  • Ok got it, didn't occur to me that there was a missing schema example in the @resolver docs. I've let our docs team know and constructed an answer below :) – Brecht De Rooms May 10 '20 at 12:46

1 Answers1

7

my apologies for the delay, I wasn't sure exactly what the missing information was hence why I commented first :).

The Schema

An example of a part of a schema that has arguments:

type Mutation {
  register(email: String!, password: String!): Account! @resolver
  login(email: String!, password: String!): String! @resolver
}

When such a schema is imported in FaunaDB there will be placeholder functions provided.

The UDF parameters

Function stubs

As you can see all the function does is Abort with the message that the function still has to be implemented. The implementation starts with a Lambda that takes arguments and those arguments have to match what you defined in the resolver.

Query(Lambda(['email', 'password'],
    ... function body ...
))

Using the arguments is done with Var, that means Var('email') or Var('password') in this case. For example, in my specific case we would use the email that was passed in to get an account by email and use the password to pass on to the Login function which will return a secret (the reason I do the select here is that the return value for a GraphQL resolver has to be a valid GraphQL result (e.g. plain JSON

Query(Lambda(['email', 'password'],
    Select(
      ['secret'],
      Login(Match(Index('accountsByEmail'), Var('email')), {
        password: Var('password')
      })
    )
))

Calling the UDF resolver via GraphQL

Finally, how to pass parameters when calling it? That should be clear from the GraphQL playground as it will provide you with the docs and autocompletion. For example, this is what the auto-generated GraphQL docs tell me after my schema import:

enter image description here

Which means we can call it as follows:

mutation CallLogin {
  login (
    email: "<some email>"
    password: "<some pword>"
  )
}

Bulk updates

For bulk updates, you can also pass a list of values to the User Defined Function (UDF). Let's say we would want to group a number of accounts together in a specific team via the UI and therefore want to update multiple accounts at the same time.

The mutation in our Schema could look as follows (ID's in GraphQL are similar to Strings)

type Mutation { updateAccounts(accountRefs: [ID]): [ID]! @resolver } 

We could then call the mutation by providing in the id's that we receive from FaunaDB (the string, not the Ref in case you are mixing FQL and GraphQL, if you only use GraphQL, don't worry about it).

mutation {
    updateAccounts(accountRefs: ["265317328423485952", "265317336075993600"] )
}

Just like before, we will have to fill in the User Defined Function that was generated by FaunaDB. A skeleton function that just takes in the array and returns it would look like:

Query(Lambda(['arr'], 
  Var('arr')
))

Some people might have seen an easier syntax and would be tempted to use this:

Query(Lambda(arr => arr))

However, this currently does not work with GraphQL when passing in arrays, it's a known issue that will be fixed. The next step is to actually loop over the array. FQL is not declarative and draws inspiration from functional languages which means you would do that just by using a 'map' or a 'foreach'

Query(Lambda(["accountArray"], 
  Map(Var("accountArray"), 
      Lambda("account", Var("account")))
))

We now loop over the list but don't do anything with it yet since we just return the account in the map's body. We will now update the account and just set a value 'teamName' on there. For that we need the Update function which takes a FaunaDB Reference. GraphQL sends us strings and not references so we need to transform these ID strings to a reference with Ref as follows:

 Ref(Collection('Account'), Var("account"))

If we put it all together we can add an extra attribute to a list of accounts ids as follows:

Query(Lambda(["accountArray"], 
  Map(Var("accountArray"), 
    Lambda("account", 
       Do(
         Update(
          Ref(Collection('Account'), Var("account")),
          { data: { teamName: "Awesome live-coders" } }
        ),
        Var("account")
      )
    )
  )
))

At the end of the Map, we just return the ID of the account again with Var("account") in order to return something that is just plain JSON, else we would be returning FaunaDB Refs which are more than just JSON and will not be accepted by the GraphQL call.

enter image description here

Passing in more complex types.

Sometimes you want to pass in more complex types. Let's say we have a simple todo schema.

type Todo {
  title: String!
  completed: Boolean!
}

And we want to set the completed value of a list of todos with specific titles to true. We can see in the extended schema generated by FaunaDB that there is a TodoInput.

enter image description here

If you see that extended schema you might think, "Hey that's exactly what I need!" but you can't access it when you write your mutations since you do not have that part of the schema at creation time and therefore can't just write:

type Mutation { updateTodos(todos: [TodoInput]): Boolean! @resolver } 

As it will return the following error. enter image description here

However, we can just add it to the schema ourselves. Fauna will just accept that you already wrote it and not override it (make sure that you keep the required fields, else your generated 'createTodo' mutation won't work anymore).

type Todo {
  title: String!
  completed: Boolean!
}

input TodoInput {
  title: String!
  completed: Boolean!
}

type Mutation { updateTodos(todos: [TodoInput]): Boolean! @resolver } 

Which means that I can now write:

mutation {
  updateTodos(todos: [{title: "test", completed: true}])
}

and dive into the FQL function to do things with this input. Or if you want to include the ID along with data you can define a new type.

input TodoUpdateInput {
  id: ID!
  title: String!
  completed: Boolean!
}
type Mutation { updateTodos(todos: [TodoUpdateInput]): Boolean! @resolver } 

Once you get the hang of it and want to learn more about FQL (that's a whole different topic) we are currently writing a series of articles along with code for which the first one appeared here: https://css-tricks.com/rethinking-twitter-as-a-serverless-app/ which is probably a good gentle introduction.

Brecht De Rooms
  • 1,802
  • 7
  • 15
  • Thanks lot to for your answer @BrechtDeRooms. The only puzzle missing from your example is that we need to pass in an array of IDs in order to make a bulk update. So how do I pass all documents in that need to be changed? E.g. updateBulkArticle(id: [ID]): [Article] @resolver(name: "bulkArticleUpdate" ) – crisscross May 10 '20 at 21:17
  • I assume that you mean FaunaDB References (Ref) with id right? If so, I think I get your confusion – Brecht De Rooms May 11 '20 at 19:45
  • Yes anything that I can send in my app using graphql and the UDF knows what to "bulk update". I believe the original question above had the same intent. – crisscross May 11 '20 at 19:49
  • Yeah, I did indeed forget to address the bulk aspect. Before I edit the answer, does this answer your question: A schema example to just pass an array of items: type Mutation { test(testInput: [String]): [String]! @resolver } Calling it: mutation Test { test(testInput: ["test"]) } An example UDF that takes in the array maps (or use Foreach) over it and just returns the original strings. But the elements are there for you to do something with it Query(Lambda(["arr"], Map(Var("arr"), Lambda("x", Var("x"))))) It has to be ["arr"], not "arr" – Brecht De Rooms May 11 '20 at 20:30
  • Finally, I'm just sending an array of strings since GraphQL does not know about FaunaDB 'Refs'. You can easily convert a string to a Ref though in the UDF. https://docs.fauna.com/fauna/current/api/fql/functions/ref If that answers your question I'll adapt the answer and write it out for you, but I wanted to get back quick in between two other tasks :) – Brecht De Rooms May 11 '20 at 20:31
  • And yeah, you can replace the String with ID as well – Brecht De Rooms May 11 '20 at 20:43
  • 1
    Yes that does answer it. Hope it the same case for @user1276919 and we can check mark this answer. Sure Graphql does not understand 'Refs', but I can pass an array of IDs and the UDF will then take care of it. Thanks so much for your support. I am sure this will be also useful for other people in the future. – crisscross May 11 '20 at 21:15
  • I got it working. Thanks so much! Through UDFs and resolvers Graphql is amazingly extended. Love it. – crisscross May 11 '20 at 21:32
  • Updated :), thanks for the comments and for posting on StackOverflow, that indeed helps us answer the question for multiple people at once :) – Brecht De Rooms May 12 '20 at 12:53