improve: remove resolver shorthands (#592)

closes #582

As discussed. In most cases resolver shorthands are not a serious use of the API.

BREAKING CHANGE:

Resolver shorthand API is now removed. The following will now not typecheck:

```ts
t.string('foo', () => ... )
```

Instead use:

```ts
t.string('foo', { resolve: () => ... })
```

Runtime support is still intact but will result in a logged warning. Runtime support will be removed in the next Nexus release.
This commit is contained in:
Jason Kuhrt 2020-11-04 12:03:29 -05:00 committed by GitHub
parent b19e83ebfc
commit f60938079a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 101 additions and 95 deletions

View File

@ -1,31 +1,33 @@
import { objectType } from "@nexus/schema";
import dedent from "dedent";
import dedent from 'dedent'
import { objectType } from '@nexus/schema'
export const Launch = objectType({
name: "Launch",
name: 'Launch',
definition: (t) => {
t.id("id");
t.string("site", { nullable: true });
t.field("mission", { type: "Mission" });
t.field("rocket", { type: "Rocket" });
t.boolean("isBooked", (launch, _, { dataSources }) => {
return dataSources.userAPI.isBookedOnLaunch({
launchId: `${launch.id}`,
});
});
t.id('id')
t.string('site', { nullable: true })
t.field('mission', { type: 'Mission' })
t.field('rocket', { type: 'Rocket' })
t.boolean('isBooked', {
resolve: (launch, _, { dataSources }) => {
return dataSources.userAPI.isBookedOnLaunch({
launchId: `${launch.id}`,
})
},
})
},
});
})
export const LaunchConnection = objectType({
name: "LaunchConnection",
name: 'LaunchConnection',
description: dedent`
Simple wrapper around our list of launches that contains a cursor to the
last item in the list. Pass this cursor to the launches query to fetch results
after these.
`,
definition: (t) => {
t.string("cursor", { nullable: true });
t.boolean("hasMore");
t.field("launches", { type: "Launch", list: [false] });
t.string('cursor', { nullable: true })
t.boolean('hasMore')
t.field('launches', { type: 'Launch', list: [false] })
},
});
})

View File

@ -1,6 +1,7 @@
{
"name": "@graphql-nexus/example-kitchen-sink",
"version": "0.0.0",
"license": "MIT",
"scripts": {
"build": "ts-node --log-error src/index.ts",
"debug": "ts-node-dev --inspect-brk --no-notify --transpileOnly --respawn ./src",

View File

@ -1,21 +1,21 @@
import { connectionFromArray } from 'graphql-relay'
import _ from 'lodash'
import {
objectType,
inputObjectType,
interfaceType,
unionType,
arg,
booleanArg,
connectionPlugin,
extendType,
scalarType,
intArg,
idArg,
inputObjectType,
intArg,
interfaceType,
mutationField,
mutationType,
booleanArg,
objectType,
queryField,
connectionPlugin,
scalarType,
unionType,
} from '@nexus/schema'
import _ from 'lodash'
import { connectionFromArray } from 'graphql-relay'
const USERS_DATA = _.times(100, (i) => ({
pk: i,
@ -33,7 +33,7 @@ export const testArgs2 = {
export const Mutation = mutationType({
definition(t) {
t.boolean('ok', () => true)
t.boolean('ok', { resolve: () => true })
},
})
@ -194,7 +194,7 @@ export const Query = objectType({
},
resolve: () => 'ok',
})
t.list.date('dateAsList', () => [])
t.list.date('dateAsList', { resolve: () => [] })
t.connectionField('booleanConnection', {
type: 'Boolean',

View File

@ -1,6 +1,7 @@
{
"name": "@graphql-nexus/ts-ast-reader",
"version": "0.0.0",
"license": "MIT",
"scripts": {
"start": "ts-node-dev --no-notify --transpileOnly --respawn ./src"
},

View File

@ -1,90 +1,90 @@
import { interfaceType, arg } from "@nexus/schema";
import { SyntaxKind, JSDoc } from "typescript";
import { allKnownNodes, syntaxKindFilter } from "./utils";
import { JSDoc, SyntaxKind } from 'typescript'
import { arg, interfaceType } from '@nexus/schema'
import { allKnownNodes, syntaxKindFilter } from './utils'
const syntaxKindArgs = {
skip: arg({ type: "SyntaxKind", list: true }),
only: arg({ type: "SyntaxKind", list: true }),
};
skip: arg({ type: 'SyntaxKind', list: true }),
only: arg({ type: 'SyntaxKind', list: true }),
}
export const MaybeOptional = interfaceType({
name: "MaybeOptional",
name: 'MaybeOptional',
definition(t) {
t.field("questionToken", { type: "Token", nullable: true });
t.resolveType((o) => o.kind as any);
t.field('questionToken', { type: 'Token', nullable: true })
t.resolveType((o) => o.kind as any)
},
});
})
export const Node = interfaceType({
name: "Node",
name: 'Node',
definition(t) {
t.int("pos");
t.int("end");
t.string("nameText", {
t.int('pos')
t.int('end')
t.string('nameText', {
nullable: true,
resolve: (root) =>
// @ts-ignore
root.name ? root.name.escapedText : null,
});
t.field("name", { type: "DeclarationName", nullable: true });
t.field("typeName", { type: "DeclarationName", nullable: true });
t.field("kind", { type: "SyntaxKind" });
t.int("kindCode", (o) => o.kind);
t.field("flags", { type: "NodeFlags" });
})
t.field('name', { type: 'DeclarationName', nullable: true })
t.field('typeName', { type: 'DeclarationName', nullable: true })
t.field('kind', { type: 'SyntaxKind' })
t.int('kindCode', { resolve: (o) => o.kind })
t.field('flags', { type: 'NodeFlags' })
// t.field('decorators', 'Decorator', {list: true, nullable: true})
t.list.field("modifiers", {
type: "Token",
t.list.field('modifiers', {
type: 'Token',
nullable: true,
args: syntaxKindArgs,
async resolve(root, args) {
if (!root.modifiers) {
return null;
return null
}
return syntaxKindFilter(args, Array.from(root.modifiers));
return syntaxKindFilter(args, Array.from(root.modifiers))
},
});
t.field("parent", { type: "Node" });
t.string("rawText", {
})
t.field('parent', { type: 'Node' })
t.string('rawText', {
args: syntaxKindArgs,
resolve(root, args, ctx) {
const filtered = syntaxKindFilter(args, [root]);
return filtered.length ? filtered[0].getText(ctx.source) : "";
const filtered = syntaxKindFilter(args, [root])
return filtered.length ? filtered[0].getText(ctx.source) : ''
},
});
})
t.resolveType((node, ctx, info) => {
if (KeywordKinds.has(node.kind)) {
return "KeywordTypeNode";
return 'KeywordTypeNode'
}
if (allKnownNodes(info.schema).has(SyntaxKind[node.kind])) {
return SyntaxKind[node.kind] as any;
return SyntaxKind[node.kind] as any
}
return "UNKNOWN_NODE";
});
return 'UNKNOWN_NODE'
})
},
});
})
export const JSDocInterface = interfaceType({
name: "HasJSDoc",
name: 'HasJSDoc',
definition(t) {
t.list.field("jsDoc", {
type: "JSDoc",
t.list.field('jsDoc', {
type: 'JSDoc',
nullable: true,
resolve(root) {
if ("jsDoc" in root) {
if ('jsDoc' in root) {
// https://github.com/Microsoft/TypeScript/issues/19856
return ((root as unknown) as { jsDoc: JSDoc[] }).jsDoc;
return ((root as unknown) as { jsDoc: JSDoc[] }).jsDoc
}
return null;
return null
},
});
})
t.resolveType((node, ctx, info) => {
if (allKnownNodes(info.schema).has(SyntaxKind[node.kind])) {
return SyntaxKind[node.kind] as any;
return SyntaxKind[node.kind] as any
}
return "UNKNOWN_NODE";
});
return 'UNKNOWN_NODE'
})
},
});
})
const KeywordKinds = new Set([
SyntaxKind.AnyKeyword,
@ -100,4 +100,4 @@ const KeywordKinds = new Set([
SyntaxKind.UndefinedKeyword,
SyntaxKind.NullKeyword,
SyntaxKind.NeverKeyword,
]);
])

View File

@ -1,8 +1,8 @@
import { GraphQLFieldResolver } from 'graphql'
import { AllInputTypes, FieldResolver, GetGen, GetGen3, HasGen3, NeedsResolver } from '../typegenTypeHelpers'
import { BaseScalars } from './_types'
import { ArgsRecord } from './args'
import { AllNexusInputTypeDefs, AllNexusOutputTypeDefs } from './wrapping'
import { BaseScalars } from './_types'
export interface CommonFieldConfig {
/**
@ -59,16 +59,14 @@ export type NexusOutputFieldDef = NexusOutputFieldConfig<string, any> & {
// prettier-ignore
export type ScalarOutSpread<TypeName extends string, FieldName extends string> =
NeedsResolver<TypeName, FieldName> extends true
? HasGen3<'argTypes', TypeName, FieldName> extends true
? [ScalarOutConfig<TypeName, FieldName>]
: [ScalarOutConfig<TypeName, FieldName>] | [FieldResolver<TypeName, FieldName>]
? [ScalarOutConfig<TypeName, FieldName>]
: HasGen3<'argTypes', TypeName, FieldName> extends true
? [ScalarOutConfig<TypeName, FieldName>]
: [] | [FieldResolver<TypeName, FieldName>] | [ScalarOutConfig<TypeName, FieldName>]
: [ScalarOutConfig<TypeName, FieldName>] | []
// prettier-ignore
export type ScalarOutConfig<TypeName extends string, FieldName extends string> =
NeedsResolver<TypeName, FieldName> extends true
NeedsResolver<TypeName, FieldName> extends true
? OutputScalarConfig<TypeName, FieldName> &
{
resolve: FieldResolver<TypeName, FieldName>
@ -160,12 +158,16 @@ export class OutputDefinitionBlock<TypeName extends string> {
name: fieldName,
type: typeName,
}
if (typeof opts[0] === 'function') {
// FIXME ditto to the one in `field` method
config.resolve = opts[0] as any
console.warn(
`Since v0.18.0 Nexus no longer supports resolver shorthands like:\n\n t.string("${fieldName}", () => ...).\n\nInstead please write:\n\n t.string("${fieldName}", { resolve: () => ... })\n\nIn the next version of Nexus this will be a runtime error.`
)
} else {
config = { ...config, ...opts[0] }
}
this.typeBuilder.addField(this.decorateField(config))
}

View File

@ -7,9 +7,9 @@ import { inputObjectType, objectType } from '../../src'
export const UserObject = objectType({
name: 'User',
definition(t) {
t.id('id', () => `User:1` as any)
t.string('email', () => 'test@example.com' as any)
t.string('name', () => `Test User` as any)
t.id('id', { resolve: () => `User:1` as any })
t.string('email', { resolve: () => 'test@example.com' as any })
t.string('name', { resolve: () => `Test User` as any })
},
})

View File

@ -1,5 +1,5 @@
import * as path from 'path'
import { core, enumType, makeSchema, objectType, queryType } from '..'
import { core, enumType, makeSchema, objectType, queryType } from '../'
import { A, B } from './_types'
const { TypegenPrinter, TypegenMetadata } = core
@ -121,7 +121,7 @@ describe('rootTypings', () => {
name: 'SomeType',
rootTyping: {
name: 'invalid',
path: 'fzeffezpokm',
path: './fzeffezpokm',
},
definition(t) {
t.id('id')
@ -140,7 +140,7 @@ describe('rootTypings', () => {
})
expect(() => typegen.print()).toThrowErrorMatchingInlineSnapshot(
`"Expected an absolute path for the root typing path of the type SomeType, saw fzeffezpokm"`
`"Expected an absolute path for the root typing path of the type SomeType, saw ./fzeffezpokm"`
)
})
@ -174,7 +174,7 @@ describe('rootTypings', () => {
typegen.print()
} catch (e) {
expect(e.message.replace(__dirname, '')).toMatchInlineSnapshot(
`"Root typing path /invalid_path.ts of the type SomeType does not exist"`
`"Root typing path /invalid_path.ts for the type SomeType does not exist"`
)
}
})

View File

@ -16,7 +16,7 @@ import { mockStream } from '../../__helpers'
export const query = queryType({
definition(t) {
t.string('foo', () => 'bar')
t.string('foo', { resolve: () => 'bar' })
},
})

View File

@ -640,7 +640,7 @@ describe('field level configuration', () => {
{},
{
extendConnection(t) {
t.int('totalCount', () => 1)
t.int('totalCount', { resolve: () => 1 })
},
}
)
@ -653,7 +653,7 @@ describe('field level configuration', () => {
{},
{
extendEdge(t) {
t.string('role', () => 'admin')
t.string('role', { resolve: () => 'admin' })
},
}
)

View File

@ -9,7 +9,7 @@ describe('unionType', () => {
objectType({
name: 'DeletedUser',
definition(t) {
t.string('message', (root) => `This user ${root.id} was deleted`)
t.string('message', { resolve: (root) => `This user ${root.id} was deleted` })
},
rootTyping: `{ id: number; deletedAt: Date }`,
}),