github.com/fortexxx/gqlgen@v0.10.3-0.20191216030626-ca5ea8b21ead/plugin/federation/federation.go (about)

     1  package federation
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"os"
     8  	"path/filepath"
     9  	"strings"
    10  
    11  	"github.com/99designs/gqlgen/codegen"
    12  	"github.com/99designs/gqlgen/codegen/config"
    13  	"github.com/99designs/gqlgen/codegen/templates"
    14  	"github.com/99designs/gqlgen/plugin"
    15  	"github.com/vektah/gqlparser"
    16  	"github.com/vektah/gqlparser/ast"
    17  	"github.com/vektah/gqlparser/formatter"
    18  )
    19  
    20  type federation struct {
    21  	SDL      string
    22  	Entities []*Entity
    23  }
    24  
    25  // New returns a federation plugin that injects
    26  // federated directives and types into the schema
    27  func New() plugin.Plugin {
    28  	return &federation{}
    29  }
    30  
    31  func (f *federation) Name() string {
    32  	return "federation"
    33  }
    34  
    35  func (f *federation) MutateConfig(cfg *config.Config) error {
    36  	entityFields := map[string]config.TypeMapField{}
    37  	for _, e := range f.Entities {
    38  		entityFields[e.ResolverName] = config.TypeMapField{Resolver: true}
    39  		for _, r := range e.Requires {
    40  			if cfg.Models[e.Name].Fields == nil {
    41  				model := cfg.Models[e.Name]
    42  				model.Fields = map[string]config.TypeMapField{}
    43  				cfg.Models[e.Name] = model
    44  			}
    45  			cfg.Models[e.Name].Fields[r.Name] = config.TypeMapField{Resolver: true}
    46  		}
    47  	}
    48  	builtins := config.TypeMap{
    49  		"_Service": {
    50  			Model: config.StringList{
    51  				"github.com/99designs/gqlgen/graphql/introspection.Service",
    52  			},
    53  		},
    54  		"_Any": {Model: config.StringList{"github.com/99designs/gqlgen/graphql.Map"}},
    55  		"Entity": {
    56  			Fields: entityFields,
    57  		},
    58  	}
    59  	for typeName, entry := range builtins {
    60  		if cfg.Models.Exists(typeName) {
    61  			return fmt.Errorf("%v already exists which must be reserved when Federation is enabled", typeName)
    62  		}
    63  		cfg.Models[typeName] = entry
    64  	}
    65  	cfg.Directives["external"] = config.DirectiveConfig{SkipRuntime: true}
    66  	cfg.Directives["requires"] = config.DirectiveConfig{SkipRuntime: true}
    67  	cfg.Directives["provides"] = config.DirectiveConfig{SkipRuntime: true}
    68  	cfg.Directives["key"] = config.DirectiveConfig{SkipRuntime: true}
    69  	cfg.Directives["extends"] = config.DirectiveConfig{SkipRuntime: true}
    70  
    71  	return nil
    72  }
    73  
    74  func (f *federation) InjectSources(cfg *config.Config) {
    75  	cfg.AdditionalSources = append(cfg.AdditionalSources, f.getSource(false))
    76  	f.setEntities(cfg)
    77  	var fieldArray []string
    78  	s := "type Entity {\n"
    79  	for _, e := range f.Entities {
    80  		for _, r := range e.Requires {
    81  			for _, rf := range r.Fields {
    82  				fieldArray=append(fieldArray,fmt.Sprintf("%s:String!",rf.Name))
    83  			}
    84  		}
    85  
    86  		if(len(fieldArray) > 0 ){
    87  			s += fmt.Sprintf("\t%s(%s): %s!\n", e.ResolverName, strings.Join(fieldArray,","), e.Def.Name)
    88  		} else {
    89  			s += fmt.Sprintf("\t%s(%s: %s): %s!\n", e.ResolverName, e.FieldName, e.FieldTypeGQL, e.Def.Name)
    90  		}
    91  	}
    92  	s += "}"
    93  	
    94  	cfg.AdditionalSources = append(cfg.AdditionalSources, &ast.Source{Name: "entity.graphql", Input: s, BuiltIn: true})
    95  }
    96  
    97  func (f *federation) MutateSchema(s *ast.Schema) error {
    98  	// --- Set _Entity Union ---
    99  	union := &ast.Definition{
   100  		Name:        "_Entity",
   101  		Kind:        ast.Union,
   102  		Description: "A union unifies all @entity types (TODO: interfaces)",
   103  		Types:       []string{},
   104  	}
   105  	for _, ent := range f.Entities {
   106  		union.Types = append(union.Types, ent.Name)
   107  		s.AddPossibleType("_Entity", ent.Def)
   108  		// s.AddImplements(ent.Name, union) // Do we need this?
   109  	}
   110  	s.Types[union.Name] = union
   111  
   112  	// --- Set _entities query ---
   113  	fieldDef := &ast.FieldDefinition{
   114  		Name: "_entities",
   115  		Type: ast.NonNullListType(ast.NamedType("_Entity", nil), nil),
   116  		Arguments: ast.ArgumentDefinitionList{
   117  			{
   118  				Name: "representations",
   119  				Type: ast.NonNullListType(ast.NonNullNamedType("_Any", nil), nil),
   120  			},
   121  		},
   122  	}
   123  	if s.Query == nil {
   124  		s.Query = &ast.Definition{
   125  			Kind: ast.Object,
   126  			Name: "Query",
   127  		}
   128  		s.Types["Query"] = s.Query
   129  	}
   130  	s.Query.Fields = append(s.Query.Fields, fieldDef)
   131  
   132  	// --- set _Service type ---
   133  	typeDef := &ast.Definition{
   134  		Kind: ast.Object,
   135  		Name: "_Service",
   136  		Fields: ast.FieldList{
   137  			&ast.FieldDefinition{
   138  				Name: "sdl",
   139  				Type: ast.NonNullNamedType("String", nil),
   140  			},
   141  		},
   142  	}
   143  	s.Types[typeDef.Name] = typeDef
   144  
   145  	// --- set _service query ---
   146  	_serviceDef := &ast.FieldDefinition{
   147  		Name: "_service",
   148  		Type: ast.NonNullNamedType("_Service", nil),
   149  	}
   150  	s.Query.Fields = append(s.Query.Fields, _serviceDef)
   151  	return nil
   152  }
   153  
   154  func (f *federation) getSource(builtin bool) *ast.Source {
   155  	return &ast.Source{
   156  		Name: "federation.graphql",
   157  		Input: `# Declarations as required by the federation spec 
   158  # See: https://www.apollographql.com/docs/apollo-server/federation/federation-spec/
   159  
   160  scalar _Any
   161  scalar _FieldSet
   162  
   163  directive @external on FIELD_DEFINITION
   164  directive @requires(fields: _FieldSet!) on FIELD_DEFINITION
   165  directive @provides(fields: _FieldSet!) on FIELD_DEFINITION
   166  directive @key(fields: _FieldSet!) on OBJECT | INTERFACE
   167  directive @extends on OBJECT
   168  `,
   169  		BuiltIn: builtin,
   170  	}
   171  }
   172  
   173  // Entity represents a federated type
   174  // that was declared in the GQL schema.
   175  type Entity struct {
   176  	Name         string // The same name as the type declaration
   177  	FieldName    string // The field name declared in @key
   178  	FieldTypeGo  string // The Go representation of that field type
   179  	FieldTypeGQL string // The GQL represetation of that field type
   180  	ResolverName string // The resolver name, such as FindUserByID
   181  	Def          *ast.Definition
   182  	Requires     []*Requires
   183  	RequiresFieldName string
   184  }
   185  
   186  // Requires represents an @requires clause
   187  type Requires struct {
   188  	Name   string          // the name of the field
   189  	Fields []*RequireField // the name of the sibling fields
   190  }
   191  
   192  // RequireField is similar to an entity but it is a field not
   193  // an object
   194  type RequireField struct {
   195  	Name          string                // The same name as the type declaration
   196  	NameGo        string                // The Go struct field name
   197  	TypeReference *config.TypeReference // The Go representation of that field type
   198  }
   199  
   200  func (f *federation) GenerateCode(data *codegen.Data) error {
   201  	sdl, err := f.getSDL(data.Config)
   202  	if err != nil {
   203  		return err
   204  	}
   205  	f.SDL = sdl
   206  	data.Objects.ByName("Entity").Root = true
   207  	for _, e := range f.Entities {
   208  		obj := data.Objects.ByName(e.Name)
   209  		for _, f := range obj.Fields {
   210  			if f.Name == e.FieldName {
   211  				e.FieldTypeGo = f.TypeReference.GO.String()
   212  			}
   213  			for _, r := range e.Requires {
   214  				for _, rf := range r.Fields {
   215  					if rf.Name == f.Name {
   216  						rf.TypeReference = f.TypeReference
   217  						rf.NameGo = f.GoFieldName
   218  					}
   219  				}
   220  			}
   221  		}
   222  	}
   223  	return templates.Render(templates.Options{
   224  		Template:        tmpl,
   225  		PackageName:     data.Config.Service.Package,
   226  		Filename:        data.Config.Service.Filename,
   227  		Data:            f,
   228  		GeneratedHeader: true,
   229  	})
   230  }
   231  
   232  func (f *federation) setEntities(cfg *config.Config) {
   233  	schema, err := cfg.LoadSchema()
   234  	if err != nil {
   235  		panic(err)
   236  	}
   237  	for _, schemaType := range schema.Types {
   238  		if schemaType.Kind == ast.Object {
   239  			dir := schemaType.Directives.ForName("key") // TODO: interfaces
   240  			if dir != nil {
   241  				fieldName := dir.Arguments[0].Value.Raw // TODO: multiple arguments,a nd multiple keys
   242  				if strings.Contains(fieldName, " ") {
   243  					panic("only single fields are currently supported in @key declaration")
   244  				}
   245  				field := schemaType.Fields.ForName(fieldName)
   246  				requires := []*Requires{}
   247  				requiresFieldName := []string{}
   248  				for _, f := range schemaType.Fields {
   249  					dir := f.Directives.ForName("requires")
   250  					if dir == nil {
   251  						continue
   252  					}
   253  					fields := strings.Split(dir.Arguments[0].Value.Raw, " ")
   254  					requireFields := []*RequireField{}
   255  					for _, f := range fields {
   256  						requiresFieldName = append(requiresFieldName,f)
   257  						requireFields = append(requireFields, &RequireField{
   258  							Name: f,
   259  						})
   260  					}
   261  					requires = append(requires, &Requires{
   262  						Name:   f.Name,
   263  						Fields: requireFields,
   264  					})
   265  				}
   266  				
   267  				f.Entities = append(f.Entities, &Entity{
   268  					Name:         schemaType.Name,
   269  					FieldName:    fieldName,
   270  					FieldTypeGQL: field.Type.String(),
   271  					Def:          schemaType,
   272  					ResolverName: fmt.Sprintf("find%sBy%s", schemaType.Name, templates.ToGo(fieldName)),
   273  					Requires:     requires,
   274  					RequiresFieldName:  strings.Join(requiresFieldName,","),
   275  				})
   276  			}
   277  		}
   278  	}
   279  }
   280  
   281  func (f *federation) getSDL(c *config.Config) (string, error) {
   282  	sources := []*ast.Source{f.getSource(true)}
   283  	for _, filename := range c.SchemaFilename {
   284  		filename = filepath.ToSlash(filename)
   285  		var err error
   286  		var schemaRaw []byte
   287  		schemaRaw, err = ioutil.ReadFile(filename)
   288  		if err != nil {
   289  			fmt.Fprintln(os.Stderr, "unable to open schema: "+err.Error())
   290  			os.Exit(1)
   291  		}
   292  		sources = append(sources, &ast.Source{Name: filename, Input: string(schemaRaw)})
   293  	}
   294  	schema, err := gqlparser.LoadSchema(sources...)
   295  	if err != nil {
   296  		return "", err
   297  	}
   298  	var buf bytes.Buffer
   299  	formatter.NewFormatter(&buf).FormatSchema(schema)
   300  	return buf.String(), nil
   301  }
   302  
   303  var tmpl = `
   304  {{ reserveImport "context"  }}
   305  {{ reserveImport "errors"  }}
   306  
   307  {{ reserveImport "github.com/99designs/gqlgen/graphql/introspection" }}
   308  
   309  func (ec *executionContext) __resolve__service(ctx context.Context) (introspection.Service, error) {
   310  	if ec.DisableIntrospection {
   311  		return introspection.Service{}, errors.New("federated introspection disabled")
   312  	}
   313  	return introspection.Service{
   314  		SDL: ` + "`{{.SDL}}`" + `,
   315  	}, nil
   316  }
   317  
   318  func (ec *executionContext) __resolve_entities(ctx context.Context, representations []map[string]interface{}) ([]_Entity, error) {
   319  	list := []_Entity{}
   320  	for _, rep := range representations {
   321  		typeName, ok := rep["__typename"].(string)
   322  		if !ok {
   323  			return nil, errors.New("__typename must be an existing string")
   324  		}
   325  		switch typeName {
   326  		{{ range .Entities }}
   327  		case "{{.Name}}":
   328  
   329  			id, ok := rep["{{.FieldName}}"].({{.FieldTypeGo}})
   330  			if !ok {
   331  				return nil, errors.New("opsies")
   332  			}
   333  			resp, err := ec.resolvers.Entity().{{.ResolverName | go}}(ctx, id)
   334  
   335  			if err != nil {
   336  				return nil, err
   337  			}
   338  			{{ range .Requires }}
   339  				{{ range .Fields}}
   340  					resp.{{.NameGo}}, err = ec.{{.TypeReference.UnmarshalFunc}}(ctx, rep["{{.Name}}"])
   341  					if err != nil {
   342  						return nil, err
   343  					}
   344  				{{ end }}
   345  			{{ end }}
   346  			list = append(list, resp)
   347  		{{ end }}
   348  		default:
   349  			return nil, errors.New("unknown type: "+typeName)
   350  		}
   351  	}
   352  	return list, nil
   353  }
   354  `