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

Read Operations (Query and Input Generation) #932

Open
6 tasks
colin-oos opened this issue Jan 6, 2024 · 0 comments · May be fixed by #933
Open
6 tasks

Read Operations (Query and Input Generation) #932

colin-oos opened this issue Jan 6, 2024 · 0 comments · May be fixed by #933
Labels
type/feat Add a new capability or enhance an existing one

Comments

@colin-oos
Copy link

colin-oos commented Jan 6, 2024

Perceived Problem

There is currently no support for read operations.

Ideas / Proposed Solution(s)

Modify the module generators to generate Query fields and Input types corresponding to the Prisma schema models, as well as the root resolver logic for said query fields.

Overview

I've been working on developing support for read operations in nexus-prisma for use on some projects with my team. It has been very beneficial for us to be able to do this. Ideally, I'd like full crud support but figured the best place to start would be with read since that would probably be the easiest operation to adopt as well as the most helpful one to add. In general, this modification works similarly to the old nexus-plugin-prisma package, but models the newer patterns established by nexus-prisma. I feel pretty good about the stuff we've added for read operations and we are using it on a project with success so far, so I figured it might be a good time to share what I've done with the community! Please note, this patch should not be considered production ready.

I will be creating a PR soon, but figured the easiest way to let the community get their hands on it would be to create a patch file and share that here. This way, if you want to experiment, you can apply the patch directly to your node_modules/nexis-prisma build. This is because that is the simplest, most seamless way to get code generation to operate exactly as before in existing projects. However, please be on the lookup for my PR which I will link in the comment thread below if you would like to reference the source code or play around with that.

Patch

Perform the following steps to install this patch:

Install patch-package

yarn install -D patch-package

Add postinstall script

Add the patch-package command to your postinstall script inside of package.json

{
  ...
  "scripts": {
    ...
    "postinstall": "patch-package",
    ...
  }
  ...
}

Install nexus-prisma version 2.0.1

You must make sure you have exactly version 2.0.1 installed of nexus-prisma because this is the version that the patch is made off of. You can do this by ensuring you have the following in your package.json:

{
  ...
  "dependencies": {
    ...
    "nexus-prisma": "2.0.1",
    ...
  }
  ...
}

Also be sure you follow the instruction for in the docs if installing and setting up nexus-prisma in your project for the first time.

Add patch file

Click the link below to download the patch file, and add it to patches/nexus-prisma+2.0.1.patch where patches is a directory in the root of your project. Note that your file path and file name must match exactly in your project.

nexus-prisma+2.0.1.patch

Run yarn install

Run the following script to ensure version 2.0.1 of nexus-prisma gets installed and the patch applied.

yarn install

Documentation

The subsequent examples are based on the following Prisma schema:

// Prisma Schema

model User {
  id        Int @id() @default(autoincrement())
  email     String
  name      String?
  posts     Post[]
  
  tenantId  Int
  tenant    Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
  
  @@unique([email, tenantId])
}

model Tenant {
  id        Int @id() @default(autoincrement())
  name      String
  users     User[]
}

model Post {
  id        Int @id() @default(autoincrement())
  title     String
  
  userId    Int
  user      User @relation(fields: [userId], references: [id])
}

Query Fields

Generates two query fields for each Prisma model that perform the following Prisma read operations:

findUnique

Each Prisma model gets a corresponding findUnique query field that is named based on a camelCase version of the model's name. For example Query.user

Prior to defining the query field you must also define the operation's [Model]WhereUniqueInput type where [Model] is the name of your model being queried. For example UserWhereUniqueInput.

Basic example
import { inputObjectType, extendType } from 'nexus'
import { Query, UserWhereUniqueInput, User } from 'nexus-prisma'

export const UserObjectType = objectType({
  name: User.$name,
  definition(t) {
    t.field(User.id)
    t.field(User.email)
    t.field(User.name)
  }
})

export const UserWhereUniqueInputObjectType = inputObjectType({
  name: UserWhereUniqueInput.$name,
  definition(t) {
    t.field(UserWhereUniqueInput.id)
    t.field(UserWhereUniqueInput.email_tenantId)
  }
})

export const UserQueryFields = extendType({
  type: 'Query',
  definition(t) {
    t.field(Query.user)
  }
})
query User {
  user(where: {
    id: 1,
    # Or
    email_tenantId {
      email: "[email protected]",
      tenantId: 1,
    }
  }) {
    id
    email
    name
  }
}
Change input field name example

You can also use the as helper method to optionally change the name of your input field if you'd like it to be different than the name Prisma uses under the hood. Note that in the event of combined unique fields, you currently cannot change the name of the nested input types within the combined field.

import { inputObjectType, extendType } from 'nexus'
import { Query, UserWhereUniqueInput } from 'nexus-prisma'

export const UserWhereUniqueInputObjectType = inputObjectType({
  name: UserWhereUniqueInput.$name,
  definition(t) {
    t.field(UserWhereUniqueInput.id)
    t.field(UserWhereUniqueInput.email_tenantId.as('email_organizationId'))
  }
})
query User {
  user(where: {
    id: 1,
    # Or
    email_organizationId {
      email: "[email protected]",
      tenantId: 1,
    }
  }) {
    id
    email
    name
  }
}

findMany

Each Prisma model also gets a corresponding findMany query field that is named based on a camelCase version of the model's name followed by an s. For example Query.users. Note that currently there is no way to modify the naming convention of findMany query fields and an s is always automatically applied even if it does not make grammatical sense. There are plans to add the ability to change the general naming convention or specific query field names via the Settings in the future.

Prior to defining the query field you must also define the operation's [Model]WhereInput type where [Model] is the name of your model being queried. For example UserWhereInput. Additionally, the where input type contains field configs for AND, OR, and NOT filter types allowing for advanced-nested filtering logic.

Basic example
import { inputObjectType, extendType } from 'nexus'
import { Query, UserWhereInput, User } from 'nexus-prisma'

export const UserObjectType = objectType({
  name: User.$name,
  definition(t) {
    t.field(User.id)
    t.field(User.email)
    t.field(User.name)
  }
})

export const UserWhereInputObjectType = inputObjectType({
  name: UserWhereInput.$name,
  definition(t) {
    t.field(UserWhereInput.AND)
    t.field(UserWhereInput.OR)
    t.field(UserWhereInput.NOT)
    t.field(UserWhereInput.id)
    t.field(UserWhereInput.email)
    t.field(UserWhereInput.name)
  }
})

export const UserQueryFields = extendType({
  type: 'Query',
  definition(t) {
    // ...
    t.field(Query.users)
  }
})
query Users {
  users(where: {
    OR: [
      {
        id: {
          equals: 1
        }
      },
      {
        id: {
          equals: 2
        }
      }
    ]
    name: {
      not: {
        equals: "John"
      }
    }
    email: {
      contains: "@gmail.com"
    }
  }) {
    list {
      id
      email
      name
    }
  }
}
Nested filters example

In this example, we must additionally define the TenantWhereInput and PostWhereInput so that we can then add the tenant and posts input fields to our UserWhereInput. This allows us to filter based on relationship sub-fields.

import { inputObjectType, extendType } from 'nexus'
import { Query, TenantWhereInput, PostWhereInput, UserWhereInput } from 'nexus-prisma'

// ...

export const TenantWhereInputObjectType = inputObjectType({
  name: TenantWhereInput.$name,
  definition(t) {
    t.field(TenantWhereInput.AND)
    t.field(TenantWhereInput.OR)
    t.field(TenantWhereInput.NOT)
    t.field(TenantWhereInput.id)
    t.field(TenantWhereInput.name)
  }
})

export const PostWhereInputObjectType = inputObjectType({
  name: PostWhereInput.$name,
  definition(t) {
    t.field(PostWhereInput.AND)
    t.field(PostWhereInput.OR)
    t.field(PostWhereInput.NOT)
    t.field(PostWhereInput.title)
  }
})

export const UserWhereInputObjectType = inputObjectType({
  name: UserWhereInput.$name,
  definition(t) {
    t.field(UserWhereInput.AND)
    t.field(UserWhereInput.OR)
    t.field(UserWhereInput.NOT)
    t.field(UserWhereInput.id)
    t.field(UserWhereInput.email)
    t.field(UserWhereInput.name)
    t.field(UserWhereInput.tenant)
    t.field(UserWhereInput.posts)
  }
})

// ...
query Users {
  users(where: {
    OR: [
      {
        id: {
          equals: 1
        }
      },
      {
        id: {
          equals: 2
        }
      }
    ]
    name: {
      not: {
        equals: "John"
      }
    }
    email: {
      contains: "@gmail.com"
    }
    tenant: {
      AND: [
        {
          name: {
            contains: "Apple"
          }
        },
        {
          name: {
            contains: "Inc."
          }
        }
      ]
    }
    posts: {
      some: {
        title: {
          contains: "Hello there"
        }
      }
    }
  }) {
    list {
      id
      email
      name
    }
  }
}
Pagination example

You can paginate your queries easily by adding take and/or skip arguments to your query. Your selection can include total and hasMore where total is the total number of results based on your filter (if any) and hasMore is true if there are more records after the amount you took.

query Users {
  users(
    where: {
      name: {
        startsWith: "A",
      }
    }
    take: 25
    skip: 50
  ) {
    list {
      id
      email
      name
    }
    total
    hasMore
  }
}

Field Resolver Filters

With the introduction of where input types for queries as described above, it made sense to also update field resolver logic to be able to handle where filters if provided for list relationship fields. When this is done, the resolver is smart enough to return a paginated result containing list, total, and hasMore as opposed to just a single list. This is not a breaking change since not adding a where filter will still return a single list of results for your existing field resolvers.

Basic example

import { inputObjectType, extendType } from 'nexus'
import { Query, PostWhereInput, User } from 'nexus-prisma'

export const PostWhereInputObjectType = inputObjectType({
 name: PostWhereInput.$name,
 definition(t) {
   t.field(PostWhereInput.AND)
   t.field(PostWhereInput.OR)
   t.field(PostWhereInput.NOT)
   t.field(PostWhereInput.title)
 }
})

export const UserObjectType = objectType({
 name: User.$name,
 definition(t) {
   t.field(User.id)
   t.field(User.email)
   t.field(User.name)
   t.field({
     ...User.posts,
     args: {
       where: PostWhereInputObjectType
       take: 'Int',
       skip: 'Int',
     }
   })
 }
})
query Users {
 users(
   where: {
     name: {
       startsWith: "A",
     }
   }
   take: 25
   skip: 50
 ) {
   list {
     id
     email
     name
     posts(
       where: {
         title: {
           contains: "Hello there"
         }
       },
       take: 10
       skip: 10
     ) {
       list {
         title
       }
       total
       hasMore
     }
   }
   total
   hasMore
 }
}

Todos

  • Ordering results
  • Configurable findMany query field names
  • Configurable names for unique combination nested input fields
  • Better decorative comments around newly generated typescript
  • Better comments for newly added code
  • Create tests

Additional Notes

This issue pertains specifically to read operations and therefore does not include create, update, or delete operations as they relate to the Mutation type. When I have time, I may work on these as well modeling similar patterns set forth herein and then create separate issues / pull requests for those. Ultimately stepping towards the end goal of full crud support. If anyone has any additional ideas or thoughts related to all of the above please let me know below!

@colin-oos colin-oos added the type/feat Add a new capability or enhance an existing one label Jan 6, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type/feat Add a new capability or enhance an existing one
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant