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 `