How to avoid `dependency cycle` between modules when implementing GraphQL resolvers


#1

Hi,

I am trying to define my graphql schema/resolvers using Reason and I have a problem related to dependency cycle that I don’t know how to solve.

ninja: error: dependency cycle: src/User.cmj -> src/Project.cmj -> src/User.cmj

Obviously I have a cyclic dependency graph. I have checked some OCaml resources and found some ways that could solve this problem (recursive modules, functors, :sweat_smile) but they seem over complicated and not recommended!

My GraphQL schema is really simple so I am not sure If the only option to solve this problem is to use recursive modules or not.

There is a User with a reference to its projects and a Project which also has a reference to the user that created it, i.e. users have projects and each project is assigned to a user.

GraphQL schema (I put it together here for simplicity but it is defined as a multiline string on the reason modules )

type User {
  id: Int!
  name: String!
  projects: [Project!]
}

type Project {
  id: Int!
  name: String!
  user: User!
}

Here are my modules:

User.re

type t = {
  .
  "id": int,
  "name": string,
};

let typeDef = {|
  type User {
    id: Int!
    name: String
    projects: [Project!]
  }
|};

/* Here is where the problem lies, I need a reference to the Project module types and methods! */
type resolvers = {
  .
  "projects": t => array(Project.t),
};

let resolvers = {
  "projects": (user: t) =>
    Js.Array.filter(
      project => project##userId === user##id,
      Project.projects,
    ),
};

/* mock some users */
let users = [|{"id": 1, "name": "Alistair"}, {"id": 2, "name": "Ariel"}|];

Project.re

type t = {
  .
  "id": int,
  "name": string,
  "userId": int,
};

let typeDef = {|
  type Project {
    id: Int!
    name: String!
    user: User!
  }
|};

type resolvers = {
  .
  "user": t => Js.Nullable.t(User.t),
};

let resolvers: resolvers = {
  "user": project =>
    switch (Js.Array.find(user => user##id === project##userId, User.users)) {
    | None => Js.Nullable.undefined
    | Some(p) => Js.Nullable.return(p)
    },
};

/* mock some projects */
let projects = [|
  {"id": 1, "userId": 1},
  {"id": 2, "userId": 1},
  {"id": 3, "userId": 2},
|];

I appreciate any kind of help in advance! :slight_smile:

Thank you!
Ariel.


#2

I usually put all of them in the same module to avoid this situation. Not ideal but dependency cycle is a hard problem


#3

I’ve usually found that a dependency cycle error meant there was a better way of modelling the types, but in this case, as @thangngoc89 suggested, moving everything into the same module seems to be the only way to go.

You also have to be careful about value semantics. I don’t know how GraphQL models data, but if this were plain records, it won’t be very ergonomic to create a project that references a user which references a project which in turn references the same user.

This is because there are no “memory references” in immutable value-based programming. It is easier to do this instead by using identifiers (like user_id or project_id) in one of the types, and look up the values when you need them, or memoize it as part of the type itself in case time is more expensive.

Great talk by Rich Hickey to value-based programming for those of us who come from an imperative place-based programming background: https://www.infoq.com/presentations/Value-Values

But if you do want to use cyclic data without mutation, then the section “Cyclic Data Structures” in http://dev.realworldocaml.org/imperative-programming.html#scrollNav-4 has some pointers on using recursive lets to model that.

eg:

type user = {
  user_name: string,
  project,
}
and project = {
  project_name: string,
  users: list(user),
};

let rec u = {user_name: "abcd", project: p}
and p = {project_name: "xyz", users: [u]};
Js.log(u.user_name);
Js.log(p.project_name);

So far so good, but if you try Js.log(p.users) or Js.log(u.projects) it’ll raise an exception. There might be a way to access those values, but it is guaranteed to be deeply unergonomic.


#4

I had the same idea but I wasn’t eager to follow that path because my GraphQL schema has around 15 non scalar types and it will look quite messy very soon :-). Anyhow I am going to try this one module approach and refactor if needed!. Thanks for your help!


#5

Instead of putting them together you can got the other way around, and separate them even more so that cycle dependency does not happen.

You could have: UserResolvers, ProjectResolver and User(Data?), Project(Data)
Then you would have clean dependencies
UserResolvers -> UserData, ProjectData
ProjectResolvers -> UserData, ProjectData

Resolvers would know how where to get data from and have some glue, Data modules would own the actual type and serve as Data loaders, fetch from DB, API or object…

Not sure if possible but you might be able to do User.Resolvers and User.Data to make it a bit cleaner.