Skip to content

Commit

Permalink
chore: merge fields that are duplicated across subgraphs (#554)
Browse files Browse the repository at this point in the history
  • Loading branch information
Aenimus committed Jul 7, 2023
1 parent 42b8e80 commit 7c894f2
Show file tree
Hide file tree
Showing 15 changed files with 883 additions and 1 deletion.
62 changes: 62 additions & 0 deletions pkg/federation/sdlmerge/merge_duplicated_fields.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package sdlmerge

import (
"github.com/wundergraph/graphql-go-tools/pkg/ast"
"github.com/wundergraph/graphql-go-tools/pkg/astvisitor"
"github.com/wundergraph/graphql-go-tools/pkg/operationreport"
)

type mergeDuplicatedFieldsVisitor struct {
*astvisitor.Walker
document *ast.Document
}

func newMergeDuplicatedFieldsVisitor() *mergeDuplicatedFieldsVisitor {
return &mergeDuplicatedFieldsVisitor{
nil,
nil,
}
}

func (m *mergeDuplicatedFieldsVisitor) Register(walker *astvisitor.Walker) {
m.Walker = walker
walker.RegisterEnterDocumentVisitor(m)
walker.RegisterLeaveObjectTypeDefinitionVisitor(m)
}

func (m *mergeDuplicatedFieldsVisitor) EnterDocument(document, _ *ast.Document) {
m.document = document
}

func (m *mergeDuplicatedFieldsVisitor) LeaveObjectTypeDefinition(ref int) {
var refsForDeletion []int
fieldByTypeRefSet := make(map[string]int)
for _, fieldRef := range m.document.ObjectTypeDefinitions[ref].FieldsDefinition.Refs {
fieldName := m.document.FieldDefinitionNameString(fieldRef)
newTypeRef := m.document.FieldDefinitions[fieldRef].Type
if oldTypeRef, ok := fieldByTypeRefSet[fieldName]; ok {
if m.document.TypesAreEqualDeep(oldTypeRef, newTypeRef) {
refsForDeletion = append(refsForDeletion, fieldRef)
continue
}
oldFieldTypeNameBytes, err := m.document.PrintTypeBytes(oldTypeRef, nil)
if err != nil {
m.Walker.StopWithInternalErr(err)
return
}
newFieldTypeNameBytes, err := m.document.PrintTypeBytes(newTypeRef, nil)
if err != nil {
m.Walker.StopWithInternalErr(err)
return
}
m.Walker.StopWithExternalErr(operationreport.ErrDuplicateFieldsMustBeIdentical(
fieldName, m.document.ObjectTypeDefinitionNameString(ref), string(oldFieldTypeNameBytes), string(newFieldTypeNameBytes),
))
return
}

fieldByTypeRefSet[fieldName] = newTypeRef
}

m.document.RemoveFieldDefinitionsFromObjectTypeDefinition(refsForDeletion, ref)
}
26 changes: 26 additions & 0 deletions pkg/federation/sdlmerge/object_type_extending_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,4 +209,30 @@ func TestExtendObjectType(t *testing.T) {
}
`, nonEntityExtensionErrorMessage("Mammal"))
})

t.Run("", func(t *testing.T) {
run(t, newExtendObjectTypeDefinition(newTestNormalizer(true)), `
type Mammal @key(fields: "name") {
name: String!
age: Int!
}
extend type Mammal @key(fields: "name") {
name: String! @external
age: Int!
}
`, `
type Mammal @key(fields: "name") @key(fields: "name") {
name: String!
age: Int!
name: String! @external
age: Int!
}
extend type Mammal @key(fields: "name") {
name: String! @external
age: Int!
}
`)
})
}
1 change: 1 addition & 0 deletions pkg/federation/sdlmerge/sdlmerge.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ func (m *normalizer) setupWalkers() {
newRemoveFieldDefinitions("external"),
newRemoveDuplicateFieldedSharedTypesVisitor(),
newRemoveDuplicateFieldlessSharedTypesVisitor(),
newMergeDuplicatedFieldsVisitor(),
newRemoveInterfaceDefinitionDirective("key"),
newRemoveObjectTypeDefinitionDirective("key"),
newRemoveFieldDefinitionDirective("provides", "requires"),
Expand Down
100 changes: 100 additions & 0 deletions pkg/federation/sdlmerge/sdlmerge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,100 @@ func TestMergeSDLs(t *testing.T) {
emptyTypeBodyErrorMessage("object", "Message"),
accountSchema, negativeTestingProductSchema,
))

t.Run("Fields should merge successfully", runMergeTest(
`
type Mammal {
name: String!
age: Int!
}
`,
`
type Mammal @key(fields: "name") {
name: String!
age: Int!
}
`, `
extend type Mammal @key(fields: "name") {
name: String! @external
age: Int!
}
`,
))

t.Run("Operation fields should merge successfully", runMergeTest(
`
type Query {
_service: _Service!
}
type _Service {
sdl: String
}
`,
`
type Query {
_service: _Service!
}
type _Service {
sdl: String
}
`, `
type Query {
_service: _Service!
}
type _Service {
sdl: String
}
`,
))

t.Run("Non-identical fields should fail to merge #1", runMergeTestAndExpectError(
unmergableDuplicateFieldsErrorMessage("age", "Mammal", "Int", "String"),
`
type Mammal @key(fields: "name") {
name: String!
age: Int
}
`, `
extend type Mammal @key(fields: "name") {
name: String! @external
age: String
}
`,
))

t.Run("Non-identical fields should fail to merge #2", runMergeTestAndExpectError(
unmergableDuplicateFieldsErrorMessage("age", "Mammal", "Int!", "String!"),
`
type Mammal @key(fields: "name") {
name: String!
age: Int!
}
`, `
extend type Mammal @key(fields: "name") {
name: String! @external
age: String!
}
`,
))

t.Run("Non-dentical fields should fail to merge #3", runMergeTestAndExpectError(
unmergableDuplicateFieldsErrorMessage("ages", "Mammal", "[Int!]!", "[String!]!"),
`
type Mammal @key(fields: "name") {
name: String!
ages: [Int!]!
}
`, `
extend type Mammal @key(fields: "name") {
name: String! @external
ages: [String!]!
}
`,
))
}

const (
Expand Down Expand Up @@ -672,6 +766,12 @@ func duplicateEntityErrorMessage(typeName string) string {
return fmt.Sprintf("the entity named '%s' is defined in the subgraph(s) more than once", typeName)
}

func unmergableDuplicateFieldsErrorMessage(fieldName, parentName, typeOne, typeTwo string) string {
return fmt.Sprintf("field '%s' on type '%s' is defined in multiple subgraphs "+
"but the fields cannot be merged because the types of the fields are non-identical:\n"+
"first subgraph: type '%s'\n second subgraph: type '%s'", fieldName, parentName, typeOne, typeTwo)
}

func Test_validateSubgraphs(t *testing.T) {
basePath := filepath.Join(".", "testdata", "validate-subgraph")

Expand Down
7 changes: 7 additions & 0 deletions pkg/operationreport/externalerror.go
Original file line number Diff line number Diff line change
Expand Up @@ -475,3 +475,10 @@ func ErrExtensionWithKeyDirectiveMustExtendEntity(typeName string) (err External
err.Message = fmt.Sprintf("the extension named '%s' has a key directive but there is no entity of the same name", typeName)
return err
}

func ErrDuplicateFieldsMustBeIdentical(fieldName, parentName, typeOne, typeTwo string) (err ExternalError) {
err.Message = fmt.Sprintf("field '%s' on type '%s' is defined in multiple subgraphs "+
"but the fields cannot be merged because the types of the fields are non-identical:\n"+
"first subgraph: type '%s'\n second subgraph: type '%s'", fieldName, parentName, typeOne, typeTwo)
return err
}
Loading

0 comments on commit 7c894f2

Please sign in to comment.