feat: add onObjectDefinition / onInputObjectDefinition (#533)
This commit is contained in:
parent
0dd8ea367b
commit
b4e0debd95
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
codecov:
|
||||
require_ci_to_pass: no
|
||||
notify:
|
||||
wait_for_ci: no
|
||||
|
||||
comment:
|
||||
layout: "diff"
|
||||
layout: 'diff'
|
||||
|
||||
coverage:
|
||||
status:
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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')
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -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) => {
|
||||
|
|
Loading…
Reference in New Issue