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 }