Skip to content

Commit

Permalink
docs: cover projecting 1-to-1 relations (#32)
Browse files Browse the repository at this point in the history
  • Loading branch information
jasonkuhrt authored May 11, 2021
1 parent d0fd64a commit bb70ea4
Show file tree
Hide file tree
Showing 11 changed files with 742 additions and 67 deletions.
212 changes: 211 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ Official Prisma plugin for Nexus.
- [Type-safe Generated Library Code](#type-safe-generated-library-code)
- [Project Enums](#project-enums)
- [Project Scalars](#project-scalars)
- [Project Relations](#project-relations)
- [Limitation: Hardcoded key for Prisma Client on GraphQL Context](#limitation-hardcoded-key-for-prisma-client-on-graphql-context)
- [Example: Exposing Prisma Client on GraphQL Context with Apollo Server](#example-exposing-prisma-client-on-graphql-context-with-apollo-server)
- [Project 1:1 Relation](#project-11-relation)
- [Example: Full 1:1](#example-full-11)
- [Limitation: Nullable on Without-Relation-Scalar Side](#limitation-nullable-on-without-relation-scalar-side)
- [Prisma ID field to GraphQL ID scalar type mapping](#prisma-id-field-to-graphql-id-scalar-type-mapping)
- [Prisma Schema docs re-used for GraphQL schema doc](#prisma-schema-docs-re-used-for-graphql-schema-doc)
- [Prisma Schema docs re-used for JSDoc](#prisma-schema-docs-re-used-for-jsdoc)
Expand Down Expand Up @@ -98,7 +104,7 @@ export const schema = makeSchema({

##### Midterm

- [ ] Support for Prisma Model field types relating to other Models 1:1
- [x] ([#25](https://github.com/prisma/nexus-prisma/pull/25), []())Support for Prisma Model field types relating to other Models 1:1
- [ ] Support for Prisma Model field types relating to other Models 1:n
- [ ] Support for Prisma Model field types relating to other Models n:n

Expand Down Expand Up @@ -198,6 +204,210 @@ makeSchema({

There is a [recipe below](#Supply-custom-custom-scalars-to-your-GraphQL-schema) showing how to add your own custom scalars if you want.

### Project Relations

You can project [relations](https://www.prisma.io/docs/concepts/components/prisma-schema/relations) into your API with Nexus Prisma. Nexus Prisma even includes the resolver you'll need at runtime to fulfill the projection by automating use of your Prisma Client instance.

Please note that not all kinds of relationships are supported yet. Details about projecting each kind of relation are documented in their respective sections. This section only contains general documentation common to all.

#### Limitation: Hardcoded key for Prisma Client on GraphQL Context

To project relations you must expose an instance of Prisma Client on the GraphQL context under the key name `prisma`. This limitation may be a problem for your project. There is an [issue tracking this](https://github.com/prisma/nexus-prisma/issues/35) that you can subscribe to if interested. The only workaround is to write your own resolvers.

#### Example: Exposing Prisma Client on GraphQL Context with Apollo Server

```ts
import { ApolloServer } from 'apollo-server'
import { PrismaClient } from '@prisma/client'
import schema from './your/schema/somewhere'

const prisma = new PrismaClient()

new ApolloServer({
schema,
context() {
return {
prisma,
}
},
})
```

### Project 1:1 Relation

You can project [1:1 relationships](https://www.prisma.io/docs/concepts/components/prisma-schema/relations#one-to-one-relations) into your API.

#### Example: Full 1:1

```prisma
// Database Schema
model User {
id String @id
profile Profile @relation(fields: [profileId], references: [id])
profileId String
}
model Profile {
id String @id
user User?
}
```

```ts
// API Schema

import { User, Profile } from 'nexus-prisma'

objectType({
name: User.$name,
definition(t) {
t.field(User.id.name, User.id)
t.field(User.profile.name, User.profile)
},
})

objectType({
name: Profile.$name,
definition(t) {
t.field(Profile.id.name, Profile.id)
},
})

queryType({
definition(t) {
t.list.field('users', {
type: 'User',
resolve(_, __, ctx) {
return ctx.prisma.user.findMany()
},
})
},
})
```

```graphql
# API Schema Represented in GraphQL SDL (this is generated by Nexus)

type User {
id: ID
profile: Profile
}

type Profile {
id: ID
}
```

```ts
// Example Database Data (for following example)

await prisma.user.create({
data: {
id: 'user1',
profile: {
create: {
id: 'profile1',
},
},
},
})
```

```graphql
# Example API Client Query

query {
users {
id
profile {
id
}
}
}
```

```json
{
"data": {
"users": [
{
"id": "user1",
"profile": {
"id": "profile1"
}
}
]
}
}
```

#### Limitation: Nullable on Without-Relation-Scalar Side

Prisma requires that a 1:1 relationship has one side that is optional. For example in the following it is **not** possible for `Profile` to have a required relationship to `User`. For more detail you can read the Prisma docs abuot this [here](https://www.prisma.io/docs/concepts/components/prisma-schema/relations#one-to-one-relations).

```prisma
model User {
id String @id
profile Profile @relation(fields: [profileId], references: [id])
profileId String
}
model Profile {
id String @id
user User? // <-- "?" required
}
```

Prisma inherits this limitation from databases. In turn Nexus Prisma inherits this limitation from Prisma. For example consider this projection and then look at the resulting GraphQL SDL representation.

```ts
import { User, Profile } from 'nexus-prisma'

objectType({
name: User.$name,
definition(t) {
t.field(User.id.name, User.id)
t.field(User.profile.name, User.profile)
},
})

objectType({
name: Profile.$name,
definition(t) {
t.field(Profile.id.name, Profile.id)
t.field(User.profile.name, User.profile)
},
})
```

```graphql
type User {
id: ID
profile: Profile!
}

type Profile {
id: ID
user: User # <-- Nullable!
}
```

This limitation may be a problem for your API. There is an [issue track this that you can subscribe to](https://github.com/prisma/nexus-prisma/issues/34) if interested. As a workaround for now you can do this:

```ts
objectType({
name: Profile.$name,
definition(t) {
t.field(Profile.id.name, Profile.id)
t.field(User.profile.name, {
...User.profile,
type: nonNull(User.profile.type),
})
},
})
```

### Prisma ID field to GraphQL ID scalar type mapping

All `@id` fields in your Prisma Schema get projected as `ID` types, not `String` types.
Expand Down
1 change: 1 addition & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const config: InitialOptionsTsJest = {
preset: 'ts-jest',
testEnvironment: 'node',
watchPlugins: ['jest-watch-typeahead/filename', 'jest-watch-typeahead/testname'],
testPathIgnorePatterns: process.env.CI ? [] : ['.*e2e.*'],
globals: {
'ts-jest': {
diagnostics: Boolean(process.env.CI),
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"execa": "^5.0.0",
"graphql": "^15.5.0",
"graphql-request": "^3.4.0",
"graphql-tag": "^2.12.4",
"jest": "26.6.3",
"jest-watch-typeahead": "0.6.3",
"lodash": "^4.17.21",
Expand Down
52 changes: 42 additions & 10 deletions src/generator/models/javascript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,36 @@ export function createModuleSpec(): ModuleSpec {
}
}

export function createNexusTypeDefConfigurations(dmmf: DMMF.Document): NexusTypeDefConfigurations {
export function createNexusTypeDefConfigurations(
dmmf: DMMF.Document,
settings: Settings = {}
): NexusTypeDefConfigurations {
const configuration = resolveSettings(settings)
return {
...createNexusObjectTypeDefConfigurations(dmmf),
...createNexusObjectTypeDefConfigurations(dmmf, configuration),
...createNexusEnumTypeDefConfigurations(dmmf),
}
}

function resolveSettings(settings: Settings): Configuration {
return {
prismaClientImport: settings.prismaClientImport ?? '@prisma/client',
}
}

type Settings = {
/**
* The import ID of prisma client.
*
* @default @prisma/client
*/
prismaClientImport?: string
}

type Configuration = {
prismaClientImport: string
}

type NexusObjectTypeDefConfigurations = Record<PrismaModelName, NexusObjectTypeDefConfiguration>

type NexusObjectTypeDefConfiguration = Record<
Expand All @@ -69,7 +92,10 @@ type NexusObjectTypeDefConfiguration = Record<
/**
* Create Nexus object type definition configurations for Prisma models found in the given DMMF.
*/
function createNexusObjectTypeDefConfigurations(dmmf: DMMF.Document): NexusObjectTypeDefConfigurations {
function createNexusObjectTypeDefConfigurations(
dmmf: DMMF.Document,
configuration: Configuration
): NexusObjectTypeDefConfigurations {
return chain(dmmf.datamodel.models)
.map((model) => {
return {
Expand All @@ -81,7 +107,7 @@ function createNexusObjectTypeDefConfigurations(dmmf: DMMF.Document): NexusObjec
name: field.name,
type: prismaFieldToNexusType(field),
description: field.documentation,
resolve: prismaFieldToNexusResolver(model, field),
resolve: prismaFieldToNexusResolver(model, field, configuration),
}
})
.keyBy('name')
Expand All @@ -106,7 +132,11 @@ export function prismaFieldToNexusType(field: DMMF.Field) {
}
}

export function prismaFieldToNexusResolver(model: DMMF.Model, field: DMMF.Field): undefined | Resolver {
export function prismaFieldToNexusResolver(
model: DMMF.Model,
field: DMMF.Field,
configuration: Configuration
): undefined | Resolver {
/**
* Allow Nexus default resolver to handle resolving scalars.
*
Expand Down Expand Up @@ -154,24 +184,26 @@ export function prismaFieldToNexusResolver(model: DMMF.Model, field: DMMF.Field)
}

// eslint-disable-next-line
const PrismaClientPackage = require('@prisma/client')
const PrismaClientPackage = require(configuration.prismaClientImport)

// eslint-disable-next-line
if (!(ctx.prisma instanceof PrismaClientPackage.PrismaClient)) {
// TODO rich errors
throw new Error(`todo`)
throw new Error(
`The GraphQL context.prisma value is not an instance of the Prisma Client (imported from ${configuration.prismaClientImport}).`
)
}

const methodName = lowerFirst(model.name)
const propertyModelName = lowerFirst(model.name)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const prisma: any = ctx.prisma
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
const prismaModel = prisma[methodName]
const prismaModel = prisma[propertyModelName]

// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (typeof prismaModel.findUnique !== 'function') {
// TODO rich errors
throw new Error(`todo`)
throw new Error(`The prisma model ${model.name} does not have a findUnique method available.`)
}

// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
Expand Down
Loading

0 comments on commit bb70ea4

Please sign in to comment.