From 3a2c9cf8e1b708b124fe47a9d12b05db6c6c5bc1 Mon Sep 17 00:00:00 2001 From: ishveda Date: Tue, 2 Feb 2021 14:18:38 +0200 Subject: [PATCH 1/4] add plugins --- definition.go | 1 + executor.go | 26 ++++++++++----- plugins.go | 89 +++++++++++++++++++++++++++++++++++++++++++++++++++ schema.go | 12 +++++++ 4 files changed, 120 insertions(+), 8 deletions(-) create mode 100644 plugins.go diff --git a/definition.go b/definition.go index b5f0048bb..19761e45f 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 7440ae21e..c38cb9b75 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,8 +644,11 @@ func resolveField(eCtx *executionContext, parentType *Object, source interface{} RootValue: eCtx.Root, Operation: eCtx.Operation, VariableValues: eCtx.VariableValues, + ArgumentValues: args, } + handlePluginsResolveFieldFinished(eCtx, info) + var resolveFnError error extErrs, resolveFieldFinishFn := handleExtensionsResolveFieldDidStart(eCtx.Schema.extensions, eCtx, &info) diff --git a/plugins.go b/plugins.go new file mode 100644 index 000000000..8ad37b646 --- /dev/null +++ b/plugins.go @@ -0,0 +1,89 @@ +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{}, args map[string]interface{}) (interface{}, error) +} + +type ElementPath string + +func handlePluginsResolveFieldFinished(eCtx *executionContext, info ResolveInfo) { + for _, p := range info.Schema.plugins { + if p.IsCompatible(eCtx.Context, info) { + eCtx.PluginExecRegistry.Register(PluginExecutable{ + path: info.Path, + args: info.ArgumentValues, + plugin: p, + }) + } + } +} + +type PluginExecutionRegistry struct { + plugins map[*ResponsePath][]PluginExecutable +} + +func NewPluginExecRegistry() *PluginExecutionRegistry { + return &PluginExecutionRegistry{ + plugins: make(map[*ResponsePath][]PluginExecutable, 0), + } +} + +type PluginExecutable struct { + path *ResponsePath + args map[string]interface{} + plugin Plugin +} + +func (pr *PluginExecutionRegistry) Register(pe PluginExecutable) { + plugins := pr.plugins[pe.path] + plugins = append(plugins, pe) + pr.plugins[pe.path] = plugins +} + +func (pr *PluginExecutionRegistry) Execute(ctx context.Context, data interface{}) (interface{}, []gqlerrors.FormattedError) { + var plgErrs []gqlerrors.FormattedError + var err error + for info, plugins := range pr.plugins { + elPath := constructPointer(info.AsArray()) + for _, p := range plugins { + data, err = p.plugin.Execute(ctx, elPath, data, p.args) + 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 35519ac42..69f96dc14 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 From f9e1fa7a396709bdba7de2494812eb4ce9862ba5 Mon Sep 17 00:00:00 2001 From: ishveda Date: Tue, 2 Feb 2021 14:51:55 +0200 Subject: [PATCH 2/4] drop obsolete type --- plugins.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugins.go b/plugins.go index 8ad37b646..c2abcc02f 100644 --- a/plugins.go +++ b/plugins.go @@ -23,8 +23,6 @@ type Plugin interface { Execute(ctx context.Context, pointer string, data interface{}, args map[string]interface{}) (interface{}, error) } -type ElementPath string - func handlePluginsResolveFieldFinished(eCtx *executionContext, info ResolveInfo) { for _, p := range info.Schema.plugins { if p.IsCompatible(eCtx.Context, info) { From 2b8c20bf3b45dde3e5dc202df94302ccbde48a0a Mon Sep 17 00:00:00 2001 From: ishveda Date: Tue, 2 Feb 2021 16:18:36 +0200 Subject: [PATCH 3/4] add resolve info to plugin's execute --- plugins.go | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/plugins.go b/plugins.go index c2abcc02f..1314decd1 100644 --- a/plugins.go +++ b/plugins.go @@ -20,15 +20,14 @@ type Plugin interface { // 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{}, args map[string]interface{}) (interface{}, error) + 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{ - path: info.Path, - args: info.ArgumentValues, + info: info, plugin: p, }) } @@ -46,15 +45,14 @@ func NewPluginExecRegistry() *PluginExecutionRegistry { } type PluginExecutable struct { - path *ResponsePath - args map[string]interface{} + info ResolveInfo plugin Plugin } func (pr *PluginExecutionRegistry) Register(pe PluginExecutable) { - plugins := pr.plugins[pe.path] + plugins := pr.plugins[pe.info.Path] plugins = append(plugins, pe) - pr.plugins[pe.path] = plugins + pr.plugins[pe.info.Path] = plugins } func (pr *PluginExecutionRegistry) Execute(ctx context.Context, data interface{}) (interface{}, []gqlerrors.FormattedError) { @@ -63,7 +61,7 @@ func (pr *PluginExecutionRegistry) Execute(ctx context.Context, data interface{} for info, plugins := range pr.plugins { elPath := constructPointer(info.AsArray()) for _, p := range plugins { - data, err = p.plugin.Execute(ctx, elPath, data, p.args) + 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))) From b48edab853c10bd0dd69fc26fa0b3e2c44446a69 Mon Sep 17 00:00:00 2001 From: ishveda Date: Wed, 3 Feb 2021 18:43:42 +0200 Subject: [PATCH 4/4] keep plugin registration order --- executor.go | 5 +++-- plugins.go | 22 +++++++++------------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/executor.go b/executor.go index c38cb9b75..669e0dc83 100644 --- a/executor.go +++ b/executor.go @@ -647,8 +647,6 @@ func resolveField(eCtx *executionContext, parentType *Object, source interface{} ArgumentValues: args, } - handlePluginsResolveFieldFinished(eCtx, info) - var resolveFnError error extErrs, resolveFieldFinishFn := handleExtensionsResolveFieldDidStart(eCtx.Schema.extensions, eCtx, &info) @@ -673,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 index 1314decd1..1aaa0fff9 100644 --- a/plugins.go +++ b/plugins.go @@ -35,12 +35,12 @@ func handlePluginsResolveFieldFinished(eCtx *executionContext, info ResolveInfo) } type PluginExecutionRegistry struct { - plugins map[*ResponsePath][]PluginExecutable + plugins []PluginExecutable } func NewPluginExecRegistry() *PluginExecutionRegistry { return &PluginExecutionRegistry{ - plugins: make(map[*ResponsePath][]PluginExecutable, 0), + plugins: make([]PluginExecutable, 0), } } @@ -50,22 +50,18 @@ type PluginExecutable struct { } func (pr *PluginExecutionRegistry) Register(pe PluginExecutable) { - plugins := pr.plugins[pe.info.Path] - plugins = append(plugins, pe) - pr.plugins[pe.info.Path] = plugins + 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 info, plugins := range pr.plugins { - elPath := constructPointer(info.AsArray()) - for _, p := range plugins { - 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))) - } + 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))) } }