1

I started using TypeORM + type-graphql and trying to implement different use cases.

A Queue has a many-to-many relationship with User.

My problem is that I want to retrieve a list of users given a list of queue ids:

Queue Entity

@ObjectType()
@Entity()
export class Queue extends BaseEntity {
  @Field(() => Int)
  @PrimaryGeneratedColumn()
  id!: number

  @Field(() => [User])
  @ManyToMany(() => User, (user) => user.adminOfQueues, { onDelete: "CASCADE" })
  @JoinTable()
  admins!: User[]
}

User Entity

@ObjectType()
@Entity()
export class User extends BaseEntity {
  @Field(() => Int)
  @PrimaryGeneratedColumn()
  id!: number

  @Field(() => [Queue], { nullable: true })
  @ManyToMany(() => Queue, (queue) => queue.admins)
  adminOfQueues!: Queue[]

Queue Resolver

@Resolver(() => Queue)
export class QueueResolver {
  @FieldResolver(() => User)
  async admins(
    @Root() queue: Queue,
    @Ctx() { userFromQueueLoader }: MyContext
  ) {  
    const q = await Queue.findOne(queue.id, { relations: ["admins"] })

    const admins = q.admins?.map?.((user) => user.id)

    return User.find({
      where: {
        id: In(admins),
      },
    })
  }

  @Query(() => Queue)
  async queues(): Promise<Queue> {

    const qb = getConnection()
      .getRepository(Queue)
      .createQueryBuilder("q") // alias
      .orderBy("q.createdAt", "DESC")

    const queues = await qb.getMany()

    return queues
  }

^ works fine, however I would like to use a dataloader because as far as I understood the n+1 problem will bite me when fetching all queues (and the need to query the admin users in the db on each of them separately).

so in the admin FieldResolver I would just use it like:

  @FieldResolver(() => User)
  async admins(
    @Root() queue: Queue,
    @Ctx() { userFromQueueLoader }: MyContext
  ) {
    const users = await userFromQueueLoader.load(queue.id)

    return users
}

However I don't know how to retrieve a list of admin users given a list of queueIds in the dataloader

// [queueId] > batched queueIds
// => [{User}] > should return a list of users where user.adminOfQueues contains a queueId

export const createUserFromQueueLoader = () =>
  new DataLoader<number, User>(async (queueIds) => {
    const users = await User.find({
      join: {
        alias: "userQueue",
        innerJoinAndSelect: {
          adminOfQueues: "userQueue.adminOfQueues",
        },
      },
      where: { adminOfQueues: In(queueIds as number[]) }, // << can't compare 2 arrays, how to achieve that?
    })

    const userIdToUser: Record<number, User> = {}
    users.forEach((user: User) => {
      const usersQueues = user.adminOfQueues
      usersQueues.forEach((userQueue) => {
        const userQueueId = userQueue.id

        userIdToUser[userQueueId] = userIdToUser[userQueueId]
          ? [...userIdToUser[userQueueId], user]
          : [user]
      })
    })

    return queueIds.map((queueId) => userIdToUser[queueId])
  })

Following doesn't help either:

 const users = await User.find({
      relations: ['adminOfQueues'],
      where: { adminOfQueues: In(queueIds as number[]) }, // << can't compare 2 arrays, how to achieve that?
    })

How is this feasable? There must be a way to query all users given a list of ids for the many-to-many relation, no?

Help is highly appreciated :) Thanks

Caruso33
  • 183
  • 1
  • 1
  • 10

1 Answers1

1

instead of loading from User repository, try loading from Queue repository, i.e.

const queues = await Queue.find({
  relations: ['admins'],
  where: { id: In(queueIds) },
})

and then you can extract Users like this:

const queueMap = new Map<number, User[]>()
queues.forEach(queue =>
    queueMap.set(
        queue.id,
        queue.admins
    )
)

return queueIds.map(queueId => queueMap.get(queueId) as User[])

not ideal, but still a lot faster and only one fetch instead of N.

Dharman
  • 30,962
  • 25
  • 85
  • 135
hp10
  • 602
  • 6
  • 11