0
votes

I'm having trouble understanding how to retrieve information from a GraphQL Union. I have something in place like this:

const Profile = StudentProfile | TeacherProfile

Then in my resolver I have:

  Profile: {
    __resolveType(obj, context, info) {
      if (obj.studentId) {
        return 'StudentProfile'
      } else if (obj.salaryGrade) {
        return 'TeacherProfile'
      }
    },
  },

This doesn't throw any errors, but when I run a query like this:

query {
  listUsers {
    id
    firstName
    lastName
    email
    password
    profile {
      __typename
      ... on StudentProfile {
        studentId
      }
      ... on TeacherProfile {
         salaryGrade
     }
    }
  }
}

This returns everything except for profile which just returns null. I'm using Sequelize to handle my database work, but my understanding of Unions was that it would simply look up the relevant type for the ID being queried and return the appropriate details in the query.

If I'm mistaken, how can I get this query to work?

edit:

My list user resolver:

const listUsers = async (root, { filter }, { models }) => {
  const Op = Sequelize.Op
  return models.User.findAll(
    filter
      ? {
          where: {
            [Op.or]: [
              {
                email: filter,
              },
              {
                firstName: filter,
              },
              {
                lastName: filter,
              },
            ],
          },
        }
      : {},
  )
}

User model relations (very simple and has no relation to profiles):

User.associate = function(models) {
    User.belongsTo(models.UserType)
    User.belongsTo(models.UserRole)
  }

and my generic user resolvers:

User: {
    async type(type) {
      return type.getUserType()
    },

    async role(role) {
      return role.getUserRole()
    },
  },
1
Your resolveType method is fine -- the problem likely lies with either the resolver or the model. Please share the code for both.Daniel Rearden
Hi @DanielRearden - thanks for the reply (again!) I've added my resolvers and the model code :)roo
It's not really clear where profile would be coming from if you have no relation to a model for it.Daniel Rearden
Ah that makes sense, so in that respect I'd need to add relations to StudentProfile, TeacherProfile etc in my User model?roo
Yes, although querying multiple models and then combining the results can be bothersome. You might consider using a single model in front of either a single table or a view that aggregates multiple tables.Daniel Rearden

1 Answers

1
votes

The easiest way to go about this is to utilize a single table (i.e. single table inheritance).

  • Create a table that includes columns for all the types. For example, it would include both student_id and salary_grade columns, even though these will be exposed as fields on separate types in your schema.
  • Add a "type" column that identifies each row's actual type. In practice, it's helpful to name this column __typename (more on that later).
  • Create a Sequelize model for your table. Again, this model will include all attributes, even if they don't apply to a specific type.
  • Define your GraphQL types and your interface/union type. You can provide a __resolveType method that returns the appropriate type name based on the "type" field you added. However, if you named this field __typename and populated it with the names of the GraphQL types you are exposing, you can actually skip this step!
  • You can use your model like normal, utilizing find methods to query your table or creating associations with it. For example, you might add a relationship like User.belongsTo(Profile) and then lazy load it: User.findAll({ include: [Profile] }).

The biggest drawback to this approach is you lose database- and model-level validation. Maybe salary_grade should never be null for a TeacherProfile but you cannot enforce this with a constraint or set the allowNull property for the attribute to false. At best, you can only rely on GraphQL's type system to enforce validation but this is not ideal.

You can take this a step further and create additional Sequelize models for each individual "type". These models would still point to the same table, but would only include attributes specific to the fields you're exposing for each type. This way, you could at least enforce "required" attributes at the model level. Then, for example, you use your Profile model for querying all profiles, but use the TeacherProfile when inserting or updating a teacher profile. This works pretty well, just be mindful that you cannot use the sync method when structuring your models like this -- you'll need to handle migrations manually. You shouldn't use sync in production anyway, so it's not a huge deal, but definitely something to be mindful of.