Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

field projection for explicit many-to-many relations #1023

Open
gex opened this issue Jan 14, 2021 · 3 comments
Open

field projection for explicit many-to-many relations #1023

gex opened this issue Jan 14, 2021 · 3 comments
Labels
scope/projecting Exposing Prisma Models in the GQL API

Comments

@gex
Copy link

gex commented Jan 14, 2021

is there an easy way to add a field projection for an explicit many-to-many relation while hiding the relation table from the graphql schema?

schema.prisma:

model User {
  id        String       @id @default(uuid())
  createdAt DateTime     @default(now())
  updatedAt DateTime?    @updatedAt
  name      String
  roles     UserToRole[]
}

model Role {
  id          String             @id @default(uuid())
  createdAt   DateTime           @default(now())
  updatedAt   DateTime?          @updatedAt
  name        String             @unique
  users       UserToRole[]
}

model UserToRole {
  createdAt DateTime @default(now())
  user      User     @relation(fields: [userId], references: [id])
  userId    String
  role      Role     @relation(fields: [roleId], references: [id])
  roleId    String

  @@id([userId, roleId])
}

for this specific use case (not too many roles) i'd like to see this at the end:

graphql.schema:

type User {
  createdAt: DateTime!
  id: String!
  name: String!
  roles: [Role!]!
  updatedAt: DateTime
}

first i tried to simply add the roles field projection to my User model but it was missing the UserToRole model so it did not work:

const user = objectType({
  name: 'User',
  definition(t) {
    t.model.id()
    t.model.createdAt()
    t.model.updatedAt()
    t.model.name()
    t.model.roles() // error
  }
})

as i didn't want to reveal that relation in the graphql api i solved it by adding a custom field to the User model but i'm not sure this is the best way to add this data, especially because i have to add something similar to the other end of the relationship (to the users field of the Role model) which is repetitive in the long run with many relations:

const user = objectType({
  name: 'User',
  definition(t) {
    t.model.id()
    t.model.createdAt()
    t.model.updatedAt()
    t.model.name()
    t.field('roles', {
      type: nonNull(list(nonNull('Role'))),
      async resolve(root, args, ctx) {
        const userToRoles = await ctx.prisma.userToRole.findMany({
          where: { userId: root.id },
          include: { role: true }
        })
        const roles = userToRoles.map((userToRole) => userToRole.role)
        return roles
      }
    })
  }
})

const role = objectType({
  name: 'Role',
  definition(t) {
    t.model.id()
    t.model.createdAt()
    t.model.updatedAt()
    t.model.name()
    t.field('users', {
      type: nonNull(list(nonNull('User'))),
      async resolve(root, args, ctx) {
        const userToRoles = await ctx.prisma.userToRole.findMany({
          where: { roleId: root.id },
          include: { user: true }
        })
        const users = userToRoles.map((userToRole) => userToRole.user)
        return users
      }
    })
  }
})

while this solves the problem temporarily it doesn't seem like the best idea when i have a lot of data. and i still have to figure out how to reuse and add the auto-generated query arguments as field arguments here to have a schema like this:

type User {
  # ...
  roles(
    after: RoleWhereUniqueInput
    before: RoleWhereUniqueInput
    first: Int
    last: Int
    orderBy: [RoleOrderByInput!]
    where: RoleWhereInput
  ): [Role!]!
}

what's the best way to do this?

@gex
Copy link
Author

gex commented Jan 14, 2021

well, i gave it another shot and tried field projections for implicit and explicit relations again.

prisma.schema:

# implicit relation

model ImplicitA {
  id         String      @id @default(uuid())
  name       String
  implicitBs ImplicitB[]
}

model ImplicitB {
  id         String      @id @default(uuid())
  name       String
  implicitAs ImplicitA[]
}

# explicit relation

model ExplicitA {
  id         String                 @id @default(uuid())
  name       String
  explicitBs ExplicitAToExplicitB[]
}

model ExplicitB {
  id         String                 @id @default(uuid())
  name       String
  explicitAs ExplicitAToExplicitB[]
}

model ExplicitAToExplicitB {
  explicitA   ExplicitA @relation(fields: [explicitAId], references: [id])
  explicitAId String
  explicitB   ExplicitB @relation(fields: [explicitBId], references: [id])
  explicitBId String

  @@id([explicitAId, explicitBId])
}

my models:

// implicit relation

const implicitA = objectType({
  name: 'ImplicitA',
  definition(t) {
    t.model.id()
    t.model.name()
    t.model.implicitBs()
  }
})

const implicitB = objectType({
  name: 'ImplicitB',
  definition(t) {
    t.model.id()
    t.model.name()
    t.model.implicitAs()
  }
})

// explicit relation

const explicitA = objectType({
  name: 'ExplicitA',
  definition(t) {
    t.model.id()
    t.model.name()
    t.model.explicitBs()
  }
})

const explicitB = objectType({
  name: 'ExplicitB',
  definition(t) {
    t.model.id()
    t.model.name()
    t.model.explicitAs()
  }
})

const explicitAToExplicitB = objectType({
  name: 'ExplicitAToExplicitB',
  definition(t) {
    t.model.explicitA()
    t.model.explicitB()
  }
})

and my queries:

// implicit relation

const implicitA = extendType({
  type: 'Query',
  definition(t) {
    t.crud.implicitA()
    t.crud.implicitAs()
  }
})

const implicitB = extendType({
  type: 'Query',
  definition(t) {
    t.crud.implicitB()
    t.crud.implicitBs()
  }
})

// explicit relation

const explicitA = extendType({
  type: 'Query',
  definition(t) {
    t.crud.explicitA()
    t.crud.explicitAs()
  }
})

const explicitB = extendType({
  type: 'Query',
  definition(t) {
    t.crud.explicitB()
    t.crud.explicitBs()
  }
})

this way i can send the following queries to the graphql api:

query ImplicitAs {
  name
  explicitBs {
    name
  }
}

query ExplicitAs {
  name
  explicitBs {
    explicitB {
      name
    }
  }
}

i tried to override the resolve function (t.mode.explicitBs({ resolve: ... }) but the type can be ExplicitAToExplicitB or BatchPayload only so i was't able to hide the connection and use the field projection at the same time.

what's also not clear is how can i order (and maybe filter) the related nodes with these field projections. pagination is there by default as i can send implicitBs(first: 3) { name } in both cases but without the ability to order the paginated data it's not that useful for me.

another great feature would be adding some metadata to the explicit relation, for example the number of nodes. this is probably offtopic here because the batch read of t.crud doesn't provide this either.

@jasonkuhrt jasonkuhrt added the scope/projecting Exposing Prisma Models in the GQL API label Jan 22, 2021
@RomanTsegelskyi
Copy link

Any updates on this? Or will be resolved in nexus-prisma?

@gex
Copy link
Author

gex commented Aug 30, 2021

@RomanTsegelskyi sorry for the late reply, this project is dead as per #1039. you can find the new plugin here:
prisma/nexus-prisma (still in development)

n:n relations are in the midterm while crud operations are in the longterm goals. initially they estimated the new plugin end of q1/early q2 but tbh i wouldn't expect these features to be done this year.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
scope/projecting Exposing Prisma Models in the GQL API
Projects
None yet
Development

No branches or pull requests

3 participants