Skip to content

Commit

Permalink
feat: control rejectOnNotFound client setting (#135)
Browse files Browse the repository at this point in the history
  • Loading branch information
jasonkuhrt authored Aug 30, 2021
1 parent f9e2f2e commit 01daf38
Show file tree
Hide file tree
Showing 9 changed files with 206 additions and 15 deletions.
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ Official Prisma plugin for Nexus.
- [Project 1:n Relation](#project-1n-relation)
- [Example: Tests](#example-tests-1)
- [Example: Full 1:n](#example-full-1n)
- [Projecting Nullability](#projecting-nullability)
- [Prisma Client `rejectOnNotFound` Handling](#prisma-client-rejectonnotfound-handling)
- [Related Issues](#related-issues)
- [Runtime Settings](#runtime-settings)
- [Reference](#reference)
- [Generator Settings](#generator-settings)
Expand Down Expand Up @@ -589,6 +592,54 @@ query {
}
```

### Projecting Nullability

Currently nullability projection is not configurable. This section describes how Nexus Prsisma handles it.

```
Nexus Prisma Projects
DB Layer (Prisma) → → ┴ → → API Layer (GraphQL)
––––––––––––––––– –––––––––––––––––––
Nullable Field Relation Nullable Field Relation
model A { type A {
foo Foo? foo: Foo
} }
Non-Nullable Field Relation Non-Nullable Field Relation
model A { type A {
foo Foo foo: Foo!
} }
List Field Relation Non-Nullable Field Relation Within Non-Nullable List
model A { type A {
foos Foo[] foo: [Foo!]!
} }
```

If a `findOne` or `findUnique` for a non-nullable Prisma field return null for some reason (e.g. data corruption in the database) then the standard [GraphQL `null` propagation](https://medium.com/@calebmer/when-to-use-graphql-non-null-fields-4059337f6fc8) will kick in.

#### Prisma Client `rejectOnNotFound` Handling

Prisma Client's [`rejectOnNotFound` feature](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#rejectonnotfound) is effectively ignored by Nexus Prisma. For example if you [set `rejectOnNotFound` globally on your Prisma Client](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#enable-globally-for-findunique-and-findfirst) it will not effect Nexus Prisma when it uses Prisma Client. This is because Nexus Prisma [sets `rejectOnNotFound: false` for every `findUnique`/`findFirst`](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#remarks-2) request it sends.

The reason for this design choice is that when Nexus Prisma's logic is handling a GraphQL resolver that includes how to handle nullability issues which it has full knowledge about.

If you have a use-case for different behaviour [please open a feature request](https://github.com/prisma/nexus-prisma/issues/new?assignees=&labels=type%2Ffeat&template=10-feature.md&title=Better%20rejectOnNotFound%20Handling). Also, remember, you can always override the Nexus Prisma resolvers with your own logic ([receipe](#Project-relation-with-custom-resolver-logic)).

#### Related Issues

- [`#98` Always set rejectOnNotFound to false](https://github.com/prisma/nexus-prisma/issues/98)

### Runtime Settings

#### Reference
Expand Down
14 changes: 14 additions & 0 deletions src/generator/models/javascript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,20 @@ export function prismaFieldToNexusResolver(

const result: unknown = findUnique({
where: buildWhereUniqueInput(root, uniqueIdentifiers),
/**
*
* The user might have configured Prisma Client globally to rejectOnNotFound.
* In the context of this Nexus Prisma managed resolver, we don't want that setting to
* be a behavioural factor. Instead, Nexus Prisma has its own documented rules about the logic
* it uses to project nullability from the database to the api.
*
* More details about this design can be found in the README.
*
* References:
*
* - https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#rejectonnotfound
*/
rejectOnNotFound: false,
})

// @ts-expect-error Only known at runtime
Expand Down
39 changes: 33 additions & 6 deletions tests/__helpers__/testers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,29 @@ export type IntegrationTestSpec = {
* Note datasource and generator blocks are taken care of automatically for you.
*/
datasourceSchema: string
/**
* Define the GraphQL API. All returned type defs are added to the final GraphQL schema.
*/
apiSchema: APISchemaSpec
datasourceSeed: (prismaClient: any) => Promise<void>
/**
* Access the Prisma Client instance and run some setup side-effects.
*
* Examples of things to do there:
*
* 1. Seed the database.
*/
setup?: (prismaClient: any) => Promise<void>
/**
* Handle instantiation of a Prisma Client instance.
*
* Examples of things to do there:
*
* 1. Customize the Prisma Client settings.
*/
setupPrismaClient?: (prismaClientPackage: any) => Promise<any>
/**
* A Graphql document to execute against the GraphQL API. The result is snapshoted.
*/
apiClientQuery: DocumentNode
}

Expand Down Expand Up @@ -70,9 +91,9 @@ export function testIntegration(params: IntegrationTestParams) {
if (params.skip && params.only)
throw new Error(`Cannot specify to skip this test AND only run this test at the same time.`)

const itOrItOnlyOrItSkip = params.only ? it.only : params.skip ? it.skip : it
const test = params.only ? it.only : params.skip ? it.skip : it

itOrItOnlyOrItSkip(
test(
params.description,
async () => {
const result = await integrationTest(params)
Expand Down Expand Up @@ -116,7 +137,8 @@ export function testGraphqlSchema(params: {
export async function integrationTest({
datasourceSchema,
apiSchema,
datasourceSeed,
setup,
setupPrismaClient,
apiClientQuery,
}: IntegrationTestParams) {
const dir = fs.tmpDir().cwd()
Expand All @@ -138,8 +160,13 @@ export async function integrationTest({
execa.commandSync(`yarn -s prisma db push --force-reset --schema ${dir}/schema.prisma`)

const prismaClientPackage = require(prismaClientImportId)
const prismaClient = new prismaClientPackage.PrismaClient()
await datasourceSeed(prismaClient)
const prismaClient = setupPrismaClient
? setupPrismaClient(prismaClientPackage)
: new prismaClientPackage.PrismaClient()

if (setup) {
await setup(prismaClient)
}

const dmmf = await PrismaSDK.getDMMF({
datamodel: prismaSchemaContents,
Expand Down
31 changes: 31 additions & 0 deletions tests/integration/__snapshots__/rejectOnNotFound.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`ignores global rejectOnNotFound Prisma Client settings: graphqlOperationExecutionResult 1`] = `
Object {
"data": Object {
"users": Array [
Object {
"id": "user1",
"profile": null,
},
],
},
}
`;

exports[`ignores global rejectOnNotFound Prisma Client settings: graphqlSchemaSDL 1`] = `
"
type Query {
users: [User!]!
}
type User {
id: ID!
profile: Profile
}
type Profile {
id: ID!
}
"
`;
2 changes: 1 addition & 1 deletion tests/integration/json.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ testIntegration({
}),
]
},
async datasourceSeed(prisma) {
async setup(prisma) {
await prisma.foo.create({
data: {
id: 'foo1',
Expand Down
68 changes: 68 additions & 0 deletions tests/integration/rejectOnNotFound.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import gql from 'graphql-tag'
import { objectType, queryType } from 'nexus'
import { testIntegration } from '../__helpers__/testers'

testIntegration({
description: 'ignores global rejectOnNotFound Prisma Client settings',
datasourceSchema: `
model User {
id String @id
profile Profile? @relation(fields: [profileId], references: [id])
profileId String?
}
model Profile {
id String @id
user User?
}
`,
apiSchema({ User, Profile }) {
return [
queryType({
definition(t) {
t.nonNull.list.nonNull.field('users', {
type: 'User',
resolve(_, __, ctx) {
return ctx.prisma.user.findMany()
},
})
},
}),
objectType({
name: User.$name,
definition(t) {
t.field(User.id)
t.field(User.profile)
},
}),
objectType({
name: Profile.$name,
definition(t) {
t.field(Profile.id)
},
}),
]
},
setupPrismaClient(prismaClientPackage) {
// This global setting should have no effect on Nexus Prisma
return new prismaClientPackage.PrismaClient({
rejectOnNotFound: true,
})
},
async setup(prisma) {
await prisma.user.create({
data: {
id: 'user1',
},
})
},
apiClientQuery: gql`
query {
users {
id
profile {
id
}
}
}
`,
})
8 changes: 4 additions & 4 deletions tests/integration/relation1To1.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ testIntegration({
}),
]
},
async datasourceSeed(prisma) {
async setup(prisma) {
await prisma.user.create({
data: {
id: 'user1',
Expand Down Expand Up @@ -109,7 +109,7 @@ testIntegration({
}),
]
},
async datasourceSeed(prisma) {
async setup(prisma) {
await prisma.user.create({
data: {
id: 'user1',
Expand Down Expand Up @@ -181,7 +181,7 @@ testIntegration({
}),
]
},
async datasourceSeed(prisma) {
async setup(prisma) {
await prisma.user.create({
data: {
id: 'user1',
Expand Down Expand Up @@ -255,7 +255,7 @@ testIntegration({
}),
]
},
async datasourceSeed(prisma) {
async setup(prisma) {
await prisma.user.create({
data: {
id: 'user1',
Expand Down
4 changes: 2 additions & 2 deletions tests/integration/relation1ToN.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ testIntegration({
}),
]
},
async datasourceSeed(prisma) {
async setup(prisma) {
await prisma.user.create({
data: {
id: 'user1',
Expand Down Expand Up @@ -112,7 +112,7 @@ testIntegration({
}),
]
},
async datasourceSeed(prisma) {
async setup(prisma) {
await prisma.user.create({
data: {
id1: 'user1',
Expand Down
4 changes: 2 additions & 2 deletions tests/specs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export namespace Specs {
}),
]
},
async datasourceSeed(prisma) {
async setup(prisma) {
await prisma.user.create({
data: {
id: 'user1',
Expand Down Expand Up @@ -111,7 +111,7 @@ export namespace Specs {
}),
]
},
async datasourceSeed(prisma) {
async setup(prisma) {
await prisma.user.create({
data: {
id: 'user1',
Expand Down

1 comment on commit 01daf38

@vercel
Copy link

@vercel vercel bot commented on 01daf38 Aug 30, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.