2
votes

I'm facing a problem where I need to reference a resolved field on the parent from inside the __resolveType. Unfortunately the field I need to reference did not come as part of the original api response for the parent, but from another field resolver, which I would not have though mattered, but indeed it does, so it is undefined.

But I need these fields (in this example the; obj.barCount and obj.bazCount) to be able to make the following query, so I've hit a dead end. I need them to be available in the resolveType function so that I can use them to determine what type to resolve in case this field is defined.

Here's an example:

The graphql query I wish to be able to make:

{
  somethings { 
    hello
    ... on HasBarCount {
      barCount
    }
    ... on HasBazCount {
      bazCount
    }
  }
}

Schema:

type ExampleWithBarCount implements Something & HasBarCount & Node {
  hello: String!
  barCount: Int
}

type ExampleWithBazCount implements Something & HasBazCount & Node {
  hello: String!
  bazCount: Int
}

interface Something {
  hello: String!
}

interface HasBarCount {
  barCount: Int
}

interface HasBazCount {
  bazCount: Int
}

Resolvers:

ExampleWithBarCount: {
  barCount: (obj) => {
    return myApi.getBars(obj.id).length || 0
  }
}

ExampleWithBazCount {
  bazCount: (obj) => {
    return myApi.getBazs(obj.id).length || 0
  }
}

Problem:

Something: {
  __resolveType(obj) {
    console.log(obj.barCount) // Problem: this is always undefined
    console.log(obj.bazCount) // Problem: this is always undefined

    if (obj.barCount) {
      return 'ExampleWithBarCount';
    }

    if (obj.bazCount) {
      return 'ExampleWithBazCount';
    }

    return null;
  }
}

Any ideas of alternative solutions or what am I missing?

Here's a little more about the use case.

In the database we have a table "entity". This table is very simple and only really important columns are id, parent_id, name. type, and then you can of course attach some additional metadata to it.

Like with "entity", types are created dynamically from within the backend management system, and aftewards you can assign a type to your concrete entity.

The primary purpose of "entity" is to establish a hierarchy / tree of nested entities by parent_id and with different "types" (in the type column of entity). There will be some different meta data, but let's not focus on that.

Note: entity can be named anything, and the type can be anything.

In the API we then have an endpoint where we can get all entities with a specific type (sidenote: and in addition to the single type on an entitiy we also have an endpoint to get all entities by their taxonomy/term).

In the first implementation I modeled the schema by adding all the "known" types I had in my specification from the UX'er during development. The tree of entities could be like eg.

  • Company (or Organization, ..., Corporation... etc)
    • Branch (or Region, ..., etc)
    • Factory (or Building, facility, ..., etc)
      • Zone (or Room, ..., etc)

But this hierarchy is just one way it could be done. The naming of each might be totally different, and you might move some of them a level up or down or not have them at all, depending on the use case.

Only thing that is set in stone is that they share the same database table, will have the type column/field defined and they may or may not have children. The bottom layer in the hierarchy will not have children, but machines instead. The rest of just diffent metadata, which I think we should ignore for to not complicate this further.

As you can see the hierarchy needs to be very flexible and dynamic, so I realized it wasn't a great solution I had begun on.

At the lowest level "Zone" in this case, there will need to be a "machines" field, which should return a list of machines (they are in a "machines" table in the db, and not part of the hierarchy, but simply related with an "entity_id" on the "machines" table.

I had schema types and resolvers for all in the above hierarchy: Organization, Branch, Factory, Zone etc, but I was for the most part just repeating myself, so I thought I could turn to interfaces to try to generalize this more.

So instead of doing

{
  companies{
    name
    branchCount
    buildingCount
    zoneCount
    branches {
      name
      buildingCount
      zoneCount
      buildings {
        name
        zoneCount
        zones {
          name
          machines {
            name
          } 
        }
      }
    }
  }
}

And having to add schema/resolvers for all the different namings of the entities, I thought this would work:

{
  entities(type: "companies") { 
    name
    ... on HasEntityCount {
      branchCount: entityCount(type: "branch")
      buildingCount: entityCount(type: "building")
      zoneCount: entityCount(type: "zone")
    }
    ... on HasSubEntities {
      entities(type: "branch") {
        name
        ... on HasEntityCount {
          buildingCount: entityCount(type: "building")
          zoneCount: entityCount(type: "zone")
        }
        ... on HasMachineCount {
          machineCount
        }
        ... on HasSubEntities {
          entities(type: "building") {
            name
            ... on HasEntityCount {
              zoneCount: entityCount(type: "zone")
            }
            ... on HasMachineCount {
              machineCount
            }
            ... on HasSubEntities {
              entities(type: "zone") {
                name
                ... on HasMachines {
                  machines
                }
              }
            }
          }
        }
      }
    }
  }
}

With the interfaces being:

interface HasMachineCount {
  machineCount: Int
}

interface HasEntityCount  {
  entitiyCount(type: String): Int
}

interface HasSubEntities {
  entities(
    type: String
  ): [Entity!]
}

interface HasMachines {
  machines: [Machine!]
}

interface Entity {
  id: ID!
  name: String!
  type: String!
}

The below works, but I really want to avoid a single type with lots of optional / null fields:

type Entity {
  id: ID!
  name: String!
  type: String!
  # Below is what I want to avoid, by using interfaces
  # Imagine how this would grow
  entityCount
  machineCount
  entities
  machines
}

In my own logic I don't care what the entities are called, only what fields expected. I'd like to avoid a single Entity type with alot of nullable fields on it, so I thought interfaces or unions would be helpful for keeping things separated so I ended up with HasSubEntities, HasEntityCount, HasMachineCount and HasMachines since the bottom entity will not have entities below, and only the bottom entity will have machines. But in the real code there would be much more than the 2, and it could end up with a lot of optional fields, if not utilizing interfaces or unions in some way I think.

1
Have you tried storing a __typename field on each entity type? If an entity has a __typename field on it & implements an interface Graphql can infer what type to return. This would mean you would have to couple your Graphql logic with data access. Although you can use something like 'Virtuals'... you won't be able to get out of specifying what type each entity is.DanStarns

1 Answers

2
votes

There's two separate problems here.

One, GraphQL resolves fields in a top down fashion. Parent fields are always resolved before any children fields. So it's never possible to access the value that a field resolved to from the parent field's resolver (or a "sibling" field's resolver). In the case of fields with an abstract type, this applies to type resolvers as well. A field type will be resolved before any children resolvers are called. The only way to get around this issue is to move the relevant logic from the child resolver to inside the parent resolver.

Two, assuming the somethings field has the type Something (or [Something], etc.), the query you're trying to run will never work because HasBarCount and HasBazCount are not subtypes of Something. When you tell GraphQL that a field has an abstract type (an interface or a union), you're saying that what's returned by the field could be one of several object types that will be narrowed down to exactly one object type at runtime. The possible types are either the types that make up the union, or types that implement the interface.

A union may only be made up of object types, not interfaces or other unions. Similarly, only an object type may implement an interface -- other interfaces or unions may not implement interfaces. Therefore, when using inline fragments with a field that returns an abstract type, the on condition for those inline fragments will always be an object type and must be one of the possible types for the abstract type in question.

Because this is pseudocode, it's not really clear what business rules or use case you're trying to model with this sort of schema. But I can say that there's generally no need to create an interface and have a type implement it unless you're planning on adding a field in your schema that will have that interface as its type.

Edit: At a high level, it sounds like you probably just want to do something like this:

type Query {
  entities(type: String!): [Entity!]!
}

interface Entity {
  type: String!
  # other shared entity fields
}

type EntityWithChildren implements Entity {
  type: String!
  children: [Entity!]!
}

type EntityWithModels implements Entity {
  type: String!
  models: [Model!]!
}

The type resolver needs to check for whether we have models, so you'll want to make sure you fetch the related models when you fetch the entity (as opposed to fetching them inside the models resolver). Alternatively, you may be able to add some kind of column to your db that identifies an entity as the "lowest" in the hierarchy, in which case you can just use this property instead.

function resolveType (obj) {
  return obj.models ? 'EntityWithModels' : 'EntityWithChildren'
}

Now your query looks like this:

entities {
  type
  ... on EntityWithModels {
    models { ... }
  }
  ... on EntityWithChildren {
    children {
      ... on EntityWithModels {
        models { ... }
      }
      ... on EntityWithChildren {
        # etc.
      }
    }
  }
}

The counts are a bit trickier because of the variability in the entity names and the variability in the depth of the hierarchy. I would suggest just letting the client figure out the counts once it gets the whole graph from the server. If you really want to add count fields, you'd have to have fields like childrenCount, grandchildrenCount, etc. Then the only way to populate those fields correctly would be to fetch the whole graph at the root.