Ability to connect if exists, otherwise create in GraphQL mutations

Hello there,
I’m wondering if it would be possible to implement some kind of upsert but for relational connections in mutations.
Let me explain:
Right now when using a graphQL connection we have to the ability to either create or connect to an existing entity: https://docs.fauna.com/fauna/current/api/graphql/relations#mutations
In my case, I would need the ability to “up-nect” which means, that if the unique identifier of the specified item already exists, then connect to it, otherwise creates it with the given data.
This would be very handy without having to resort to a UDF.
Also, I don’t know how to proceed with such a UDF. Any help appreciated.
Thanks

Hi Louis! :slight_smile:

The generated connect mutation used the ref.id to connect (saves the entire Ref in the Document), and you cannot specify a different unique field. You will need to use an UDF.

That UDF will need to be a root level mutation, not a nested CRUD mutation. This sounds like a thing that would be better as a separate mutation anyway, considering best advise from GraphQL people.

Can you share the SDL definitions of the types that need to be connected? Query will matter on what cardinality the relationship is, and (unfortunately) maybe also the names of the type to determine how the Refs are stored.

What you will be doing is finding a Document by your unique field, but still have to store the relationship by the Ref. This will ensure that after this mutation is run, all the existing GraphQL queries still retrieve the relations correctly.

Thanks for your answer Paul,
I’m not sure I understand how that would not be possible to have such a “up-nect” mutation, as it already tests for uniqueness based on the specified unique field, but I trust you on this one.
Or maybe you’re only referring to the present day (which I understand is not possible at the moment). But having that baked into GQL would be awesome.

Anyhow here is my SDL:

type FileMeta {
  sha: String! @unique
  gitSha: String!
  size: Int!
  path: String!
  gitRefs: [GitRef!] @relation
}

type GitRef {
  commitSha: String! @unique
  filesMeta: [FileMeta!]! @relation
}

It’s pretty simple but it does have a many-to-many relation.
So I guess I need to reproduce the behaviour of the create mutation with a few If() and Exists() added.
I’ll let you know how it goes and post back my finding in case it helps others.

1 Like

Are you trying to “upnect” in both directions? (I am loving this word btw)

Or maybe this mutation can just take the unique fields of both as an input?

Haha glad you like it.
For now I only need to create one GitRef and its multiple children FileMeta

Two more questions:

Do you REALLY need this to be many-to-many? That makes things much more complicated for what you are trying to do in my opinion.

Also, Since I am not familiar with the inner workings of Git, is this FileMeta.sha or FileMeta.gitSha supposed to match GitRef.commitSha?

I promise it’s relevant to helping! :smiley:

Hum, I’m thinking about it.
A few precisions:

  • FileMeta.sha is different from FileMeta.gitSha because git computes the files’ sha differently. In both cases, these refers to a computed hash of a file. We’re only interested in FileMeta.sha, we can ignore the other.
  • GitRef.commitSha in the other hand refers to the commit to which the files belongs to.

So each file can belong to one or more commit (if it hasn’t changed for example, it keeps the same FileMeta.sha)

The other solution I though about is NOT to enforce uniqueness regarding FileMeta.sha and simply allow duplicated entries of FileMeta. Which would mean a one-to-many only relationship.
But I find this weird as I would have duplicated files that are the same but just linked to different GItRef.
What are your thoughts ?

Or… even simpler, removing the relation altogether, and simply have the commitRef as a property of the file. And allow FileMeta duplicates based on the file’s sha.

I think you might be misunderstanding the @unique directive. While the it ensures it is unique within the Collection, it has no bearing on the relationships.

That is, you can have a unique fields specified in a collection but still have a one-to-many relation.

You need to store the Refs with relations to allow the GraphQL queries to work. The native GraphQL API will not resolve relations in any other way, and you cannot specify resolvers on user defined types (yet).

Even if this was all pure FQL, storing a Ref is the way to go, even if it means using the other foreign IDs to match and connect initially.

I’m not sure I understand correctly.
Here is what I would ideally need:
Each FileMeta being uniquely identified by its sha. If the file’s content change, the sha will too. Otherwise, it stays the same. That way, when a commit reference a file, it can “use” the ones that haven’t changed, and create the one that are new.

In that case, I don’t see how I could use a one-to-many relationship. Because my GitRef obviously references multiple files, but also my FileMeta can be referenced by multiple commits (if it hasn’t changed between two commits for example)

1 Like

I have this working

pseudo code:

if (git and fileMeta docs exist based on sha) then
  if (not already a link created between them) then
    Create new link
    "created a new link"
  else
    "link already created"
else
  "git or fileMeta docs do not exist"

GraphQL Schema

type FileMeta {
  sha: String! @unique
  gitSha: String
  size: Int
  path: String
  gitRefs: [GitRef!] @relation
}

type GitRef {
  commitSha: String! @unique
  filesMeta: [FileMeta!]! @relation
}

type Mutation {
  upnect(commitSha: String!, fileSha: String!): 
      String! @resolver(name: "up-nect")
}

Updated “up-nect” function

{
  name: "up-nect",
  body: Query(
    Lambda(
      ["commitSha", "fileSha"],
      Let(
        {
          gitRefRef_maybe: Match(
            Index("unique_GitRef_commitSha"),
            Var("commitSha")
          ),
          fileMetaRef_maybe: Match(
            Index("unique_FileMeta_sha"),
            Var("fileSha")
          ),
          stuffExists: And(
            Exists(Var("gitRefRef_maybe")),
            Exists(Var("fileMetaRef_maybe"))
          )
        },
        If(
          Var("stuffExists"),
          Let(
            {
              gitRefRef: Select(["data", 0], Paginate(Var("gitRefRef_maybe"))),
              fileMetaRef: Select(
                ["data", 0],
                Paginate(Var("fileMetaRef_maybe"))
              ),
              link_maybe: Match(
                Index("fileMeta_gitRefs_by_fileMeta_and_gitRef"),
                [Var("fileMetaRef"), Var("gitRefRef")]
              )
            },
            If(
              Exists(Var("link_maybe")),
              "Link Already Exists",
              Let(
                {
                  newLink: Create(Collection("fileMeta_gitRefs"), {
                    data: {
                      gitRefID: Var("gitRefRef"),
                      fileMetaID: Var("fileMetaRef")
                    }
                  })
                },
                "New Link Created"
              )
            )
          ),
          "Stuff does not exists"
        )
      )
    )
  )
}

Variation (input)

It should be simple enough to change to accepting the ID for the gitRef

// ...
Lambda(
  ["commitId", "fileSha"],
  Let(
    {
      gitRefRef_maybe: Ref(Collection("GitRef"), Var("commitId")),
//...
        gitRefRef: Get(Var("gitRefRef_maybe")))
//...

Variation (output)

And the output can be changed to be someother scalar, or you can specify an @embedded payload object that you construct. For example:

enum UpnextResultType {
  SUCCESS
  ERROR
}

type UpnectPayload @embedded {
  type: UpnectResultType!
  message: String
}

type Mutation {
  upnect(commitSha: String!, fileSha: String!): 
      String! @resolver(name: "up-nect")
}
//change
"New Link Created"

//to
{
  type: "SUCCESS",
}
//change
"Link Already Exists"

//to
{
  type: "ERROR",
  message: "Link Already Exists"
}
1 Like

Thanks a lot Paul for your input !
I’m working on having this implemented now :raised_hands:
I’ll let you know how it goes !

So here’s what I came up with.
It’s working properly for my use case. The difference with yours is mainly that I iterate over an array of FileMeta instead of just one.
In the end, I also only return the shas that have been created, not the ones that have been connected.

Thanks again for your help.
Here is my code if it is of any help out there.

Query(
        Lambda(
          ['commitSha', 'filesMeta'],
          Let(
            {
              newGitRef: Create(Collection('GitRef'), {
                data: { commitSha: Var('commitSha') },
              }),
            },
            Filter(
              Map(
                Var('filesMeta'),
                Lambda(
                  ['fileMeta'],
                  Let(
                    {
                      fileMetaRef_maybe: Match(
                        Index('unique_FileMeta_sha'),
                        Select(['sha'], Var('fileMeta')),
                      ),
                    },
                    If(
                      Exists(Var('fileMetaRef_maybe')),
                      Do(
                        Create(Collection('fileMeta_gitRefs'), {
                          data: {
                            gitRefID: Select(['ref'], Var('newGitRef')),
                            fileMetaID: Select(
                              ['data', 0],
                              Paginate(Var('fileMetaRef_maybe')),
                            ),
                          },
                        }),
                        null,
                      ),
                      Let(
                        {
                          newFileMeta: Create(Collection('FileMeta'), {
                            data: Var('fileMeta'),
                          }),
                          link: Create(Collection('fileMeta_gitRefs'), {
                            data: {
                              gitRefID: Select(['ref'], Var('newGitRef')),
                              fileMetaID: Select(['ref'], Var('newFileMeta')),
                            },
                          }),
                        },
                        Select(['data', 'sha'], Var('newFileMeta')),
                      ),
                    ),
                  ),
                ),
              ),
              // only returns the created files sha
              Lambda(['nullOrCreated'], Not(IsNull(Var('nullOrCreated')))),
            ),
          ),
        ),
      )

Also my feedback to have upnect built in the GraphQL engine still stands :wink:

1 Like