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 run: yarn -s test:ci
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@v1 uses: codecov/codecov-action@v1
if: matrix.os == 'ubuntu-latest' && matrix.node-version == '10.x'
with: with:
directory: ./coverage directory: ./coverage

View File

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

View File

@ -1,5 +1,10 @@
codecov:
require_ci_to_pass: no
notify:
wait_for_ci: no
comment: comment:
layout: "diff" layout: 'diff'
coverage: coverage:
status: 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) ### onCreateFieldResolver(config)
Every ObjectType, whether they are defined via Nexus' `objectType` api, or elsewhere is given a resolver. 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({ export const logMutationTimePlugin = plugin({
name: 'LogMutationTime', 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 { separateOperations } from 'graphql'
import { fieldExtensionsEstimator, getComplexity, simpleEstimator } from 'graphql-query-complexity' import { fieldExtensionsEstimator, getComplexity, simpleEstimator } from 'graphql-query-complexity'
import path from 'path' import path from 'path'
import { logMutationTimePlugin } from './example-plugins' import { logMutationTimePlugin, NodePlugin } from './example-plugins'
import * as types from './kitchen-sink-definitions' import * as types from './kitchen-sink-definitions'
const DEBUGGING_CURSOR = false const DEBUGGING_CURSOR = false
@ -24,6 +24,7 @@ const schema = makeSchema({
typegen: path.join(__dirname, './kitchen-sink.gen.ts'), typegen: path.join(__dirname, './kitchen-sink.gen.ts'),
}, },
plugins: [ plugins: [
NodePlugin,
connectionPlugin({ connectionPlugin({
encodeCursor: fn, encodeCursor: fn,
decodeCursor: fn, decodeCursor: fn,

View File

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

View File

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

View File

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

View File

@ -11,6 +11,9 @@ import {
} from './definitions/_types' } from './definitions/_types'
import { NexusSchemaExtension } from './extensions' import { NexusSchemaExtension } from './extensions'
import { isPromiseLike, PrintedGenTyping, PrintedGenTypingImport, venn } from './utils' 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 } export { PluginBuilderLens }
@ -89,6 +92,19 @@ export interface PluginConfig {
* After the schema is built, provided the Schema to do any final config validation. * After the schema is built, provided the Schema to do any final config validation.
*/ */
onAfterBuild?: (schema: GraphQLSchema) => void 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, * 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 * 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', 'onBeforeBuild',
'onMissingType', 'onMissingType',
'onAfterBuild', 'onAfterBuild',
'onObjectDefinition',
'onInputObjectDefinition',
] ]
const validOptionalProps = ['description', 'fieldDefTypes', 'objectTypeDefTypes', ...optionalPropFns] const validOptionalProps = ['description', 'fieldDefTypes', 'objectTypeDefTypes', ...optionalPropFns]

View File

@ -1,5 +1,5 @@
import { buildSchema, graphql, GraphQLSchema, printSchema } from 'graphql' import { buildSchema, graphql, GraphQLSchema, printSchema, introspectionFromSchema } from 'graphql'
import { makeSchema, MiddlewareFn, objectType, plugin, queryField } from '../src/core' import { makeSchema, MiddlewareFn, objectType, plugin, queryField, interfaceType } from '../src/core'
import { nullabilityGuardPlugin } from '../src/plugins' import { nullabilityGuardPlugin } from '../src/plugins'
import { EXAMPLE_SDL } from './_sdl' import { EXAMPLE_SDL } from './_sdl'
@ -203,6 +203,70 @@ describe('plugin', () => {
) )
expect(calls).toMatchSnapshot() 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 () => { it('has a plugin.completeValue fn which is used to efficiently complete a value which is possibly a promise', async () => {
const calls: string[] = [] const calls: string[] = []
const testResolve = (name: string) => (): MiddlewareFn => async (root, args, ctx, info, next) => { const testResolve = (name: string) => (): MiddlewareFn => async (root, args, ctx, info, next) => {