diff --git a/definition.go b/definition.go index b5f0048b..19761e45 100644 --- a/definition.go +++ b/definition.go @@ -597,6 +597,7 @@ type ResolveInfo struct { RootValue interface{} Operation ast.Definition VariableValues map[string]interface{} + ArgumentValues map[string]interface{} } type Fields map[string]*Field diff --git a/executor.go b/executor.go index 7440ae21..669e0dc8 100644 --- a/executor.go +++ b/executor.go @@ -103,13 +103,14 @@ type buildExecutionCtxParams struct { } type executionContext struct { - Schema Schema - Fragments map[string]ast.Definition - Root interface{} - Operation ast.Definition - VariableValues map[string]interface{} - Errors []gqlerrors.FormattedError - Context context.Context + Schema Schema + Fragments map[string]ast.Definition + Root interface{} + Operation ast.Definition + VariableValues map[string]interface{} + Errors []gqlerrors.FormattedError + Context context.Context + PluginExecRegistry *PluginExecutionRegistry } func buildExecutionContext(p buildExecutionCtxParams) (*executionContext, error) { @@ -155,6 +156,7 @@ func buildExecutionContext(p buildExecutionCtxParams) (*executionContext, error) eCtx.Operation = operation eCtx.VariableValues = variableValues eCtx.Context = p.Context + eCtx.PluginExecRegistry = NewPluginExecRegistry() return eCtx, nil } @@ -279,8 +281,13 @@ func executeFields(p executeFieldsParams) *Result { dethunkMapWithBreadthFirstTraversal(finalResults) + finalModified, errs := p.ExecutionContext.PluginExecRegistry.Execute(p.ExecutionContext.Context, finalResults) + if len(errs) > 0 { + p.ExecutionContext.Errors = append(p.ExecutionContext.Errors, errs...) + } + return &Result{ - Data: finalResults, + Data: finalModified, Errors: p.ExecutionContext.Errors, } } @@ -637,6 +644,7 @@ func resolveField(eCtx *executionContext, parentType *Object, source interface{} RootValue: eCtx.Root, Operation: eCtx.Operation, VariableValues: eCtx.VariableValues, + ArgumentValues: args, } var resolveFnError error @@ -663,6 +671,9 @@ func resolveField(eCtx *executionContext, parentType *Object, source interface{} } completed := completeValueCatchingError(eCtx, returnType, fieldASTs, info, path, result) + + handlePluginsResolveFieldFinished(eCtx, info) + return completed, resultState } diff --git a/plugins.go b/plugins.go new file mode 100644 index 00000000..1aaa0fff --- /dev/null +++ b/plugins.go @@ -0,0 +1,81 @@ +package graphql + +import ( + "context" + "fmt" + "strings" + + "github.com/graphql-go/graphql/gqlerrors" +) + +// Plugin is an interface for custom post-processing based on the execution context +// all the pre-processing happens in Resolve*() +// plugins differs from extensions by the time of execution and by how they modify the result +// in resolve func plugin analyzes execution context for each field in order to collect information of +// what to process during execution +// execution happens once query resolve is fully finished +type Plugin interface { + // Name returns name of the plugin + Name() string + // IsCompatible tests whether current field is compatible with the plugin + IsCompatible(ctx context.Context, i ResolveInfo) bool + // Execute runs plugin processing on the data accessed by provided json pointer + Execute(ctx context.Context, pointer string, data interface{}, i ResolveInfo) (interface{}, error) +} + +func handlePluginsResolveFieldFinished(eCtx *executionContext, info ResolveInfo) { + for _, p := range info.Schema.plugins { + if p.IsCompatible(eCtx.Context, info) { + eCtx.PluginExecRegistry.Register(PluginExecutable{ + info: info, + plugin: p, + }) + } + } +} + +type PluginExecutionRegistry struct { + plugins []PluginExecutable +} + +func NewPluginExecRegistry() *PluginExecutionRegistry { + return &PluginExecutionRegistry{ + plugins: make([]PluginExecutable, 0), + } +} + +type PluginExecutable struct { + info ResolveInfo + plugin Plugin +} + +func (pr *PluginExecutionRegistry) Register(pe PluginExecutable) { + pr.plugins = append(pr.plugins, pe) +} + +func (pr *PluginExecutionRegistry) Execute(ctx context.Context, data interface{}) (interface{}, []gqlerrors.FormattedError) { + var plgErrs []gqlerrors.FormattedError + var err error + for _, p := range pr.plugins { + elPath := constructPointer(p.info.Path.AsArray()) + data, err = p.plugin.Execute(ctx, elPath, data, p.info) + if err != nil { + plgErrs = append(plgErrs, gqlerrors.FormatError( + fmt.Errorf("%s.PluginExecution: %v", p.plugin.Name(), err))) + } + } + + return data, plgErrs +} + +func constructPointer(path []interface{}) string { + var buf = strings.Builder{} + buf.WriteString("/") + for i := 0; i < len(path); i++ { + buf.WriteString(fmt.Sprintf("%v", path[i])) + if i != len(path)-1 { + buf.WriteString("/") + } + } + return buf.String() +} diff --git a/schema.go b/schema.go index 35519ac4..69f96dc1 100644 --- a/schema.go +++ b/schema.go @@ -7,6 +7,7 @@ type SchemaConfig struct { Types []Type Directives []*Directive Extensions []Extension + Plugins []Plugin } type TypeMap map[string]Type @@ -41,6 +42,7 @@ type Schema struct { implementations map[string][]*Object possibleTypeMap map[string]map[string]bool extensions []Extension + plugins []Plugin } func NewSchema(config SchemaConfig) (Schema, error) { @@ -142,6 +144,11 @@ func NewSchema(config SchemaConfig) (Schema, error) { schema.extensions = config.Extensions } + // Add plugins from config + if len(config.Plugins) != 0 { + schema.plugins = config.Plugins + } + return schema, nil } @@ -267,6 +274,11 @@ func (gq *Schema) AddExtensions(e ...Extension) { gq.extensions = append(gq.extensions, e...) } +// AddPlugins can be used to add additional plugins to the schema +func (gq *Schema) AddPlugins(p ...Plugin) { + gq.plugins = append(gq.plugins, p...) +} + // map-reduce func typeMapReducer(schema *Schema, typeMap TypeMap, objectType Type) (TypeMap, error) { var err error