diff --git a/plugin/federation/federation_test.go b/plugin/federation/federation_test.go index 37a7eeaf218..c59a5533cb3 100644 --- a/plugin/federation/federation_test.go +++ b/plugin/federation/federation_test.go @@ -3,6 +3,7 @@ package federation import ( "testing" + "github.com/99designs/gqlgen/codegen" "github.com/99designs/gqlgen/codegen/config" "github.com/stretchr/testify/require" ) @@ -77,6 +78,27 @@ func TestNoEntities(t *testing.T) { require.Len(t, f.Entities, 0) } +func TestInterfaces(t *testing.T) { + require.Panics(t, func() { + load(t, "testdata/interfaces/gqlgen.yml") + }) +} + +func TestCodeGeneration(t *testing.T) { + f, cfg := load(t, "testdata/allthethings/gqlgen.yml") + + require.Len(t, cfg.Schema.Types["_Entity"].Types, 6) + require.Len(t, f.Entities, 6) + + require.NoError(t, f.MutateConfig(cfg)) + + data, err := codegen.BuildData(cfg) + if err != nil { + panic(err) + } + require.NoError(t, f.GenerateCode(data)) +} + func load(t *testing.T, name string) (*federation, *config.Config) { t.Helper() diff --git a/plugin/federation/testdata/allthethings/generated/federation.go b/plugin/federation/testdata/allthethings/generated/federation.go new file mode 100644 index 00000000000..0dab355e281 --- /dev/null +++ b/plugin/federation/testdata/allthethings/generated/federation.go @@ -0,0 +1,256 @@ +// Code generated by github.com/99designs/gqlgen, DO NOT EDIT. + +package generated + +import ( + "context" + "errors" + "fmt" + "strings" + "sync" + + "github.com/99designs/gqlgen/plugin/federation/fedruntime" +) + +func (ec *executionContext) __resolve__service(ctx context.Context) (fedruntime.Service, error) { + if ec.DisableIntrospection { + return fedruntime.Service{}, errors.New("federated introspection disabled") + } + + var sdl []string + + for _, src := range sources { + if src.BuiltIn { + continue + } + sdl = append(sdl, src.Input) + } + + return fedruntime.Service{ + SDL: strings.Join(sdl, "\n"), + }, nil +} + +func (ec *executionContext) __resolve_entities(ctx context.Context, representations []map[string]interface{}) []fedruntime.Entity { + list := make([]fedruntime.Entity, len(representations)) + + repsMap := map[string]struct { + i []int + r []map[string]interface{} + }{} + + // We group entities by typename so that we can parallelize their resolution. + // This is particularly helpful when there are entity groups in multi mode. + buildRepresentationGroups := func(reps []map[string]interface{}) { + for i, rep := range reps { + typeName, ok := rep["__typename"].(string) + if !ok { + // If there is no __typename, we just skip the representation; + // we just won't be resolving these unknown types. + ec.Error(ctx, errors.New("__typename must be an existing string")) + continue + } + + _r := repsMap[typeName] + _r.i = append(_r.i, i) + _r.r = append(_r.r, rep) + repsMap[typeName] = _r + } + } + + isMulti := func(typeName string) bool { + switch typeName { + default: + return false + } + } + + resolveEntity := func(ctx context.Context, typeName string, rep map[string]interface{}, idx []int, i int) (err error) { + // we need to do our own panic handling, because we may be called in a + // goroutine, where the usual panic handling can't catch us + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + } + }() + + switch typeName { + + case "ExternalExtension": + id0, err := ec.unmarshalNString2string(ctx, rep["upc"]) + if err != nil { + return errors.New(fmt.Sprintf("Field %s undefined in schema.", "upc")) + } + + entity, err := ec.resolvers.Entity().FindExternalExtensionByUpc(ctx, + id0) + if err != nil { + return err + } + + list[idx[i]] = entity + return nil + + case "Hello": + id0, err := ec.unmarshalNString2string(ctx, rep["name"]) + if err != nil { + return errors.New(fmt.Sprintf("Field %s undefined in schema.", "name")) + } + + entity, err := ec.resolvers.Entity().FindHelloByName(ctx, + id0) + if err != nil { + return err + } + + list[idx[i]] = entity + return nil + + case "NestedKey": + id0, err := ec.unmarshalNString2string(ctx, rep["id"]) + if err != nil { + return errors.New(fmt.Sprintf("Field %s undefined in schema.", "id")) + } + id1, err := ec.unmarshalNString2string(ctx, rep["hello"].(map[string]interface{})["name"]) + if err != nil { + return errors.New(fmt.Sprintf("Field %s undefined in schema.", "helloName")) + } + + entity, err := ec.resolvers.Entity().FindNestedKeyByIDAndHelloName(ctx, + id0, id1) + if err != nil { + return err + } + + list[idx[i]] = entity + return nil + + case "VeryNestedKey": + id0, err := ec.unmarshalNString2string(ctx, rep["id"]) + if err != nil { + return errors.New(fmt.Sprintf("Field %s undefined in schema.", "id")) + } + id1, err := ec.unmarshalNString2string(ctx, rep["hello"].(map[string]interface{})["name"]) + if err != nil { + return errors.New(fmt.Sprintf("Field %s undefined in schema.", "helloName")) + } + id2, err := ec.unmarshalNString2string(ctx, rep["world"].(map[string]interface{})["foo"]) + if err != nil { + return errors.New(fmt.Sprintf("Field %s undefined in schema.", "worldFoo")) + } + id3, err := ec.unmarshalNInt2int(ctx, rep["world"].(map[string]interface{})["bar"]) + if err != nil { + return errors.New(fmt.Sprintf("Field %s undefined in schema.", "worldBar")) + } + id4, err := ec.unmarshalNString2string(ctx, rep["more"].(map[string]interface{})["world"].(map[string]interface{})["foo"]) + if err != nil { + return errors.New(fmt.Sprintf("Field %s undefined in schema.", "moreWorldFoo")) + } + + entity, err := ec.resolvers.Entity().FindVeryNestedKeyByIDAndHelloNameAndWorldFooAndWorldBarAndMoreWorldFoo(ctx, + id0, id1, id2, id3, id4) + if err != nil { + return err + } + + entity.ID, err = ec.unmarshalNString2string(ctx, rep["id"]) + if err != nil { + return err + } + + entity.Hello.Secondary, err = ec.unmarshalNString2string(ctx, rep["hello"].(map[string]interface{})["secondary"]) + if err != nil { + return err + } + + list[idx[i]] = entity + return nil + + case "World": + id0, err := ec.unmarshalNString2string(ctx, rep["foo"]) + if err != nil { + return errors.New(fmt.Sprintf("Field %s undefined in schema.", "foo")) + } + id1, err := ec.unmarshalNInt2int(ctx, rep["bar"]) + if err != nil { + return errors.New(fmt.Sprintf("Field %s undefined in schema.", "bar")) + } + + entity, err := ec.resolvers.Entity().FindWorldByFooAndBar(ctx, + id0, id1) + if err != nil { + return err + } + + list[idx[i]] = entity + return nil + + default: + return errors.New("unknown type: " + typeName) + } + } + + resolveManyEntities := func(ctx context.Context, typeName string, reps []map[string]interface{}, idx []int) (err error) { + // we need to do our own panic handling, because we may be called in a + // goroutine, where the usual panic handling can't catch us + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + } + }() + + switch typeName { + + default: + return errors.New("unknown type: " + typeName) + } + } + + resolveEntityGroup := func(typeName string, reps []map[string]interface{}, idx []int) { + if isMulti(typeName) { + err := resolveManyEntities(ctx, typeName, reps, idx) + if err != nil { + ec.Error(ctx, err) + } + } else { + // if there are multiple entities to resolve, parallelize (similar to + // graphql.FieldSet.Dispatch) + var e sync.WaitGroup + e.Add(len(reps)) + for i, rep := range reps { + i, rep := i, rep + go func(i int, rep map[string]interface{}) { + err := resolveEntity(ctx, typeName, rep, idx, i) + if err != nil { + ec.Error(ctx, err) + } + e.Done() + }(i, rep) + } + e.Wait() + } + } + + buildRepresentationGroups(representations) + + switch len(repsMap) { + case 0: + return list + case 1: + for typeName, reps := range repsMap { + resolveEntityGroup(typeName, reps.r, reps.i) + } + return list + default: + var g sync.WaitGroup + g.Add(len(repsMap)) + for typeName, reps := range repsMap { + go func(typeName string, reps []map[string]interface{}, idx []int) { + resolveEntityGroup(typeName, reps, idx) + g.Done() + }(typeName, reps.r, reps.i) + } + g.Wait() + return list + } +} diff --git a/plugin/federation/testdata/interfaces/gqlgen.yml b/plugin/federation/testdata/interfaces/gqlgen.yml new file mode 100644 index 00000000000..22b6f8cf12a --- /dev/null +++ b/plugin/federation/testdata/interfaces/gqlgen.yml @@ -0,0 +1,6 @@ +schema: + - "testdata/interfaces/interfaces.graphqls" +exec: + filename: testdata/interfaces/generated/exec.go +federation: + filename: testdata/interfaces/generated/federation.go diff --git a/plugin/federation/testdata/interfaces/interfaces.graphqls b/plugin/federation/testdata/interfaces/interfaces.graphqls new file mode 100644 index 00000000000..87a5c75ce85 --- /dev/null +++ b/plugin/federation/testdata/interfaces/interfaces.graphqls @@ -0,0 +1,9 @@ +interface Hello @key(fields: "name") { + name: String! + secondary: String! +} + +extend interface World { + foo: String! @external + bar: Int! +}