Skip to content

Commit

Permalink
feat: add default resolver for relation fields (#25)
Browse files Browse the repository at this point in the history
Co-authored-by: Jason Kuhrt <jasonkuhrt@me.com>
  • Loading branch information
lvauvillier and jasonkuhrt authored May 7, 2021
1 parent 624c745 commit 4f5cd70
Show file tree
Hide file tree
Showing 18 changed files with 1,337 additions and 550 deletions.
10 changes: 7 additions & 3 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
"tasks": [
{
"label": "dev:link",
"dependsOn": ["dev:yalc", "dev:ts"]
"problemMatcher": [],
"dependsOn": ["__dev:yalc", "__dev:ts"]
},
{
"label": "tdd",
"type": "shell",
"command": "yarn -s tdd",
"problemMatcher": [],
"presentation": {
"group": "test",
"focus": true,
Expand All @@ -22,9 +24,10 @@
// Lower level task building blocks

{
"label": "dev:yalc",
"label": "__dev:yalc",
"type": "shell",
"command": "yarn -s dev:yalc",
"problemMatcher": [],
"presentation": {
"group": "dev",
"focus": false,
Expand All @@ -33,9 +36,10 @@
}
},
{
"label": "dev:ts",
"label": "__dev:ts",
"type": "shell",
"command": "yarn -s dev:ts",
"problemMatcher": [],
"presentation": {
"group": "dev",
"focus": false,
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ With all this in place, the chain reaction goes like this:
1. `Project` `nodemon` reacts to this, runs `prisma generate`
1. You try things out with newly generated Nexus Prisma in `Project`!

One issues are being worked out related to `bin` and `chmod`: https://github.com/wclr/yalc/issues/156
One issue is being worked out related to `bin` and `chmod`: https://github.com/wclr/yalc/issues/156

If you change a dependency in `Nexus Prisma` while working (especially adding a new one) you will need to remove the `node_modules` in `Project` and re-install e.g. `yarn install`.

Expand Down
40 changes: 20 additions & 20 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,56 +37,56 @@
"prepublishOnly": "yarn build"
},
"devDependencies": {
"@homer0/prettier-plugin-jsdoc": "^3.0.0",
"@homer0/prettier-plugin-jsdoc": "^4.0.0",
"@prisma-labs/prettier-config": "0.1.0",
"@prisma/client": "2.18.0",
"@prisma/sdk": "^2.18.0",
"@prisma/client": "2.22.1",
"@prisma/sdk": "^2.22.1",
"@types/debug": "^4.1.5",
"@types/jest": "26.0.20",
"@types/jest": "26.0.23",
"@types/lodash": "^4.14.168",
"@types/pluralize": "^0.0.29",
"@types/semver": "^7.3.4",
"@types/semver": "^7.3.5",
"@types/strip-ansi": "^5.2.1",
"@typescript-eslint/eslint-plugin": "^4.16.1",
"@typescript-eslint/parser": "^4.16.1",
"@typescript-eslint/eslint-plugin": "^4.22.1",
"@typescript-eslint/parser": "^4.22.1",
"dripip": "0.10.0",
"eslint": "^7.21.0",
"eslint-config-prettier": "^8.1.0",
"eslint": "^7.25.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-only-warn": "^1.0.2",
"execa": "^5.0.0",
"graphql": "^15.5.0",
"jest": "26.6.3",
"jest-watch-typeahead": "0.6.1",
"jest-watch-typeahead": "0.6.3",
"lodash": "^4.17.21",
"markdown-toc": "^1.2.0",
"nexus": "^1.0.0",
"nodemon": "^2.0.7",
"prettier": "2.2.1",
"prisma": "2.18.0",
"strip-ansi": "^6.0.0",
"ts-jest": "26.5.3",
"prisma": "2.22.1",
"strip-ansi": "6",
"ts-jest": "26.5.6",
"ts-node": "^9.1.1",
"type-fest": "^0.21.2",
"typescript": "^4.2.3"
"type-fest": "^1.1.0",
"typescript": "^4.2.4"
},
"prettier": "@prisma-labs/prettier-config",
"peerDependencies": {
"@prisma/client": "2.17.x || 2.18.x",
"nexus": "^1.0.0"
},
"dependencies": {
"@prisma/generator-helper": "^2.18.0",
"@prisma/generator-helper": "^2.22.1",
"debug": "^4.3.1",
"endent": "^2.0.1",
"fs-jetpack": "^4.1.0",
"graphql-scalars": "^1.9.0",
"graphql-scalars": "^1.9.3",
"kleur": "^4.1.4",
"ono": "^7.1.3",
"pkg-up": "^3.1.0",
"pluralize": "^8.0.0",
"semver": "^7.3.4",
"setset": "^0.0.6",
"tslib": "^2.1.0"
"semver": "^7.3.5",
"setset": "^0.0.7",
"tslib": "^2.2.0"
},
"nodemonConfig": {
"events": {
Expand Down
82 changes: 82 additions & 0 deletions src/generator/helpers/constraints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { DMMF } from '@prisma/client/runtime'
import { RecordUnknown } from '../../helpers/utils'

/**
* Find the unique identifiers necessary to indentify a field
*
* Unique fields for a model can be one of (in this order):
* 1. One (and only one) field with an @id annotation
* 2. Multiple fields with @@id clause
* 3. One (and only one) field with a @unique annotation (if there are multiple, use the first one)
* 4. Multiple fields with a @@unique clause
*/
export function resolveUniqueIdentifiers(model: DMMF.Model): string[] {
// Try finding 1.
const singleIdField = model.fields.find((f) => f.isId)

if (singleIdField) {
return [singleIdField.name]
}

// Try finding 2.
if (model.idFields && model.idFields.length > 0) {
return model.idFields
}

// Try finding 3.
const singleUniqueField = model.fields.find((f) => f.isUnique)

if (singleUniqueField) {
return [singleUniqueField.name]
}

// Try finding 4.
if (model.uniqueFields && model.uniqueFields.length > 0) {
return model.uniqueFields[0] as string[] // I don't know why typescript want a cast here
}

throw new Error(`Unable to resolve a unique identifier for the Prisma model: ${model.name}`)
}

export function findMissingUniqueIdentifiers(
data: RecordUnknown,
uniqueIdentifiers: string[]
): string[] | null {
const missingIdentifiers: string[] = []

for (const identifier of uniqueIdentifiers) {
if (!data[identifier]) {
missingIdentifiers.push(identifier)
}
}

if (missingIdentifiers.length > 0) {
return missingIdentifiers
}

return null
}

export function buildWhereUniqueInput(data: RecordUnknown, uniqueIdentifiers: string[]): RecordUnknown {
if (uniqueIdentifiers.length === 1) {
return pickFromRecord(data, uniqueIdentifiers)
}

const compoundName = uniqueIdentifiers.join('_')

return {
[compoundName]: pickFromRecord(data, uniqueIdentifiers),
}
}

function pickFromRecord(record: RecordUnknown, keys: string[]) {
const output: Record<string, unknown> = {}

for (const identifier of keys) {
if (record[identifier]) {
output[identifier] = record[identifier]
}
}

return output
}
5 changes: 5 additions & 0 deletions src/generator/models/declaration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,11 @@ function renderTypeScriptDeclarationForField({
* The documentation of this field.
*/
description: ${field.documentation ? `string` : `undefined`}
/**
* The resolver of this field
*/
resolve: NexusCore.FieldResolver<'${model.name}', '${field.name}'>
}
`
}
Expand Down
91 changes: 88 additions & 3 deletions src/generator/models/javascript.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { PrismaClient } from '.prisma/client'
import { DMMF } from '@prisma/client/runtime'
import endent from 'endent'
import { chain } from 'lodash'
import { chain, lowerFirst } from 'lodash'
import * as Nexus from 'nexus'
import { NexusEnumTypeConfig, NexusListDef, NexusNonNullDef, NexusNullDef } from 'nexus/dist/core'
import { MaybePromise, RecordUnknown, Resolver } from '../../helpers/utils'
import {
buildWhereUniqueInput,
findMissingUniqueIdentifiers,
resolveUniqueIdentifiers,
} from '../helpers/constraints'
import { ModuleSpec } from '../types'
import { fieldTypeToGraphQLType } from './declaration'

Expand Down Expand Up @@ -68,13 +75,14 @@ function createNexusObjectTypeDefConfigurations(dmmf: DMMF.Document): NexusObjec
.map((model) => {
return {
$name: model.name,
$description: model.documentation ?? undefined,
$description: model.documentation,
...chain(model.fields)
.map((field) => {
return {
name: field.name,
type: prismaFieldToNexusType(field),
description: field.documentation ?? undefined,
description: field.documentation,
resolve: prismaFieldToNexusResolver(model, field),
}
})
.keyBy('name')
Expand All @@ -99,6 +107,83 @@ export function prismaFieldToNexusType(field: DMMF.Field) {
}
}

export function prismaFieldToNexusResolver(model: DMMF.Model, field: DMMF.Field): undefined | Resolver {
/**
* Allow Nexus default resolver to handle resolving scalars.
*
* By using Nexus default we also affect its generated types, assuming there are not explicit source types setup
* which actually for Nexus Prisma projects there usually will be (the Prisma model types). Still, using the Nexus
* default is a bit more idiomatic and provides the better _default_ type generation experience of scalars being
* expected to come down from the source type (aka. parent).
*
* So:
*
* t.field(M1.Foo.bar.$name, M1.Foo.bar)
*
* where `bar` is a scalar prisma field would have NO resolve generated and thus default Nexus as mentioned would
* think that `bar` field WILL be present on the source type. This is, again, mostly moot since most Nexus Prisma
* users WILL setup the Prisma source types e.g.:
*
* sourceTypes: {
* modules: [{ module: '.prisma/client', alias: 'PrismaClient' }],
* },
*
* but this is overall the better way to handle this detail it seems.
*/
if (field.kind !== 'object') {
return undefined
}

return (root: RecordUnknown, _args: RecordUnknown, ctx: RecordUnknown): MaybePromise<unknown> => {
if (!ctx.prisma) {
// TODO rich errors
throw new Error(
'Prisma client not found in context. Set a Prisma client instance to `prisma` field of Nexus context'
)
}

const uniqueIdentifiers = resolveUniqueIdentifiers(model)
const missingIdentifiers = findMissingUniqueIdentifiers(root, uniqueIdentifiers)

if (missingIdentifiers !== null) {
// TODO rich errors
throw new Error(
`Resolver ${model.name}.${
field.name
} is missing the following unique identifiers: ${missingIdentifiers.join(', ')}`
)
}

if (!(ctx.prisma instanceof PrismaClient)) {
// TODO rich errors
throw new Error(`todo`)
}

const methodName = 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]

// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (typeof prismaModel.findUnique !== 'function') {
// TODO rich errors
throw new Error(`todo`)
}

// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const findUnique = prismaModel.findUnique as (query: unknown) => MaybePromise<unknown>

const result: unknown = findUnique({
where: buildWhereUniqueInput(root, uniqueIdentifiers),
})

// @ts-expect-error Only known at runtime
// eslint-disable-next-line
return result[field.name]()
}
}

type AnyNexusEnumTypeConfig = NexusEnumTypeConfig<string>

type NexusEnumTypeDefConfigurations = Record<PrismaEnumName, NexusEnumTypeDefConfiguration>
Expand Down
6 changes: 6 additions & 0 deletions src/helpers/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { inspect } from 'util'

export type Resolver = (root: RecordUnknown, args: RecordUnknown, ctx: RecordUnknown) => MaybePromise<unknown>

export type MaybePromise<T> = T | Promise<T>

export type RecordUnknown<T = unknown> = Record<string, T>

export function allCasesHandled(x: never): never {
// Should never happen, but in case it does :)
// eslint-disable-next-line
Expand Down
8 changes: 4 additions & 4 deletions src/scalars/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,10 @@ import { Json } from './Json'
* types: [asNexusMethod(jsonScalar, 'json'), asNexusMethod(dateTimeScalar, 'dateTime')],
* })
*
* @remarks Some Of the Prisma scalars do not have a natural standard representation in GraphQL. For these
* cases Nexus Prisma generates code that references type names matching those scalar names
* in Prisma. Then, you are expected to define those custom scalar types in your GraphQL
* API. For convenience you can use these ones.
* @remarks Some Of the Prisma scalars do not have a natural standard representation in GraphQL. For
* these cases Nexus Prisma generates code that references type names matching those scalar
* names in Prisma. Then, you are expected to define those custom scalar types in your GraphQL
* API. For convenience you can use these ones.
*/
const NexusPrismaScalars = {
DateTime,
Expand Down
12 changes: 3 additions & 9 deletions tests/e2e/__snapshots__/e2e.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ enum E1 {
\\"\\"\\"
The \`JSONObject\` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).
\\"\\"\\"
scalar Json
scalar Json @specifiedBy(url: \\"http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf\\")
type M1 {
DateTimeManually: DateTime
Expand All @@ -41,7 +41,7 @@ exports[`When bundled custom scalars are used the project type checks and genera
* Do not make changes to this file directly
*/
import * as PrismaClient from \\".prisma/client\\"
import { core } from \\"nexus\\"
declare global {
interface NexusGenCustomInputMethods<TypeName extends string> {
Expand Down Expand Up @@ -91,13 +91,7 @@ export interface NexusGenScalars {
}
export interface NexusGenObjects {
M1: { // root type
DateTimeManually?: NexusGenScalars['DateTime'] | null; // DateTime
JsonManually?: NexusGenScalars['Json'] | null; // Json
e1?: NexusGenEnums['E1'] | null; // E1
someDateTimeField: NexusGenScalars['DateTime']; // DateTime!
someJsonField: NexusGenScalars['Json']; // Json!
}
M1: PrismaClient.M1;
Query: {};
}
Expand Down
Loading

0 comments on commit 4f5cd70

Please sign in to comment.