Skip to content

Commit

Permalink
Allow registering field extensions (#13623)
Browse files Browse the repository at this point in the history
  • Loading branch information
stefanprobst authored and KartSriv committed Apr 30, 2019
1 parent 9b2be71 commit e40f6d2
Show file tree
Hide file tree
Showing 11 changed files with 329 additions and 252 deletions.
84 changes: 0 additions & 84 deletions packages/gatsby/src/schema/add-field-resolvers.js

This file was deleted.

2 changes: 2 additions & 0 deletions packages/gatsby/src/schema/context.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
const { LocalNodeModel } = require(`./node-model`)
const { fieldExtensions } = require(`./extensions`)

const withResolverContext = (context, schema) => {
const nodeStore = require(`../db/nodes`)
const createPageDependency = require(`../redux/actions/add-page-dependency`)

return {
...context,
fieldExtensions,
nodeModel: new LocalNodeModel({
nodeStore,
schema,
Expand Down
210 changes: 210 additions & 0 deletions packages/gatsby/src/schema/extensions/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
const {
GraphQLBoolean,
GraphQLNonNull,
GraphQLDirective,
GraphQLString,
DirectiveLocation,
defaultFieldResolver,
} = require(`graphql`)
const { GraphQLJSON } = require(`graphql-compose`)
const report = require(`gatsby-cli/lib/reporter`)

const { link, fileByPath } = require(`../resolvers`)
const { getDateResolver } = require(`../types/date`)

// Reserved for internal use
const internalExtensionNames = [
`addDefaultResolvers`,
`createdFrom`,
`directives`,
`infer`,
`plugin`,
]

const typeExtensions = {
infer: {
description: `Infer field types from field values.`,
args: {
noDefaultResolvers: {
type: GraphQLBoolean,
description: `Don't add default resolvers to defined fields.`,
deprecationReason: `noDefaultResolvers is deprecated, annotate individual fields.`,
},
},
},
dontInfer: {
description: `Do not infer field types from field values.`,
args: {
noDefaultResolvers: {
type: GraphQLBoolean,
description: `Don't add default resolvers to defined fields.`,
deprecationReason: `noDefaultResolvers is deprecated, annotate individual fields.`,
},
},
},
}

const fieldExtensions = {
add: {
description: `Generic directive to add field extension.`,
args: {
extension: {
type: new GraphQLNonNull(GraphQLString),
},
options: {
type: GraphQLJSON,
},
},
},

addResolver: {
description: `Add a resolver specified by "type" to field.`,
args: {
type: {
type: new GraphQLNonNull(GraphQLString),
description:
`Type of the resolver. Types available by default are: ` +
`"dateformat", "link" and "fileByRelativePath".`,
},
options: {
type: GraphQLJSON,
description: `Resolver options. Vary based on resolver type.`,
},
},
process(args, fieldConfig) {
const { process } = fieldExtensions[args.type] || {}
if (typeof process === `function`) {
return process(args.options || {}, fieldConfig)
}
return {}
},
},

dateformat: {
description: `Add date formating options.`,
args: {
formatString: { type: GraphQLString },
locale: { type: GraphQLString },
},
process(args, fieldConfig) {
return getDateResolver(args)
},
},

link: {
description: `Link to node by foreign-key relation.`,
args: {
by: {
type: new GraphQLNonNull(GraphQLString),
defaultValue: `id`,
},
from: {
type: GraphQLString,
},
},
process(args, fieldConfig) {
return {
resolve: link(args),
}
},
},

fileByRelativePath: {
description: `Link to File node by relative path.`,
args: {
from: {
type: GraphQLString,
},
},
process(args, fieldConfig) {
return {
resolve: fileByPath(args),
}
},
},

// projection: {
// description: `Automatically add fields to selection set.`,
// args: {},
// process(args, fieldConfig) {},
// },

proxyFrom: {
description: `Proxy resolver from another field.`,
process(from, fieldConfig) {
const resolver = fieldConfig.resolve || defaultFieldResolver
return {
resolve(source, args, context, info) {
return resolver(source, args, context, {
...info,
fieldName: from,
})
},
}
},
},
}

const toDirectives = ({ extensions, locations }) =>
Object.keys(extensions).map(name => {
const extension = extensions[name]
const { args, description } = extension
return new GraphQLDirective({ name, args, description, locations })
})

const addDirectives = ({ schemaComposer }) => {
const fieldDirectives = toDirectives({
extensions: fieldExtensions,
locations: [DirectiveLocation.FIELD_DEFINITION],
})
fieldDirectives.forEach(directive => schemaComposer.addDirective(directive))
const typeDirectives = toDirectives({
extensions: typeExtensions,
locations: [DirectiveLocation.OBJECT],
})
typeDirectives.forEach(directive => schemaComposer.addDirective(directive))
}

const processFieldExtensions = ({
schemaComposer,
typeComposer,
parentSpan,
}) => {
typeComposer.getFieldNames().forEach(fieldName => {
const extensions = typeComposer.getFieldExtensions(fieldName)
Object.keys(extensions)
.filter(name => !internalExtensionNames.includes(name))
.sort(a => a === `proxyFrom`) // Ensure `proxyFrom` is run last
.forEach(name => {
const { process } = fieldExtensions[name] || {}
if (process) {
// Always get fresh field config as it will have been changed
// by previous field extension
const prevFieldConfig = typeComposer.getFieldConfig(fieldName)
typeComposer.extendField(
fieldName,
process(extensions[name], prevFieldConfig)
)
}
})
})
}

const registerFieldExtension = (name, extension) => {
if (internalExtensionNames.includes(name)) {
report.error(`The extension ${name} is reserved for internal use.`)
} else if (!fieldExtensions[name]) {
fieldExtensions[name] = extension
} else {
report.error(
`A field extension with the name ${name} is already registered.`
)
}
}

module.exports = {
addDirectives,
fieldExtensions,
processFieldExtensions,
registerFieldExtension,
}
2 changes: 1 addition & 1 deletion packages/gatsby/src/schema/infer/__tests__/infer.js
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ describe(`GraphQL type inference`, () => {
`
with_space
with_hyphen
with_resolver(formatString:"DD.MM.YYYY")
with_resolver(formatString: "DD.MM.YYYY")
_123
_456 {
testingTypeNameCreation
Expand Down
2 changes: 1 addition & 1 deletion packages/gatsby/src/schema/infer/__tests__/merge-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -551,7 +551,7 @@ describe(`merges explicit and inferred type definitions`, () => {
expect(inferDate.resolve).toBeDefined()
})

it(`adds explicit resolvers through extensions`, async () => {})
it.todo(`adds explicit resolvers through extensions`)

it(`honors array depth when merging types`, async () => {
const typeDefs = `
Expand Down
10 changes: 5 additions & 5 deletions packages/gatsby/src/schema/infer/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const addInferredTypes = ({
: true
if (runInfer) {
if (!typeComposer.hasInterface(`Node`)) {
noNodeInterfaceTypes.push(typeComposer)
noNodeInterfaceTypes.push(typeName)
}
typesToInfer.push(typeComposer)
}
Expand All @@ -46,16 +46,16 @@ const addInferredTypes = ({
})

if (noNodeInterfaceTypes.length > 0) {
noNodeInterfaceTypes.forEach(type => {
noNodeInterfaceTypes.forEach(typeName => {
report.warn(
`Type \`${type}\` declared in \`createTypes\` looks like a node, ` +
`Type \`${typeName}\` declared in \`createTypes\` looks like a node, ` +
`but doesn't implement a \`Node\` interface. It's likely that you should ` +
`add the \`Node\` interface to your type def:\n\n` +
`\`type ${type} implements Node { ... }\`\n\n` +
`\`type ${typeName} implements Node { ... }\`\n\n` +
`If you know that you don't want it to be a node (which would mean no ` +
`root queries to retrieve it), you can explicitly disable inference ` +
`for it:\n\n` +
`\`type ${type} @dontInfer { ... }\``
`\`type ${typeName} @dontInfer { ... }\``
)
})
report.panic(`Building schema failed`)
Expand Down
10 changes: 2 additions & 8 deletions packages/gatsby/src/schema/schema-composer.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,14 @@
const { SchemaComposer, GraphQLJSON } = require(`graphql-compose`)
const { getNodeInterface } = require(`./types/node-interface`)
const { GraphQLDate } = require(`./types/date`)
const {
InferDirective,
DontInferDirective,
AddResolver,
} = require(`./types/directives`)
const { addDirectives } = require(`./extensions`)

const createSchemaComposer = () => {
const schemaComposer = new SchemaComposer()
getNodeInterface({ schemaComposer })
schemaComposer.addAsComposer(GraphQLDate)
schemaComposer.addAsComposer(GraphQLJSON)
schemaComposer.addDirective(InferDirective)
schemaComposer.addDirective(DontInferDirective)
schemaComposer.addDirective(AddResolver)
addDirectives({ schemaComposer })
return schemaComposer
}

Expand Down
Loading

0 comments on commit e40f6d2

Please sign in to comment.