diff --git a/README.md b/README.md index b8e50b223..77a000684 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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 diff --git a/src/generator/models/javascript.ts b/src/generator/models/javascript.ts index be758a747..17283fa76 100644 --- a/src/generator/models/javascript.ts +++ b/src/generator/models/javascript.ts @@ -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 diff --git a/tests/__helpers__/testers.ts b/tests/__helpers__/testers.ts index a0b940985..6fc793f82 100644 --- a/tests/__helpers__/testers.ts +++ b/tests/__helpers__/testers.ts @@ -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 + /** + * 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 + /** + * Handle instantiation of a Prisma Client instance. + * + * Examples of things to do there: + * + * 1. Customize the Prisma Client settings. + */ + setupPrismaClient?: (prismaClientPackage: any) => Promise + /** + * A Graphql document to execute against the GraphQL API. The result is snapshoted. + */ apiClientQuery: DocumentNode } @@ -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) @@ -116,7 +137,8 @@ export function testGraphqlSchema(params: { export async function integrationTest({ datasourceSchema, apiSchema, - datasourceSeed, + setup, + setupPrismaClient, apiClientQuery, }: IntegrationTestParams) { const dir = fs.tmpDir().cwd() @@ -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, diff --git a/tests/integration/__snapshots__/rejectOnNotFound.test.ts.snap b/tests/integration/__snapshots__/rejectOnNotFound.test.ts.snap new file mode 100644 index 000000000..8405d7c40 --- /dev/null +++ b/tests/integration/__snapshots__/rejectOnNotFound.test.ts.snap @@ -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! +} +" +`; diff --git a/tests/integration/json.test.ts b/tests/integration/json.test.ts index a0187d303..662cb4e8e 100644 --- a/tests/integration/json.test.ts +++ b/tests/integration/json.test.ts @@ -39,7 +39,7 @@ testIntegration({ }), ] }, - async datasourceSeed(prisma) { + async setup(prisma) { await prisma.foo.create({ data: { id: 'foo1', diff --git a/tests/integration/rejectOnNotFound.test.ts b/tests/integration/rejectOnNotFound.test.ts new file mode 100644 index 000000000..3cb63d047 --- /dev/null +++ b/tests/integration/rejectOnNotFound.test.ts @@ -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 + } + } + } + `, +}) diff --git a/tests/integration/relation1To1.test.ts b/tests/integration/relation1To1.test.ts index 16fdf9b54..d7427d02b 100644 --- a/tests/integration/relation1To1.test.ts +++ b/tests/integration/relation1To1.test.ts @@ -42,7 +42,7 @@ testIntegration({ }), ] }, - async datasourceSeed(prisma) { + async setup(prisma) { await prisma.user.create({ data: { id: 'user1', @@ -109,7 +109,7 @@ testIntegration({ }), ] }, - async datasourceSeed(prisma) { + async setup(prisma) { await prisma.user.create({ data: { id: 'user1', @@ -181,7 +181,7 @@ testIntegration({ }), ] }, - async datasourceSeed(prisma) { + async setup(prisma) { await prisma.user.create({ data: { id: 'user1', @@ -255,7 +255,7 @@ testIntegration({ }), ] }, - async datasourceSeed(prisma) { + async setup(prisma) { await prisma.user.create({ data: { id: 'user1', diff --git a/tests/integration/relation1ToN.test.ts b/tests/integration/relation1ToN.test.ts index 5f6879a41..35686edbc 100644 --- a/tests/integration/relation1ToN.test.ts +++ b/tests/integration/relation1ToN.test.ts @@ -43,7 +43,7 @@ testIntegration({ }), ] }, - async datasourceSeed(prisma) { + async setup(prisma) { await prisma.user.create({ data: { id: 'user1', @@ -112,7 +112,7 @@ testIntegration({ }), ] }, - async datasourceSeed(prisma) { + async setup(prisma) { await prisma.user.create({ data: { id1: 'user1', diff --git a/tests/specs.ts b/tests/specs.ts index f2b3295f9..7e2488038 100644 --- a/tests/specs.ts +++ b/tests/specs.ts @@ -46,7 +46,7 @@ export namespace Specs { }), ] }, - async datasourceSeed(prisma) { + async setup(prisma) { await prisma.user.create({ data: { id: 'user1', @@ -111,7 +111,7 @@ export namespace Specs { }), ] }, - async datasourceSeed(prisma) { + async setup(prisma) { await prisma.user.create({ data: { id: 'user1',