feat: add onObjectDefinition / onInputObjectDefinition (#533)

This commit is contained in:
Tim Griesser 2020-10-07 13:08:43 -04:00 committed by GitHub
parent 0dd8ea367b
commit b4e0debd95
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 223 additions and 17 deletions

View File

@ -20,6 +20,7 @@ jobs:
run: yarn -s test:ci
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1
if: matrix.os == 'ubuntu-latest' && matrix.node-version == '10.x'
with:
directory: ./coverage

View File

@ -21,6 +21,7 @@ jobs:
- name: Test
run: yarn -s test:ci
- name: Upload coverage to Codecov
if: matrix.os == 'ubuntu-latest' && matrix.node-version == '10.x'
uses: codecov/codecov-action@v1
with:
directory: ./coverage

View File

@ -1,5 +1,10 @@
codecov:
require_ci_to_pass: no
notify:
wait_for_ci: no
comment:
layout: "diff"
layout: 'diff'
coverage:
status:

View File

@ -78,6 +78,49 @@ plugin({
})
```
### onObjectDefinition(t, objectConfig)
The "onObjectDefinition" hook is called when an `objectType` is created, and is provided `t`, the object
passed into the `definition` block, as well as the `config` of the object.
```ts
export const NodePlugin = plugin({
name: 'NodePlugin',
description: 'Allows us to designate the field used to determine the "node" interface',
objectTypeDefTypes: `node?: string | core.FieldResolver<TypeName, any>`,
onObjectDefinition(t, { node }) {
if (node) {
let resolveFn
if (typeof node === 'string') {
const fieldResolve: FieldResolver<any, any> = (root, args, ctx, info) => {
return `${info.parentType.name}:${root[node]}`
}
resolveFn = fieldResolve
} else {
resolveFn = node
}
t.implements('Node')
t.id('id', {
nullable: false,
resolve: resolveFn,
})
}
},
})
```
Usage:
```ts
const User = objectType({
name: 'User',
node: 'id', // adds `id` field
definition(t) {
t.string('name')
},
})
```
### onCreateFieldResolver(config)
Every ObjectType, whether they are defined via Nexus' `objectType` api, or elsewhere is given a resolver.

View File

@ -1,4 +1,4 @@
import { plugin } from '@nexus/schema'
import { plugin, interfaceType, FieldResolver } from '@nexus/schema'
export const logMutationTimePlugin = plugin({
name: 'LogMutationTime',
@ -15,3 +15,50 @@ export const logMutationTimePlugin = plugin({
}
},
})
export const NodePlugin = plugin({
name: 'NodePlugin',
description: 'Allows us to designate the field used to ',
objectTypeDefTypes: `node?: string | core.FieldResolver<TypeName, any>`,
onObjectDefinition(t, { node }) {
if (node) {
let resolveFn
if (typeof node === 'string') {
const fieldResolve: FieldResolver<any, any> = (root, args, ctx, info) => {
return `${info.parentType.name}:${root[node]}`
}
resolveFn = fieldResolve
} else {
resolveFn = node
}
t.implements('Node')
t.id('id', {
nullable: false,
resolve: resolveFn,
})
}
},
onMissingType(t, builder) {
if (t === 'Node') {
return interfaceType({
name: 'Node',
description:
'A "Node" is a field with a required ID field (id), per the https://relay.dev/docs/en/graphql-server-specification',
definition(t) {
t.id('id', {
nullable: false,
resolve: () => {
throw new Error('Abstract')
},
})
t.resolveType((t) => {
if (t.__typename) {
return t.__typename
}
throw new Error('__typename missing for resolving Node')
})
},
})
}
},
})

View File

@ -10,7 +10,7 @@ import { ApolloServer } from 'apollo-server'
import { separateOperations } from 'graphql'
import { fieldExtensionsEstimator, getComplexity, simpleEstimator } from 'graphql-query-complexity'
import path from 'path'
import { logMutationTimePlugin } from './example-plugins'
import { logMutationTimePlugin, NodePlugin } from './example-plugins'
import * as types from './kitchen-sink-definitions'
const DEBUGGING_CURSOR = false
@ -24,6 +24,7 @@ const schema = makeSchema({
typegen: path.join(__dirname, './kitchen-sink.gen.ts'),
},
plugins: [
NodePlugin,
connectionPlugin({
encodeCursor: fn,
decodeCursor: fn,

View File

@ -31,18 +31,6 @@ export const testArgs2 = {
bar: idArg(),
}
export const Node = interfaceType({
name: 'Node',
definition(t) {
t.id('id', {
nullable: false,
resolve: () => {
throw new Error('Abstract')
},
})
},
})
export const Mutation = mutationType({
definition(t) {
t.boolean('ok', () => true)
@ -118,6 +106,7 @@ export const TestUnion = unionType({
export const TestObj = objectType({
name: 'TestObj',
node: (obj) => `TestObj:${obj.item}`,
definition(t) {
t.implements('Bar', Baz)
t.string('item')

View File

@ -613,6 +613,7 @@ enum NodeFlags {
Namespace
NestedNamespace
None
OptionalChain
PermanentlySetIncrementalFlags
PossiblyContainsDynamicImport
PossiblyContainsImportMeta
@ -621,6 +622,7 @@ enum NodeFlags {
Synthesized
ThisNodeHasError
ThisNodeOrAnySubNodesHasError
TypeCached
TypeExcludesFlags
UNKNOWN
YieldContext
@ -842,6 +844,7 @@ enum SyntaxKind {
ArrowFunction
AsExpression
AsKeyword
AssertsKeyword
AsteriskAsteriskEqualsToken
AsteriskAsteriskToken
AsteriskEqualsToken
@ -942,6 +945,7 @@ enum SyntaxKind {
FirstNode
FirstPunctuation
FirstReservedWord
FirstStatement
FirstTemplateToken
FirstToken
FirstTriviaToken
@ -994,12 +998,17 @@ enum SyntaxKind {
JSDocComment
JSDocEnumTag
JSDocFunctionType
JSDocImplementsTag
JSDocNamepathType
JSDocNonNullableType
JSDocNullableType
JSDocOptionalType
JSDocParameterTag
JSDocPrivateTag
JSDocPropertyTag
JSDocProtectedTag
JSDocPublicTag
JSDocReadonlyTag
JSDocReturnTag
JSDocSignature
JSDocTag
@ -1037,6 +1046,7 @@ enum SyntaxKind {
LastLiteralToken
LastPunctuation
LastReservedWord
LastStatement
LastTemplateToken
LastToken
LastTriviaToken
@ -1063,6 +1073,7 @@ enum SyntaxKind {
MultiLineCommentTrivia
NamedExports
NamedImports
NamespaceExport
NamespaceExportDeclaration
NamespaceImport
NamespaceKeyword
@ -1097,6 +1108,7 @@ enum SyntaxKind {
PlusToken
PostfixUnaryExpression
PrefixUnaryExpression
PrivateIdentifier
PrivateKeyword
PropertyAccessExpression
PropertyAssignment
@ -1105,6 +1117,8 @@ enum SyntaxKind {
ProtectedKeyword
PublicKeyword
QualifiedName
QuestionDotToken
QuestionQuestionToken
QuestionToken
ReadonlyKeyword
RegularExpressionLiteral
@ -1133,6 +1147,7 @@ enum SyntaxKind {
SymbolKeyword
SyntaxList
SyntheticExpression
SyntheticReferenceExpression
TaggedTemplateExpression
TemplateExpression
TemplateHead

View File

@ -403,6 +403,16 @@ export class SchemaBuilder {
*/
protected onAfterBuildFns: Exclude<PluginConfig['onAfterBuild'], undefined>[] = []
/**
* Executed after the object is defined, allowing us to add additional fields to the object
*/
protected onObjectDefinitionFns: Exclude<PluginConfig['onObjectDefinition'], undefined>[] = []
/**
* Executed after the object is defined, allowing us to add additional fields to the object
*/
protected onInputObjectDefinitionFns: Exclude<PluginConfig['onInputObjectDefinition'], undefined>[] = []
/**
* The `schemaExtension` is created just after the types are walked,
* but before the fields are materialized.
@ -673,6 +683,12 @@ export class SchemaBuilder {
if (pluginConfig.onAfterBuild) {
this.onAfterBuildFns.push(pluginConfig.onAfterBuild)
}
if (pluginConfig.onObjectDefinition) {
this.onObjectDefinitionFns.push(pluginConfig.onObjectDefinition)
}
if (pluginConfig.onInputObjectDefinition) {
this.onInputObjectDefinitionFns.push(pluginConfig.onInputObjectDefinition)
}
})
}
@ -761,6 +777,9 @@ export class SchemaBuilder {
warn: consoleWarn,
})
config.definition(definitionBlock)
this.onInputObjectDefinitionFns.forEach((fn) => {
fn(definitionBlock, config)
})
const extensions = this.inputTypeExtendMap[config.name]
if (extensions) {
extensions.forEach((extension) => {
@ -790,6 +809,9 @@ export class SchemaBuilder {
warn: consoleWarn,
})
config.definition(definitionBlock)
this.onObjectDefinitionFns.forEach((fn) => {
fn(definitionBlock, config)
})
const extensions = this.typeExtendMap[config.name]
if (extensions) {
extensions.forEach((extension) => {

View File

@ -11,6 +11,9 @@ import {
} from './definitions/_types'
import { NexusSchemaExtension } from './extensions'
import { isPromiseLike, PrintedGenTyping, PrintedGenTypingImport, venn } from './utils'
import { NexusObjectTypeConfig, ObjectDefinitionBlock } from './definitions/objectType'
import { InputDefinitionBlock } from './definitions/definitionBlocks'
import { NexusInputObjectTypeConfig } from './definitions/inputObjectType'
export { PluginBuilderLens }
@ -89,6 +92,19 @@ export interface PluginConfig {
* After the schema is built, provided the Schema to do any final config validation.
*/
onAfterBuild?: (schema: GraphQLSchema) => void
/**
* Called immediately after the object is defined, allows for using metadata
* to define the shape of the object.
*/
onObjectDefinition?: (block: ObjectDefinitionBlock<any>, objectConfig: NexusObjectTypeConfig<any>) => void
/**
* Called immediately after the input object is defined, allows for using metadata
* to define the shape of the input object
*/
onInputObjectDefinition?: (
block: InputDefinitionBlock<any>,
objectConfig: NexusInputObjectTypeConfig<any>
) => void
/**
* If a type is not defined in the schema, our plugins can register an `onMissingType` handler,
* which will intercept the missing type name and give us an opportunity to respond with a valid
@ -206,6 +222,8 @@ function validatePluginConfig(pluginConfig: PluginConfig): void {
'onBeforeBuild',
'onMissingType',
'onAfterBuild',
'onObjectDefinition',
'onInputObjectDefinition',
]
const validOptionalProps = ['description', 'fieldDefTypes', 'objectTypeDefTypes', ...optionalPropFns]

View File

@ -1,5 +1,5 @@
import { buildSchema, graphql, GraphQLSchema, printSchema } from 'graphql'
import { makeSchema, MiddlewareFn, objectType, plugin, queryField } from '../src/core'
import { buildSchema, graphql, GraphQLSchema, printSchema, introspectionFromSchema } from 'graphql'
import { makeSchema, MiddlewareFn, objectType, plugin, queryField, interfaceType } from '../src/core'
import { nullabilityGuardPlugin } from '../src/plugins'
import { EXAMPLE_SDL } from './_sdl'
@ -203,6 +203,70 @@ describe('plugin', () => {
)
expect(calls).toMatchSnapshot()
})
it('has an onObjectDefinition option, which receives the object metadata', async () => {
//
const schema = makeSchema({
outputs: false,
types: [
interfaceType({
name: 'Node',
definition(t) {
t.id('id', {
nullable: false,
resolve: () => {
throw new Error('Abstract')
},
})
t.resolveType((n) => n.__typename)
},
}),
objectType({
name: 'AddsNode',
// @ts-ignore
node: 'id',
definition(t) {
t.string('name')
},
}),
queryField('getNode', {
type: 'Node',
resolve: () => ({ __typename: 'AddsNode', name: 'test', id: 'abc' }),
}),
],
plugins: [
plugin({
name: 'Node',
onObjectDefinition(t, config) {
const node = (config as any).node as any
if (node) {
t.implements('Node')
t.id('id', {
nullable: false,
resolve: (root) => `${config.name}:${root[node]}`,
})
}
},
}),
],
})
const result = await graphql(
schema,
`
{
getNode {
__typename
id
... on AddsNode {
name
}
}
}
`
)
expect(result.data?.getNode).toEqual({ __typename: 'AddsNode', id: 'AddsNode:abc', name: 'test' })
})
it('has a plugin.completeValue fn which is used to efficiently complete a value which is possibly a promise', async () => {
const calls: string[] = []
const testResolve = (name: string) => (): MiddlewareFn => async (root, args, ctx, info, next) => {