realm-graphql-service icon indicating copy to clipboard operation
realm-graphql-service copied to clipboard

Add mutation support for relationships

Open astigsen opened this issue 7 years ago • 10 comments
trafficstars

When updating objects, you often need to set individual relations, but that is currently quite complex as the API only allows you to create new objects. This is especially problematic with lists, as you might want to do specific operations (adding, removing or moving entries), and you currently can only do that by re-creating the entire list.

There are two subproblems here:

  1. How to set relations by id (primary key) as opposed to having to create a new object (also essential if you have circular relations).
  2. How to do granular updates (add, remove, move) within lists.

For the first problem, I propose adding an argument to the property fields during that specifies that it should interpret the value as an object reference (primary key):

const mutation = gql`
  mutation {
    result: addCompany(input: {
      companyId: "some-unique-id"
      owner(byId: true): "personId"
    }) {
      companyId
      owner
    }
  }`;

For the second problem of supporting granular updates of lists, I propose a second set of arguments that you can apply to list properties:

const append = gql`
  mutation {
    result: updateCompany(input: {
      companyId: "some-unique-id"
      employees(append: true, byId: true): "personId"
    }) {
      companyId
      employees
    }
  }`;

const insert = gql`
  mutation {
    result: updateCompany(input: {
      companyId: "some-unique-id"
      employees(insertAt: 2, byId: true): "personId"
    }) {
      companyId
      employees
    }
  }`;

const remove = gql`
  mutation {
    result: updateCompany(input: {
      companyId: "some-unique-id"
      employees(remove: true): 0
    }) {
      companyId
      employees
    }
  }`;

const move = gql`
  mutation {
    result: updateCompany(input: {
      companyId: "some-unique-id"
      employees(moveTo: 0): 10
    }) {
      companyId
      employees
    }
  }`;

Note that the last two (remove and move) uses the position in the list as value. If applicable it could also use byId, which would select the first entry matching that id (since it is possible for a list to contain duplicates).

astigsen avatar Jan 22 '18 21:01 astigsen

Nice! Should we also expose the object ID in Realm as a property on all objects so that it is then easy to reference the ID. This is the PK when there is one but perhaps would simplify thinking about what is the ID in the GraphQL syntax?

bigfish24 avatar Jan 22 '18 21:01 bigfish24

I'd be very surprised if this were supported GraphQL syntax. As far as I remember, GraphQL objects can only define fields and not what seems like a method. While we can definitely extend the syntax and parse it (and it does look much nicer than what I am going to propose), I imagine it would take a serious amount of effort.

An alternative approach that seems like it will be easier to implement would be something like:

const mutation = gql`
  mutation {
    result: setRelationship(input: {
      objectType: "Car"
      objectId: 1 // alternatively this could be PK
      propertyName: "Driver"
      relatedObjectId: 2 // This could be optional and if set to null, remove the link
    }) {
      brand
      driver {
          name
      }
    }
  }`;

Similarly for lists:

const append = gql`
  mutation {
    result: insertInList(input: {
      objectType: "Person"
      objectId: 1 // alternatively this could be PK
      propertyName: "Cats"
      relatedObjectId: 2
      index: 3 // Optional
    }) {
      ...
    }
  }`;

and so on. This looks uglier and will likely require batching of mutations but seems feasible with the current architecture.

nirinchev avatar Jan 22 '18 21:01 nirinchev

Should we also expose the object ID in Realm as a property on all objects

That should be the end goal. But the current API's for working with the internal object id's is a bit clunky (and private), so we probably need a bit of design work first.

astigsen avatar Jan 22 '18 21:01 astigsen

As far as I remember, GraphQL objects can only define fields and not what seems like a method.

According to http://graphql.org/learn/queries/#arguments it is possible to define arguments on individual properties.

astigsen avatar Jan 22 '18 21:01 astigsen

My understanding was that this was only possible for return types (hence it's in the queries section), not for input types (GraphQL makes a difference between those). For example here it says:

Input types can't have fields that are other objects, only basic scalar types, list types, and other input types.

nirinchev avatar Jan 22 '18 21:01 nirinchev

You might be right. If that is the case I guess we will have to go with the more heavy approach of defining individual mutations ☹️

astigsen avatar Jan 22 '18 22:01 astigsen

Do you prefer that to the dynamic API I proposed? I guess my main motivation for suggesting them is that right now we don't do any preprocessing of the inputs - we just proxy them to realm-js. If we included the fields as part of the input schema, e.g.:

const mutation = gql`
  mutation {
    result: addCompany(input: {
      companyId: "some-unique-id"
      ownerId: "personId" // Equivalent to owner(byId: true) from your example
    }) {
      companyId
      owner
    }
  }`;

we would:

  1. Need to preprocess that - validate the object against the schema, find all fake properties that we added, remove them and set the actual relationship properties with the objects looked up from fake ones.
  2. Potentially introduce conflicts with real user properties (e.g. what happens if a user already had ownerId in their schema?).

The main benefit I can see is that we get "strong" typing and validation (e.g. users can't put any random value).

nirinchev avatar Jan 22 '18 22:01 nirinchev

If we have to go in that direction, I think that the first API you proposed is preferable. Overloading the property names seems a bit inflexible and brittle.

astigsen avatar Jan 23 '18 18:01 astigsen

So is it possible to add relation on mutation ?

EG: I would like to create a product with an existing supplier

mutation addProduct($input: ProductInput) {
  addProduct(input: $input) {
    name,
    id
  }
}

variable:

{
"input": {
  "id": "f988da1d-65ee-4c4e-97a1-63bad5f1ef22",
   "name": "new product cedric",
  "supplier": {  "id":"f480da1d-65ee-4c4e-97a1-63bad5f1ef22" },
}

This throws the error Attempting to create an object of type 'Supplier' with an existing primary key value ''f480da1d-65ee-4c4e-97a1-63bad5f1ef22''.

cedvdb avatar May 07 '18 09:05 cedvdb

If your objects have primary keys, you should be able to use updateProduct rather than addProduct. This is the only supported approach currently.

nirinchev avatar May 07 '18 10:05 nirinchev