Thomas Dittmar
Thomas Dittmar

Reputation: 1914

Is it possible to do a multi tenancy with Graphql and Sequelize?

I have a rather tricky question about GraphQl and multi-tenancy.

Let's assume there are 3 tables, OWNER, HOUSE and TENANTS. I will describe them in Sequelize and GraphQl pseudo code:

Owner table (has multiple houses and multiple tenants)

const OWNER = sequelize.define('owner', {
  ownerId: type: Sequelize.INTEGER,
  name: type: Sequelize.STRING
}

OWNER.associate = models => {
  models.owner.hasMany(models.house, {foreignKey: {name: 'ownerId', field: 'ownerId'}})
  models.owner.hasMany(models.tenant, {foreignKey: {name: 'ownerId', field: 'ownerId'}})
}

House table (belongs to owner and has multiple tenants)

const HOUSE = sequelize.define('house', {
  houseId: type: Sequelize.INTEGER,
  ownerId: type: Sequelize.INTEGER,
  name: type: Sequelize.STRING
}

HOUSE.associate = models => {
  models.house.belongsTo(models.owner, {foreignKey: {name: 'ownerId', field: 'ownerId'}})
  models.house.hasMany(models.tenant, {foreignKey: {name: 'houseId', field: 'houseId'}})
}

Tenant table (belongs to owner and house)

const TENANT = sequelize.define('tenant', {
  tenantId: type: Sequelize.INTEGER,
  ownerId: type: Sequelize.INTEGER,
  houseId: type: Sequelize.INTEGER,
  name: type: Sequelize.STRING
}

TENANT.associate = models => {
  models.tenant.belongsTo(models.owner, {foreignKey: {name: 'ownerId', field: 'ownerId'}})
  models.tenant.belongsTo(models.house, {foreignKey: {name: 'houseId', field: 'houseId'}})
}

The owner graphql object

const OwnerType = new GraphQLObjectType({
  name: 'Owner',
  fields: () => ({
    ownerId: { type: GraphQLInt },
    name: { type: GraphQLString },
    houses: {
      type: GraphQLList(HouseType),
      resolve(owner) {
        return owner.getHouse()
      }
    },
    houseById: {
      type: HouseType,
      args: <args is not defined>
      resolve(owner) {
        return <???>
      }
    },
  })
})

Here are a few simple GraphQL queries:

ownerById = {
  type: OwnerType,
  args: {
    ownerId: { type: GraphQLInt },
  },
  resolve(parents, args){
    return models.owner.findOne({ where: args })
  }
}

houses = {
  type: GraphQLList(HouseType),
  resolve(parents, args){
    return models.house.findAll()
  }
}

houseById = {
  type: HouseType,
  args: {
    houseId: { type: GraphQLInt },
  },
  resolve(parents, args){
    return models.house.findOne({ where: args })
  }
}

tenants = {
  type: GraphQLList(TenantType),
  resolve(parents, args){
    return models.tenant.findAll()
  }
}

These client queries work:

{
  ownerById(ownerId: 1) {
    ownerId
    name
    house {
      houseId
      name
    }
  }
}

{
  houseById(houseId: 2) {
    houseId
    name
    tenant {
      tenantId
      name
    }
  }
}

What I need to make multi-tenancy to work is something like that:

{
  ownerById(ownerId: 1) {
    ownerId
    name
    houseById(houseId: 2) {
      houseId
      name
      tenant {
        tenantId
        name
      }
    }
  }
}

Is there a way to archive this or is that out of scope what GraphQl can do?

If yes, how would the graphql object houseById query look like?

Thanks in advance.

Upvotes: 0

Views: 1514

Answers (1)

Daniel Rearden
Daniel Rearden

Reputation: 84697

Unless I'm missing something, it seems like your resolver for houseById would not be that different from the resolver for the houses field on the same type.

houseById: {
  type: HouseType,
  args: {
    houseId: { type: GraphQLInt },
  },
  async resolve(owner, { houseId }) {
    const houses = await owner.getHouses({ where: { id: houseId } })
    return houses[0]
  }
},

For a HasMany association, the getter for the target model resolves to an array of instances. So we need to grab that array first and then return just the first item in it, since our field represents a single object and not a list. If you don't want to use async/await, you can also do:

return owner.getHouses({ where: { id: houseId } })
  .then(houses => houses[0])

It would also be worth mentioning that this sort of pattern for a schema defies convention. Rather than having a houses field, a houseById field, a houseBySomeOtherArg field, etc., consider exposing a single houses field with one or more arguments like id, name or whatever filter criteria you want to provide. Your field can then just filter the houses based on whatever arguments are passed in, or return all results if no filter arguments were provided.

Upvotes: 1

Related Questions