graphql-nexus / nexus

Code-First, Type-Safe, GraphQL Schema Construction
https://nexusjs.org
MIT License
3.4k stars 275 forks source link

Nexus Many-to-Many and objectType definition error: ts(2322) Error: "The expected type comes from property 'resolve' which is declared here on type 'NexusOutputFieldConfig<"Query", "allUsers"> & { resolve: FieldResolver<"Query", "allUsers">; }' #914

Closed raleigh9123 closed 3 years ago

raleigh9123 commented 3 years ago

I am trying to build something akin to a CMS. So far, all I am trying to setup are clients and users via an EXPLICIT many-to-many relationship. In my nexus defined schema where I implement resolvers, I run in to errors in my VS Code console.

I am unsure whether these errors are from conflicts via my nexus "objectType()" properties, or whether I am incorrectly writing Query/Mutation resolvers from my defined objectType() definitions.

My prisma.schema is as follows:

model Client {
  id         Int             @unique @default(autoincrement())
  uuid       String          @id @default(uuid())
  createdAt  DateTime        @default(now()) @map("created_at")
  updatedAt  DateTime        @updatedAt @map("updated_at")
  clientName String
  email      String          @unique
  users      UsersOnClient[]
}

model User {
  id                 Int             @unique @default(autoincrement())
  uuid               String          @id @default(uuid())
  createdAt          DateTime        @default(now()) @map("created_at")
  updatedAt          DateTime        @updatedAt @map("updated_at")
  firstName          String?
  lastName           String?
  email              String          @unique
  password           String
  status             Status          @default(ACTIVE)
  role               Role            @default(USER)
  profile            Profile?
  clients            UsersOnClient[]

  @@map(name: "users")
}

model UsersOnClient {
  client    Client   @relation(fields: [clientId], references: [uuid])
  clientId  String // relation scalar field (used in the `@relation` attribute above)
  user      User     @relation(fields: [userId], references: [uuid])
  userId    String // relation scalar field (used in the `@relation` attribute above)
  createdAt DateTime @default(now())

  @@id([clientId, userId])
}

enum Role {
  USER   @map("user")
  ADMIN  @map("admin")
  OWNER  @map("owner")

  @@map("role")
}

enum Status {
  ACTIVE
  INACTIVE

  @@map("status")
}

My User.ts/Client.ts files are imported to my schema.ts where I then import User.ts and Client.ts as types and run the makeSchema() function.

// User.ts --> I did write an "include: { NESTED READ }" to retrieve the information for the explicit (m-n) relationship, however, I am unsure if this is how to properly select this field from 'USERS' or if I should write an additional interface to reflect the schema.prisma "UsersOnClient" model.

/* USER GQL SCHEMA
*
* This is a 1-1 (1 - n) relationship between a user and profile.
* Each user is a many to many (m - n) relationship between a user and a client.
* Typically each user only has one client, but in some exceptions may belong to more than one client.
*/
export const User = objectType({
  name: 'User',
  definition(t) {
    t.nonNull.int('id', { description: 'User id by integer. Auto-generated and auto-incremented'})
    t.nonNull.string('uuid', { description: 'User id by string. Auto-generated and auto-incremented. Machine-readable only'})
    t.nonNull.field('createdAt', { type: 'DateTime', description: 'User created at date' })
    t.nonNull.field('updatedAt', { type: 'DateTime', description: "User updated at date" })
    t.nonNull.string('firstName', { description: 'User First Name'})
    t.nonNull.string('lastName', { description: 'User Last Name'})
    t.nonNull.string('email', { description: "User's full email address"})
    t.nonNull.field('status', { type: UserStatusEnum, description: "Membership is active or inactive."})
    t.nonNull.list.field('clients', { type: Client, description: "This is a list of clients that this user may belong to. Typically users have one client, but with a few exceptions may belong to more than one." })
    t.nonNull.field('role', { type: UserRoleEnum, description: "User role for application permissions"})
    t.field('profile', { type: Profile, description: 'User profile with additional user information'})
  }
})

/* PROFILE GQL SCHEMA
*
* This is a 1-1 (1 - n) relationship between a profile and a user.
*/
export const Profile = objectType({
  name: 'Profile',
  definition(t) {
    t.nonNull.int('id', { description: 'Profile id by integer. Integer matches profile creation.'})
    t.nonNull.string('uuid', { description: 'Profile Prisma-generated UUID. Not intended to be human readable.'})
    t.string('address', { description: 'User address number'})
    t.string('city', { description: 'User city'})
    t.string('state', { description: 'User state'})
    t.int('zip', { description: 'User zip code'})
    t.field('country', { type: CountryCodeEnum , description: 'User country'})
    t.string('phoneNumber', { description: 'User cell phone number'})
    t.string('whatsApp', { description: 'User WhatsApp number'})
    t.field('dateOfBirth', { type: 'DateTime', description: "User birthday"})
    t.string('emergencyContact', { description: "User emergency contact first name and last name"})
    t.string('emergencyPhoneNumber', { description: "User emergency contact phone number"})
  }
})

//* * * * USER QUERIES * * * * /
export const UserQuery = extendType({
  type: 'Query',

  definition(t) {
    t.nonNull.list.nonNull.field('allUsers', {
      type: 'User',
      description: 'Return all users. Allows filtering, pagination, and sorting.',
      args: {
        searchString: stringArg(),
        skip: intArg(),
        take: intArg(),
        orderBy: arg({
          type: 'OrderBy',
        }),
      },
      resolve: (_, args, context: Context) => {
        const or = args.searchString ? {
            OR: [
              { firstName: { contains: args.searchString } },
              { email: { contains: args.searchString } },
            ],
          } : {}

        return context.prisma.user.findMany({
          where: {
            ...or,
          },
          include: {
            profile: true,
            clients: {
              include: {
                client:true
              }
            }
          },
          take: args.take || undefined,
          skip: args.skip || undefined,
          orderBy: args.orderBy || undefined,
        })
      }
    })
})

export const SortOrder = enumType({
  name: 'SortOrder',
  members: ['asc', 'desc'],
})

export const OrderBy = inputObjectType({
  name: 'OrderBy',
  definition(t) {
    t.nonNull.field('createdAt', { type: 'SortOrder' })
    t.nonNull.field('updatedAt', { type: 'SortOrder' })
    t.nonNull.field('id', { type: 'SortOrder' })
  },
})

// Client.ts

export const Client = objectType({
  name: 'Client',
  definition(t) {
    t.nonNull.int('id', { description: 'Client id by integer. Auto-generated and auto-incremented'})
    t.nonNull.string('uuid', { description: 'Client id by string. Auto-generated and auto-incremented. Machine-readable only'})
    t.nonNull.field('createdAt', { type: 'DateTime', description: 'Client created at date' })
    t.nonNull.field('updatedAt', { type: 'DateTime', description: "Client updated at date" })
    t.nonNull.string('clientName', { description: "This is the name of the client (e.g. Typically a facility/business name)." })
    t.nonNull.string('email', { description: "The email account used for the primary business facility. If the owner or admin also has a user account, that user must have a different, unique email address." })
    t.nonNull.list.field("users", { type: User, description: "All of the client's users. This list is populated with current and inactive users." } )
  }
})

I get quite a long error message in my console from the query 'allUsers'. I'm not sure where to start since the error message is so long, however I will note that whenever I remove the .nonNull.SCALAR from my objectType() declaration, this error does go away completely.

TS Error in VS Code

Type '(_: {}, args: { orderBy?: { createdAt: "asc" | "desc"; id: "asc" | "desc"; updatedAt: "asc" | "desc"; } | null | undefined; searchString?: string | null | undefined; skip?: number | null | undefined; take?: number | ... 1 more ... | undefined; }, context: Context) => PrismaPromise<...>' is not assignable to type 'FieldResolver<"Query", "allUsers">'.
  Type 'PrismaPromise<(User & { clients: (UsersOnClient & { client: Client; })[]; profile: Profile | null; })[]>' is not assignable to type 'MaybePromise<{ clients: ({ clientName: string; createdAt: any; email: string; id: number; updatedAt: any; users: (... | null)[]; uuid: string; } | null)[]; createdAt: any; email: string; firstName: string; id: number; ... 8 more ...; uuid: string; }[]> | MaybePromise<...>'.
    Type 'PrismaPromise<(User & { clients: (UsersOnClient & { client: Client; })[]; profile: Profile | null; })[]>' is not assignable to type 'PromiseLike<MaybePromise<{ clients: ({ clientName: string; createdAt: any; email: string; id: number; updatedAt: any; users: (... | null)[]; uuid: string; } | null)[]; createdAt: any; email: string; firstName: string; id: number; ... 8 more ...; uuid: string; } | { ...; }>[]>'.
      Types of property 'then' are incompatible.
        Type '<TResult1 = (User & { clients: (UsersOnClient & { client: Client; })[]; profile: Profile | null; })[], TResult2 = never>(onfulfilled?: ((value: (User & { clients: (UsersOnClient & { ...; })[]; profile: Profile | null; })[]) => TResult1 | PromiseLike<...>) | null | undefined, onrejected?: ((reason: any) => TResult2 |...' is not assignable to type '<TResult1 = MaybePromise<{ clients: ({ clientName: string; createdAt: any; email: string; id: number; updatedAt: any; users: (... | null)[]; uuid: string; } | null)[]; createdAt: any; email: string; firstName: string; id: number; ... 8 more ...; uuid: string; } | { ...; }>[], TResult2 = never>(onfulfilled?: ((value:...'.
          Types of parameters 'onfulfilled' and 'onfulfilled' are incompatible.
            Types of parameters 'value' and 'value' are incompatible.
              Type '(User & { clients: (UsersOnClient & { client: Client; })[]; profile: Profile | null; })[]' is not assignable to type 'MaybePromise<{ clients: ({ clientName: string; createdAt: any; email: string; id: number; updatedAt: any; users: (... | null)[]; uuid: string; } | null)[]; createdAt: any; email: string; firstName: string; id: number; ... 8 more ...; uuid: string; } | { ...; }>[]'.
                Type 'User & { clients: (UsersOnClient & { client: Client; })[]; profile: Profile | null; }' is not assignable to type 'MaybePromise<{ clients: ({ clientName: string; createdAt: any; email: string; id: number; updatedAt: any; users: (... | null)[]; uuid: string; } | null)[]; createdAt: any; email: string; firstName: string; id: number; ... 8 more ...; uuid: string; } | { ...; }>'.
                  Type 'User & { clients: (UsersOnClient & { client: Client; })[]; profile: Profile | null; }' is not assignable to type '{ clients: MaybePromise<MaybePromise<{ clientName: string; createdAt: any; email: string; id: number; updatedAt: any; users: ({ clients: (... | null)[]; createdAt: any; email: string; firstName: string; id: number; isDependent?: boolean | ... 1 more ... | undefined; ... 7 more ...; uuid: string; } | null)[]; uuid: s...'.
                    Types of property 'clients' are incompatible.
                      Type '(UsersOnClient & { client: Client; })[]' is not assignable to type 'MaybePromise<MaybePromise<{ clientName: string; createdAt: any; email: string; id: number; updatedAt: any; users: ({ clients: (... | null)[]; createdAt: any; email: string; firstName: string; id: number; isDependent?: boolean | null | undefined; ... 7 more ...; uuid: string; } | null)[]; uuid: string; } | null>[]>'.
                        Type '(UsersOnClient & { client: Client; })[]' is not assignable to type 'MaybePromise<{ clientName: string; createdAt: any; email: string; id: number; updatedAt: any; users: ({ clients: (... | null)[]; createdAt: any; email: string; firstName: string; id: number; isDependent?: boolean | null | undefined; ... 7 more ...; uuid: string; } | null)[]; uuid: string; } | null>[]'.
                          Type 'UsersOnClient & { client: Client; }' is not assignable to type 'MaybePromise<{ clientName: string; createdAt: any; email: string; id: number; updatedAt: any; users: ({ clients: (... | null)[]; createdAt: any; email: string; firstName: string; id: number; isDependent?: boolean | null | undefined; ... 7 more ...; uuid: string; } | null)[]; uuid: string; } | null>'.
                            Type 'UsersOnClient & { client: Client; }' is missing the following properties from type '{ clientName: string; createdAt: any; email: string; id: number; updatedAt: any; users: ({ clients: (... | null)[]; createdAt: any; email: string; firstName: string; id: number; isDependent?: boolean | null | undefined; ... 7 more ...; uuid: string; } | null)[]; uuid: string; }': clientName, email, id, updatedAt, and 2 more.ts(2322)
definitionBlocks.d.ts(160, 5): The expected type comes from property 'resolve' which is declared here on type 'NexusOutputFieldConfig<"Query", "allUsers"> & { resolve: FieldResolver<"Query", "allUsers">; }'

I did remove some objectType definitions to make my problem more straightforward. I've been wracking my head about trying to solve this and am frustrated at how simple Nexus and Prisma seem to be, yet after writing the code end up plagued by typescript errors that don't often have clear solutions.

My problems are as follows:

  1. Is an additional interface needed to retrieve nested reads from an explicit many to many relationship?
  2. Are the nonNull flags on the objectType definitions what cause my resolver error(s)? or are my resolvers missing type information that must be stated/ provided elsewhere? (No examples clearly illustrate this).
raleigh9123 commented 3 years ago

Additionally, these are the relevant gql schemas from the generated schema.graphql.

enum SortOrder {
  asc
  desc
}

input OrderBy {
  createdAt: SortOrder!
  updatedAt: SortOrder!
  id: SortOrder!
}

type User {
  # User id by integer. Auto-generated and auto-incremented
  id: Int!

  # User id by string. Auto-generated and auto-incremented. Machine-readable only
  uuid: String!

  # User created at date
  createdAt: DateTime!

  # User updated at date
  updatedAt: DateTime!

  # User First Name
  firstName: String!

  # User Last Name
  lastName: String!

  # User's full email address
  email: String!

  # Athlete membership is active or inactive.
  status: UserStatus!

  # This is a list of clients that this user may belong to. Typically users have one client, but with a few exceptions may belong to more than one.
  clients: [Client]!

  # User role for application permissions
  role: UserRole!

  # User profile with additional user information
  profile: Profile
}

type Profile {
  # Profile id by integer. Integer matches profile creation.
  id: Int!

  # Profile Prisma-generated UUID. Not intended to be human readable.
  uuid: String!

  # User address number
  address: String

  # User city
  city: String

  # User state
  state: String

  # User zip code
  zip: Int

  # User country
  country: CountryCode

  # User cell phone number
  phoneNumber: String

  # User WhatsApp number
  whatsApp: String

  # User birthday
  dateOfBirth: DateTime

  # User emergency contact first name and last name
  emergencyContact: String

  # User emergency contact phone number
  emergencyPhoneNumber: String
}

enum UserRole {
  USER
  ADMIN
  OWNER
}

enum UserStatus {
  ACTIVE
  INACTIVE
}

enum CountryCode {
  United_States
  India
}

type Client {
  # Client id by integer. Auto-generated and auto-incremented
  id: Int!

  # Client id by string. Auto-generated and auto-incremented. Machine-readable only
  uuid: String!

  # Client created at date
  createdAt: DateTime!

  # Client updated at date
  updatedAt: DateTime!

  # This is the name of the client (e.g. Typically a facility/business name).
  clientName: String!

  # The email account used for the primary business facility. If the owner or admin also has a user account, that user must have a different, unique email address.
  email: String!

  # All of the client's users. This list is populated with current and inactive users.
  users: [User]!
}

type Query {
  # Return all users. Allows filtering, pagination, and sorting.
  allUsers(
    searchString: String
    skip: Int
    take: Int
    orderBy: OrderBy
  ): [User!]!
}
Sytten commented 3 years ago

This is not a problem with nexus, but let me answer so it might help other people. The problem is that you are not using custom resolvers for the relations in your model. This means that allUsers must supply the exact that form that the graphql is expecting, but you are using a join table between your two models (UsersOnClient). So when you do a findMany you dont have a list of clients (what graphql is expecting), but you have a listof UsersOnClient objects. You need to map them either in the parent resolver or you need to change the source typing so you can map them in the children resolver.