github.com/senomas/gqlgen@v0.17.11-0.20220626120754-9aee61b0716a/plugin/federation/federation.go (about)

     1  package federation
     2  
     3  import (
     4  	"fmt"
     5  	"sort"
     6  	"strings"
     7  
     8  	"github.com/vektah/gqlparser/v2/ast"
     9  
    10  	"github.com/99designs/gqlgen/codegen"
    11  	"github.com/99designs/gqlgen/codegen/config"
    12  	"github.com/99designs/gqlgen/codegen/templates"
    13  	"github.com/99designs/gqlgen/plugin"
    14  	"github.com/99designs/gqlgen/plugin/federation/fieldset"
    15  )
    16  
    17  type federation struct {
    18  	Entities []*Entity
    19  	Version  int
    20  }
    21  
    22  // New returns a federation plugin that injects
    23  // federated directives and types into the schema
    24  func New(version int) plugin.Plugin {
    25  	if version == 0 {
    26  		version = 1
    27  	}
    28  
    29  	return &federation{Version: version}
    30  }
    31  
    32  // Name returns the plugin name
    33  func (f *federation) Name() string {
    34  	return "federation"
    35  }
    36  
    37  // MutateConfig mutates the configuration
    38  func (f *federation) MutateConfig(cfg *config.Config) error {
    39  	builtins := config.TypeMap{
    40  		"_Service": {
    41  			Model: config.StringList{
    42  				"github.com/99designs/gqlgen/plugin/federation/fedruntime.Service",
    43  			},
    44  		},
    45  		"_Entity": {
    46  			Model: config.StringList{
    47  				"github.com/99designs/gqlgen/plugin/federation/fedruntime.Entity",
    48  			},
    49  		},
    50  		"Entity": {
    51  			Model: config.StringList{
    52  				"github.com/99designs/gqlgen/plugin/federation/fedruntime.Entity",
    53  			},
    54  		},
    55  		"_Any": {
    56  			Model: config.StringList{"github.com/99designs/gqlgen/graphql.Map"},
    57  		},
    58  	}
    59  
    60  	for typeName, entry := range builtins {
    61  		if cfg.Models.Exists(typeName) {
    62  			return fmt.Errorf("%v already exists which must be reserved when Federation is enabled", typeName)
    63  		}
    64  		cfg.Models[typeName] = entry
    65  	}
    66  	cfg.Directives["external"] = config.DirectiveConfig{SkipRuntime: true}
    67  	cfg.Directives["requires"] = config.DirectiveConfig{SkipRuntime: true}
    68  	cfg.Directives["provides"] = config.DirectiveConfig{SkipRuntime: true}
    69  	cfg.Directives["key"] = config.DirectiveConfig{SkipRuntime: true}
    70  	cfg.Directives["extends"] = config.DirectiveConfig{SkipRuntime: true}
    71  
    72  	// Federation 2 specific directives
    73  	if f.Version == 2 {
    74  		cfg.Directives["shareable"] = config.DirectiveConfig{SkipRuntime: true}
    75  		cfg.Directives["link"] = config.DirectiveConfig{SkipRuntime: true}
    76  		cfg.Directives["tag"] = config.DirectiveConfig{SkipRuntime: true}
    77  		cfg.Directives["override"] = config.DirectiveConfig{SkipRuntime: true}
    78  		cfg.Directives["inaccessible"] = config.DirectiveConfig{SkipRuntime: true}
    79  	}
    80  
    81  	return nil
    82  }
    83  
    84  func (f *federation) InjectSourceEarly() *ast.Source {
    85  	input := `
    86  	scalar _Any
    87  	scalar _FieldSet
    88  	
    89  	directive @external on FIELD_DEFINITION
    90  	directive @requires(fields: _FieldSet!) on FIELD_DEFINITION
    91  	directive @provides(fields: _FieldSet!) on FIELD_DEFINITION
    92  	directive @extends on OBJECT | INTERFACE
    93  `
    94  	// add version-specific changes on key directive, as well as adding the new directives for federation 2
    95  	if f.Version == 1 {
    96  		input += `
    97  	directive @key(fields: _FieldSet!) repeatable on OBJECT | INTERFACE
    98  `
    99  	} else if f.Version == 2 {
   100  		input += `
   101  	directive @key(fields: _FieldSet!, resolvable: Boolean) repeatable on OBJECT | INTERFACE
   102  	directive @link(import: [String!], url: String!) repeatable on SCHEMA
   103  	directive @shareable on OBJECT | FIELD_DEFINITION
   104  	directive @tag repeatable on OBJECT | FIELD_DEFINITION | INTERFACE | UNION
   105  	directive @override(from: String!) on FIELD_DEFINITION
   106  	directive @inaccessible on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
   107  `
   108  	}
   109  	return &ast.Source{
   110  		Name:    "federation/directives.graphql",
   111  		Input:   input,
   112  		BuiltIn: true,
   113  	}
   114  }
   115  
   116  // InjectSources creates a GraphQL Entity type with all
   117  // the fields that had the @key directive
   118  func (f *federation) InjectSourceLate(schema *ast.Schema) *ast.Source {
   119  	f.setEntities(schema)
   120  
   121  	var entities, resolvers, entityResolverInputDefinitions string
   122  	for i, e := range f.Entities {
   123  		if i != 0 {
   124  			entities += " | "
   125  		}
   126  		entities += e.Name
   127  
   128  		for _, r := range e.Resolvers {
   129  			if e.Multi {
   130  				if entityResolverInputDefinitions != "" {
   131  					entityResolverInputDefinitions += "\n\n"
   132  				}
   133  				entityResolverInputDefinitions += "input " + r.InputType + " {\n"
   134  				for _, keyField := range r.KeyFields {
   135  					entityResolverInputDefinitions += fmt.Sprintf("\t%s: %s\n", keyField.Field.ToGo(), keyField.Definition.Type.String())
   136  				}
   137  				entityResolverInputDefinitions += "}"
   138  				resolvers += fmt.Sprintf("\t%s(reps: [%s!]!): [%s]\n", r.ResolverName, r.InputType, e.Name)
   139  			} else {
   140  				resolverArgs := ""
   141  				for _, keyField := range r.KeyFields {
   142  					resolverArgs += fmt.Sprintf("%s: %s,", keyField.Field.ToGoPrivate(), keyField.Definition.Type.String())
   143  				}
   144  				resolvers += fmt.Sprintf("\t%s(%s): %s!\n", r.ResolverName, resolverArgs, e.Name)
   145  			}
   146  		}
   147  	}
   148  
   149  	var blocks []string
   150  	if entities != "" {
   151  		entities = `# a union of all types that use the @key directive
   152  union _Entity = ` + entities
   153  		blocks = append(blocks, entities)
   154  	}
   155  
   156  	// resolvers can be empty if a service defines only "empty
   157  	// extend" types.  This should be rare.
   158  	if resolvers != "" {
   159  		if entityResolverInputDefinitions != "" {
   160  			blocks = append(blocks, entityResolverInputDefinitions)
   161  		}
   162  		resolvers = `# fake type to build resolver interfaces for users to implement
   163  type Entity {
   164  	` + resolvers + `
   165  }`
   166  		blocks = append(blocks, resolvers)
   167  	}
   168  
   169  	_serviceTypeDef := `type _Service {
   170    sdl: String
   171  }`
   172  	blocks = append(blocks, _serviceTypeDef)
   173  
   174  	var additionalQueryFields string
   175  	// Quote from the Apollo Federation subgraph specification:
   176  	// If no types are annotated with the key directive, then the
   177  	// _Entity union and _entities field should be removed from the schema
   178  	if len(f.Entities) > 0 {
   179  		additionalQueryFields += `  _entities(representations: [_Any!]!): [_Entity]!
   180  `
   181  	}
   182  	// _service field is required in any case
   183  	additionalQueryFields += `  _service: _Service!`
   184  
   185  	extendTypeQueryDef := `extend type ` + schema.Query.Name + ` {
   186  ` + additionalQueryFields + `
   187  }`
   188  	blocks = append(blocks, extendTypeQueryDef)
   189  
   190  	return &ast.Source{
   191  		Name:    "federation/entity.graphql",
   192  		BuiltIn: true,
   193  		Input:   "\n" + strings.Join(blocks, "\n\n") + "\n",
   194  	}
   195  }
   196  
   197  // Entity represents a federated type
   198  // that was declared in the GQL schema.
   199  type Entity struct {
   200  	Name      string // The same name as the type declaration
   201  	Def       *ast.Definition
   202  	Resolvers []*EntityResolver
   203  	Requires  []*Requires
   204  	Multi     bool
   205  }
   206  
   207  type EntityResolver struct {
   208  	ResolverName string      // The resolver name, such as FindUserByID
   209  	KeyFields    []*KeyField // The fields declared in @key.
   210  	InputType    string      // The Go generated input type for multi entity resolvers
   211  }
   212  
   213  type KeyField struct {
   214  	Definition *ast.FieldDefinition
   215  	Field      fieldset.Field        // len > 1 for nested fields
   216  	Type       *config.TypeReference // The Go representation of that field type
   217  }
   218  
   219  // Requires represents an @requires clause
   220  type Requires struct {
   221  	Name  string                // the name of the field
   222  	Field fieldset.Field        // source Field, len > 1 for nested fields
   223  	Type  *config.TypeReference // The Go representation of that field type
   224  }
   225  
   226  func (e *Entity) allFieldsAreExternal() bool {
   227  	for _, field := range e.Def.Fields {
   228  		if field.Directives.ForName("external") == nil {
   229  			return false
   230  		}
   231  	}
   232  	return true
   233  }
   234  
   235  func (f *federation) GenerateCode(data *codegen.Data) error {
   236  	if len(f.Entities) > 0 {
   237  		if data.Objects.ByName("Entity") != nil {
   238  			data.Objects.ByName("Entity").Root = true
   239  		}
   240  		for _, e := range f.Entities {
   241  			obj := data.Objects.ByName(e.Def.Name)
   242  
   243  			for _, r := range e.Resolvers {
   244  				// fill in types for key fields
   245  				//
   246  				for _, keyField := range r.KeyFields {
   247  					if len(keyField.Field) == 0 {
   248  						fmt.Println(
   249  							"skipping @key field " + keyField.Definition.Name + " in " + r.ResolverName + " in " + e.Def.Name,
   250  						)
   251  						continue
   252  					}
   253  					cgField := keyField.Field.TypeReference(obj, data.Objects)
   254  					keyField.Type = cgField.TypeReference
   255  				}
   256  			}
   257  
   258  			// fill in types for requires fields
   259  			//
   260  			for _, reqField := range e.Requires {
   261  				if len(reqField.Field) == 0 {
   262  					fmt.Println("skipping @requires field " + reqField.Name + " in " + e.Def.Name)
   263  					continue
   264  				}
   265  				cgField := reqField.Field.TypeReference(obj, data.Objects)
   266  				reqField.Type = cgField.TypeReference
   267  			}
   268  		}
   269  	}
   270  
   271  	return templates.Render(templates.Options{
   272  		PackageName:     data.Config.Federation.Package,
   273  		Filename:        data.Config.Federation.Filename,
   274  		Data:            f,
   275  		GeneratedHeader: true,
   276  		Packages:        data.Config.Packages,
   277  	})
   278  }
   279  
   280  func (f *federation) setEntities(schema *ast.Schema) {
   281  	for _, schemaType := range schema.Types {
   282  		keys, ok := isFederatedEntity(schemaType)
   283  		if !ok {
   284  			continue
   285  		}
   286  		e := &Entity{
   287  			Name:      schemaType.Name,
   288  			Def:       schemaType,
   289  			Resolvers: nil,
   290  			Requires:  nil,
   291  		}
   292  
   293  		// Let's process custom entity resolver settings.
   294  		dir := schemaType.Directives.ForName("entityResolver")
   295  		if dir != nil {
   296  			if dirArg := dir.Arguments.ForName("multi"); dirArg != nil {
   297  				if dirVal, err := dirArg.Value.Value(nil); err == nil {
   298  					e.Multi = dirVal.(bool)
   299  				}
   300  			}
   301  		}
   302  
   303  		// If our schema has a field with a type defined in
   304  		// another service, then we need to define an "empty
   305  		// extend" of that type in this service, so this service
   306  		// knows what the type is like.  But the graphql-server
   307  		// will never ask us to actually resolve this "empty
   308  		// extend", so we don't require a resolver function for
   309  		// it.  (Well, it will never ask in practice; it's
   310  		// unclear whether the spec guarantees this.  See
   311  		// https://github.com/apollographql/apollo-server/issues/3852
   312  		// ).  Example:
   313  		//    type MyType {
   314  		//       myvar: TypeDefinedInOtherService
   315  		//    }
   316  		//    // Federation needs this type, but
   317  		//    // it doesn't need a resolver for it!
   318  		//    extend TypeDefinedInOtherService @key(fields: "id") {
   319  		//       id: ID @external
   320  		//    }
   321  		if !e.allFieldsAreExternal() {
   322  			for _, dir := range keys {
   323  				if len(dir.Arguments) > 2 {
   324  					panic("More than two arguments provided for @key declaration.")
   325  				}
   326  				var arg *ast.Argument
   327  
   328  				// since keys are able to now have multiple arguments, we need to check both possible for a possible @key(fields="" fields="")
   329  				for _, a := range dir.Arguments {
   330  					if a.Name == "fields" {
   331  						if arg != nil {
   332  							panic("More than one `fields` provided for @key declaration.")
   333  						}
   334  						arg = a
   335  					}
   336  				}
   337  
   338  				keyFieldSet := fieldset.New(arg.Value.Raw, nil)
   339  
   340  				keyFields := make([]*KeyField, len(keyFieldSet))
   341  				resolverFields := []string{}
   342  				for i, field := range keyFieldSet {
   343  					def := field.FieldDefinition(schemaType, schema)
   344  
   345  					if def == nil {
   346  						panic(fmt.Sprintf("no field for %v", field))
   347  					}
   348  
   349  					keyFields[i] = &KeyField{Definition: def, Field: field}
   350  					resolverFields = append(resolverFields, keyFields[i].Field.ToGo())
   351  				}
   352  
   353  				resolverFieldsToGo := schemaType.Name + "By" + strings.Join(resolverFields, "And")
   354  				var resolverName string
   355  				if e.Multi {
   356  					resolverFieldsToGo += "s" // Pluralize for better API readability
   357  					resolverName = fmt.Sprintf("findMany%s", resolverFieldsToGo)
   358  				} else {
   359  					resolverName = fmt.Sprintf("find%s", resolverFieldsToGo)
   360  				}
   361  
   362  				e.Resolvers = append(e.Resolvers, &EntityResolver{
   363  					ResolverName: resolverName,
   364  					KeyFields:    keyFields,
   365  					InputType:    resolverFieldsToGo + "Input",
   366  				})
   367  			}
   368  
   369  			e.Requires = []*Requires{}
   370  			for _, f := range schemaType.Fields {
   371  				dir := f.Directives.ForName("requires")
   372  				if dir == nil {
   373  					continue
   374  				}
   375  				if len(dir.Arguments) != 1 || dir.Arguments[0].Name != "fields" {
   376  					panic("Exactly one `fields` argument needed for @requires declaration.")
   377  				}
   378  				requiresFieldSet := fieldset.New(dir.Arguments[0].Value.Raw, nil)
   379  				for _, field := range requiresFieldSet {
   380  					e.Requires = append(e.Requires, &Requires{
   381  						Name:  field.ToGoPrivate(),
   382  						Field: field,
   383  					})
   384  				}
   385  			}
   386  		}
   387  		f.Entities = append(f.Entities, e)
   388  	}
   389  
   390  	// make sure order remains stable across multiple builds
   391  	sort.Slice(f.Entities, func(i, j int) bool {
   392  		return f.Entities[i].Name < f.Entities[j].Name
   393  	})
   394  }
   395  
   396  func isFederatedEntity(schemaType *ast.Definition) ([]*ast.Directive, bool) {
   397  	switch schemaType.Kind {
   398  	case ast.Object:
   399  		keys := schemaType.Directives.ForNames("key")
   400  		if len(keys) > 0 {
   401  			return keys, true
   402  		}
   403  	case ast.Interface:
   404  		// TODO: support @key and @extends for interfaces
   405  		if dir := schemaType.Directives.ForName("key"); dir != nil {
   406  			fmt.Printf("@key directive found on \"interface %s\". Will be ignored.\n", schemaType.Name)
   407  		}
   408  		if dir := schemaType.Directives.ForName("extends"); dir != nil {
   409  			panic(
   410  				fmt.Sprintf(
   411  					"@extends directive is not currently supported for interfaces, use \"extend interface %s\" instead.",
   412  					schemaType.Name,
   413  				))
   414  		}
   415  	default:
   416  		// ignore
   417  	}
   418  	return nil, false
   419  }