diff --git a/go.mod b/go.mod index 8fd381e..890c225 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/sparetimecoders/goamqp v0.3.1 github.com/stretchr/testify v1.10.0 github.com/vektah/gqlparser/v2 v2.5.23 - github.com/wundergraph/graphql-go-tools v1.67.4 + github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.160 gitlab.com/unboundsoftware/eventsourced/amqp v1.8.0 gitlab.com/unboundsoftware/eventsourced/eventsourced v1.17.0 gitlab.com/unboundsoftware/eventsourced/pg v1.16.0 @@ -33,13 +33,11 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.1 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect - github.com/jensneuse/byte-template v0.0.0-20200214152254-4f3cf06e5c68 // indirect github.com/lib/pq v1.10.9 // indirect github.com/mfridman/interpolate v0.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rabbitmq/amqp091-go v1.10.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect github.com/sethvargo/go-retry v0.3.0 // indirect github.com/sosodev/duration v1.3.1 // indirect github.com/tidwall/gjson v1.17.0 // indirect @@ -47,6 +45,7 @@ require ( github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect github.com/urfave/cli/v2 v2.27.5 // indirect + github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/mod v0.23.0 // indirect diff --git a/go.sum b/go.sum index 0377c85..7cfcc06 100644 --- a/go.sum +++ b/go.sum @@ -44,8 +44,6 @@ github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7c github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/evanphx/json-patch/v5 v5.1.0 h1:B0aXl1o/1cP8NbviYiBMkcHBtUjIJ1/Ccg6b+SwCLQg= -github.com/evanphx/json-patch/v5 v5.1.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/getsentry/sentry-go v0.31.1 h1:ELVc0h7gwyhnXHDouXkhqTFSO5oslsRDk0++eyE0KJ4= @@ -59,8 +57,6 @@ github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIx github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= @@ -75,8 +71,6 @@ github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyf github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/jensneuse/byte-template v0.0.0-20200214152254-4f3cf06e5c68 h1:E80wOd3IFQcoBxLkAUpUQ3BoGrZ4DxhQdP21+HH1s6A= -github.com/jensneuse/byte-template v0.0.0-20200214152254-4f3cf06e5c68/go.mod h1:0D5r/VSW6D/o65rKLL9xk7sZxL2+oku2HvFPYeIMFr4= github.com/jensneuse/diffview v1.0.0 h1:4b6FQJ7y3295JUHU3tRko6euyEboL825ZsXeZZM47Z4= github.com/jensneuse/diffview v1.0.0/go.mod h1:i6IacuD8LnEaPuiyzMHA+Wfz5mAuycMOf3R/orUY9y4= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= @@ -129,8 +123,6 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sanity-io/litter v1.5.8 h1:uM/2lKrWdGbRXDrIq08Lh9XtVYoeGtcQxk9rtQ7+rYg= github.com/sanity-io/litter v1.5.8/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= -github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= -github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= github.com/sebdah/goldie/v2 v2.5.3 h1:9ES/mNN+HNUbNWpVAlrzuZ7jE+Nrczbj8uFRjM7624Y= github.com/sebdah/goldie/v2 v2.5.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= @@ -171,8 +163,10 @@ github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= github.com/vektah/gqlparser/v2 v2.5.23 h1:PurJ9wpgEVB7tty1seRUwkIDa/QH5RzkzraiKIjKLfA= github.com/vektah/gqlparser/v2 v2.5.23/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= -github.com/wundergraph/graphql-go-tools v1.67.4 h1:1QtoftaZz5sScV/J6XLZ/oTfi1lMHp6UmFkYRQfY2/g= -github.com/wundergraph/graphql-go-tools v1.67.4/go.mod h1:UFvflYjB/qnSCdgcHQuE6dTfwZ6viJB7yPnGOtBuibo= +github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083 h1:8/D7f8gKxTBjW+SZK4mhxTTBVpxcqeBgWF1Rfmltbfk= +github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083/go.mod h1:eOTL6acwctsN4F3b7YE+eE2t8zcJ/doLm9sZzsxxxrE= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.160 h1:whZcpumOJNwJg71M56VGZ/Ex4TnYeGjmzhWa6+1X+uI= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.160/go.mod h1:B7eV0Qh8Lop9QzIOQcsvKp3S0ejfC6mgyWoJnI917yQ= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= gitlab.com/unboundsoftware/eventsourced/amqp v1.8.0 h1:Ez3UuXvveyV/HlphqWUWZU/05DuhqKfgF9r6tCJigEI= diff --git a/graph/schema.resolvers.go b/graph/schema.resolvers.go index c1f4c3f..4daac31 100644 --- a/graph/schema.resolvers.go +++ b/graph/schema.resolvers.go @@ -9,7 +9,6 @@ import ( "fmt" "strings" - "github.com/wundergraph/graphql-go-tools/pkg/federation/sdlmerge" "gitlab.com/unboundsoftware/eventsourced/eventsourced" "gitlab.com/unboundsoftware/schemas/domain" @@ -17,6 +16,7 @@ import ( "gitlab.com/unboundsoftware/schemas/graph/model" "gitlab.com/unboundsoftware/schemas/middleware" "gitlab.com/unboundsoftware/schemas/rand" + "gitlab.com/unboundsoftware/schemas/sdlmerge" ) // AddOrganization is the resolver for the addOrganization field. diff --git a/sdlmerge/collect_entities.go b/sdlmerge/collect_entities.go new file mode 100644 index 0000000..218e0b0 --- /dev/null +++ b/sdlmerge/collect_entities.go @@ -0,0 +1,61 @@ +package sdlmerge + +import ( + "github.com/wundergraph/graphql-go-tools/v2/pkg/ast" + "github.com/wundergraph/graphql-go-tools/v2/pkg/astvisitor" + "github.com/wundergraph/graphql-go-tools/v2/pkg/operationreport" +) + +type collectEntitiesVisitor struct { + *astvisitor.Walker + document *ast.Document + collectedEntities entitySet +} + +func newCollectEntitiesVisitor(collectedEntities entitySet) *collectEntitiesVisitor { + return &collectEntitiesVisitor{ + collectedEntities: collectedEntities, + } +} + +func (c *collectEntitiesVisitor) Register(walker *astvisitor.Walker) { + c.Walker = walker + walker.RegisterEnterDocumentVisitor(c) + walker.RegisterEnterInterfaceTypeDefinitionVisitor(c) + walker.RegisterEnterObjectTypeDefinitionVisitor(c) +} + +func (c *collectEntitiesVisitor) EnterDocument(operation, _ *ast.Document) { + c.document = operation +} + +func (c *collectEntitiesVisitor) EnterInterfaceTypeDefinition(ref int) { + interfaceType := c.document.InterfaceTypeDefinitions[ref] + name := c.document.InterfaceTypeDefinitionNameString(ref) + if err := c.resolvePotentialEntity(name, interfaceType.Directives.Refs); err != nil { + c.StopWithExternalErr(*err) + } +} + +func (c *collectEntitiesVisitor) EnterObjectTypeDefinition(ref int) { + objectType := c.document.ObjectTypeDefinitions[ref] + name := c.document.ObjectTypeDefinitionNameString(ref) + if err := c.resolvePotentialEntity(name, objectType.Directives.Refs); err != nil { + c.StopWithExternalErr(*err) + } +} + +func (c *collectEntitiesVisitor) resolvePotentialEntity(name string, directiveRefs []int) *operationreport.ExternalError { + if _, exists := c.collectedEntities[name]; exists { + err := operationreport.ErrEntitiesMustNotBeDuplicated(name) + return &err + } + for _, directiveRef := range directiveRefs { + if c.document.DirectiveNameString(directiveRef) != "key" { + continue + } + c.collectedEntities[name] = struct{}{} + return nil + } + return nil +} diff --git a/sdlmerge/enum_type_extending.go b/sdlmerge/enum_type_extending.go new file mode 100644 index 0000000..5372225 --- /dev/null +++ b/sdlmerge/enum_type_extending.go @@ -0,0 +1,50 @@ +package sdlmerge + +import ( + "github.com/wundergraph/graphql-go-tools/v2/pkg/ast" + "github.com/wundergraph/graphql-go-tools/v2/pkg/astvisitor" + "github.com/wundergraph/graphql-go-tools/v2/pkg/operationreport" +) + +type extendEnumTypeDefinitionVisitor struct { + *astvisitor.Walker + document *ast.Document +} + +func newExtendEnumTypeDefinition() *extendEnumTypeDefinitionVisitor { + return &extendEnumTypeDefinitionVisitor{} +} + +func (e *extendEnumTypeDefinitionVisitor) Register(walker *astvisitor.Walker) { + e.Walker = walker + walker.RegisterEnterDocumentVisitor(e) + walker.RegisterEnterEnumTypeExtensionVisitor(e) +} + +func (e *extendEnumTypeDefinitionVisitor) EnterDocument(operation, _ *ast.Document) { + e.document = operation +} + +func (e *extendEnumTypeDefinitionVisitor) EnterEnumTypeExtension(ref int) { + nodes, exists := e.document.Index.NodesByNameBytes(e.document.EnumTypeExtensionNameBytes(ref)) + if !exists { + return + } + + hasExtended := false + for i := range nodes { + if nodes[i].Kind != ast.NodeKindEnumTypeDefinition { + continue + } + if hasExtended { + e.StopWithExternalErr(operationreport.ErrSharedTypesMustNotBeExtended(e.document.EnumTypeExtensionNameString(ref))) + return + } + e.document.ExtendEnumTypeDefinitionByEnumTypeExtension(nodes[i].Ref, ref) + hasExtended = true + } + + if !hasExtended { + e.StopWithExternalErr(operationreport.ErrExtensionOrphansMustResolveInSupergraph(e.document.EnumTypeExtensionNameBytes(ref))) + } +} diff --git a/sdlmerge/input_type_extending.go b/sdlmerge/input_type_extending.go new file mode 100644 index 0000000..4a3f554 --- /dev/null +++ b/sdlmerge/input_type_extending.go @@ -0,0 +1,50 @@ +package sdlmerge + +import ( + "github.com/wundergraph/graphql-go-tools/v2/pkg/ast" + "github.com/wundergraph/graphql-go-tools/v2/pkg/astvisitor" + "github.com/wundergraph/graphql-go-tools/v2/pkg/operationreport" +) + +func newExtendInputObjectTypeDefinition() *extendInputObjectTypeDefinitionVisitor { + return &extendInputObjectTypeDefinitionVisitor{} +} + +type extendInputObjectTypeDefinitionVisitor struct { + *astvisitor.Walker + document *ast.Document +} + +func (e *extendInputObjectTypeDefinitionVisitor) Register(walker *astvisitor.Walker) { + e.Walker = walker + walker.RegisterEnterDocumentVisitor(e) + walker.RegisterEnterInputObjectTypeExtensionVisitor(e) +} + +func (e *extendInputObjectTypeDefinitionVisitor) EnterDocument(operation, _ *ast.Document) { + e.document = operation +} + +func (e *extendInputObjectTypeDefinitionVisitor) EnterInputObjectTypeExtension(ref int) { + nodes, exists := e.document.Index.NodesByNameBytes(e.document.InputObjectTypeExtensionNameBytes(ref)) + if !exists { + return + } + + hasExtended := false + for i := range nodes { + if nodes[i].Kind != ast.NodeKindInputObjectTypeDefinition { + continue + } + if hasExtended { + e.StopWithExternalErr(operationreport.ErrSharedTypesMustNotBeExtended(e.document.InputObjectTypeExtensionNameString(ref))) + return + } + e.document.ExtendInputObjectTypeDefinitionByInputObjectTypeExtension(nodes[i].Ref, ref) + hasExtended = true + } + + if !hasExtended { + e.StopWithExternalErr(operationreport.ErrExtensionOrphansMustResolveInSupergraph(e.document.InputObjectTypeExtensionNameBytes(ref))) + } +} diff --git a/sdlmerge/interface_type_extending.go b/sdlmerge/interface_type_extending.go new file mode 100644 index 0000000..ff55c7e --- /dev/null +++ b/sdlmerge/interface_type_extending.go @@ -0,0 +1,63 @@ +package sdlmerge + +import ( + "github.com/wundergraph/graphql-go-tools/v2/pkg/ast" + "github.com/wundergraph/graphql-go-tools/v2/pkg/astvisitor" + "github.com/wundergraph/graphql-go-tools/v2/pkg/operationreport" +) + +func newExtendInterfaceTypeDefinition(collectedEntities entitySet) *extendInterfaceTypeDefinitionVisitor { + return &extendInterfaceTypeDefinitionVisitor{ + collectedEntities: collectedEntities, + } +} + +type extendInterfaceTypeDefinitionVisitor struct { + *astvisitor.Walker + document *ast.Document + collectedEntities entitySet +} + +func (e *extendInterfaceTypeDefinitionVisitor) Register(walker *astvisitor.Walker) { + e.Walker = walker + walker.RegisterEnterDocumentVisitor(e) + walker.RegisterEnterInterfaceTypeExtensionVisitor(e) +} + +func (e *extendInterfaceTypeDefinitionVisitor) EnterDocument(operation, _ *ast.Document) { + e.document = operation +} + +func (e *extendInterfaceTypeDefinitionVisitor) EnterInterfaceTypeExtension(ref int) { + nameBytes := e.document.InterfaceTypeExtensionNameBytes(ref) + nodes, exists := e.document.Index.NodesByNameBytes(nameBytes) + if !exists { + return + } + + var nodeToExtend *ast.Node + isEntity := false + for i := range nodes { + if nodes[i].Kind != ast.NodeKindInterfaceTypeDefinition { + continue + } + if nodeToExtend != nil { + e.StopWithExternalErr(*multipleExtensionError(isEntity, nameBytes)) + return + } + var err *operationreport.ExternalError + extension := e.document.InterfaceTypeExtensions[ref] + if isEntity, err = e.collectedEntities.isExtensionForEntity(nameBytes, extension.Directives.Refs, e.document); err != nil { + e.StopWithExternalErr(*err) + return + } + nodeToExtend = &nodes[i] + } + + if nodeToExtend == nil { + e.StopWithExternalErr(operationreport.ErrExtensionOrphansMustResolveInSupergraph(e.document.InterfaceTypeExtensionNameBytes(ref))) + return + } + + e.document.ExtendInterfaceTypeDefinitionByInterfaceTypeExtension(nodeToExtend.Ref, ref) +} diff --git a/sdlmerge/merge_duplicated_fields.go b/sdlmerge/merge_duplicated_fields.go new file mode 100644 index 0000000..fcfcf98 --- /dev/null +++ b/sdlmerge/merge_duplicated_fields.go @@ -0,0 +1,62 @@ +package sdlmerge + +import ( + "github.com/wundergraph/graphql-go-tools/v2/pkg/ast" + "github.com/wundergraph/graphql-go-tools/v2/pkg/astvisitor" + "github.com/wundergraph/graphql-go-tools/v2/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) +} diff --git a/sdlmerge/object_type_extending.go b/sdlmerge/object_type_extending.go new file mode 100644 index 0000000..7ac6b58 --- /dev/null +++ b/sdlmerge/object_type_extending.go @@ -0,0 +1,66 @@ +package sdlmerge + +import ( + "github.com/wundergraph/graphql-go-tools/v2/pkg/ast" + "github.com/wundergraph/graphql-go-tools/v2/pkg/astvisitor" + "github.com/wundergraph/graphql-go-tools/v2/pkg/operationreport" +) + +func newExtendObjectTypeDefinition(collectedEntities entitySet) *extendObjectTypeDefinitionVisitor { + return &extendObjectTypeDefinitionVisitor{ + collectedEntities: collectedEntities, + } +} + +type extendObjectTypeDefinitionVisitor struct { + *astvisitor.Walker + document *ast.Document + collectedEntities entitySet +} + +func (e *extendObjectTypeDefinitionVisitor) Register(walker *astvisitor.Walker) { + e.Walker = walker + walker.RegisterEnterDocumentVisitor(e) + walker.RegisterEnterObjectTypeExtensionVisitor(e) +} + +func (e *extendObjectTypeDefinitionVisitor) EnterDocument(operation, _ *ast.Document) { + e.document = operation +} + +func (e *extendObjectTypeDefinitionVisitor) EnterObjectTypeExtension(ref int) { + nameBytes := e.document.ObjectTypeExtensionNameBytes(ref) + nodes, exists := e.document.Index.NodesByNameBytes(nameBytes) + if !exists { + return + } + + var nodeToExtend *ast.Node + isEntity := false + for i := range nodes { + if nodes[i].Kind != ast.NodeKindObjectTypeDefinition { + continue + } + if nodeToExtend != nil { + e.StopWithExternalErr(*multipleExtensionError(isEntity, nameBytes)) + return + } + var err *operationreport.ExternalError + extension := e.document.ObjectTypeExtensions[ref] + if isEntity, err = e.collectedEntities.isExtensionForEntity(nameBytes, extension.Directives.Refs, e.document); err != nil { + e.StopWithExternalErr(*err) + return + } + nodeToExtend = &nodes[i] + if ast.IsRootType(nameBytes) { + break + } + } + + if nodeToExtend == nil { + e.StopWithExternalErr(operationreport.ErrExtensionOrphansMustResolveInSupergraph(nameBytes)) + return + } + + e.document.ExtendObjectTypeDefinitionByObjectTypeExtension(nodeToExtend.Ref, ref) +} diff --git a/sdlmerge/remove_duplicate_fielded_shared_types.go b/sdlmerge/remove_duplicate_fielded_shared_types.go new file mode 100644 index 0000000..a937cc4 --- /dev/null +++ b/sdlmerge/remove_duplicate_fielded_shared_types.go @@ -0,0 +1,107 @@ +package sdlmerge + +import ( + "github.com/wundergraph/graphql-go-tools/v2/pkg/ast" + "github.com/wundergraph/graphql-go-tools/v2/pkg/astvisitor" + "github.com/wundergraph/graphql-go-tools/v2/pkg/operationreport" +) + +type removeDuplicateFieldedSharedTypesVisitor struct { + *astvisitor.Walker + document *ast.Document + sharedTypeSet map[string]fieldedSharedType + rootNodesToRemove []ast.Node + lastInputRef int + lastInterfaceRef int + lastObjectRef int +} + +func newRemoveDuplicateFieldedSharedTypesVisitor() *removeDuplicateFieldedSharedTypesVisitor { + return &removeDuplicateFieldedSharedTypesVisitor{ + nil, + nil, + make(map[string]fieldedSharedType), + nil, + ast.InvalidRef, + ast.InvalidRef, + ast.InvalidRef, + } +} + +func (r *removeDuplicateFieldedSharedTypesVisitor) Register(walker *astvisitor.Walker) { + r.Walker = walker + walker.RegisterEnterDocumentVisitor(r) + walker.RegisterEnterInputObjectTypeDefinitionVisitor(r) + walker.RegisterEnterInterfaceTypeDefinitionVisitor(r) + walker.RegisterEnterObjectTypeDefinitionVisitor(r) + walker.RegisterLeaveDocumentVisitor(r) +} + +func (r *removeDuplicateFieldedSharedTypesVisitor) EnterDocument(operation, _ *ast.Document) { + r.document = operation +} + +func (r *removeDuplicateFieldedSharedTypesVisitor) EnterInputObjectTypeDefinition(ref int) { + if ref <= r.lastInputRef { + return + } + name := r.document.InputObjectTypeDefinitionNameString(ref) + refs := r.document.InputObjectTypeDefinitions[ref].InputFieldsDefinition.Refs + input, exists := r.sharedTypeSet[name] + if exists { + if !input.areFieldsIdentical(refs) { + r.StopWithExternalErr(operationreport.ErrSharedTypesMustBeIdenticalToFederate(name)) + return + } + r.rootNodesToRemove = append(r.rootNodesToRemove, ast.Node{Kind: ast.NodeKindInputObjectTypeDefinition, Ref: ref}) + } else { + r.sharedTypeSet[name] = newFieldedSharedType(r.document, ast.NodeKindInputValueDefinition, refs) + } + r.lastInputRef = ref +} + +func (r *removeDuplicateFieldedSharedTypesVisitor) EnterInterfaceTypeDefinition(ref int) { + if ref <= r.lastInterfaceRef { + return + } + name := r.document.InterfaceTypeDefinitionNameString(ref) + interfaceType := r.document.InterfaceTypeDefinitions[ref] + refs := interfaceType.FieldsDefinition.Refs + iFace, exists := r.sharedTypeSet[name] + if exists { + if !iFace.areFieldsIdentical(refs) { + r.StopWithExternalErr(operationreport.ErrSharedTypesMustBeIdenticalToFederate(name)) + return + } + r.rootNodesToRemove = append(r.rootNodesToRemove, ast.Node{Kind: ast.NodeKindInterfaceTypeDefinition, Ref: ref}) + } else { + r.sharedTypeSet[name] = newFieldedSharedType(r.document, ast.NodeKindFieldDefinition, refs) + } + r.lastInterfaceRef = ref +} + +func (r *removeDuplicateFieldedSharedTypesVisitor) EnterObjectTypeDefinition(ref int) { + if ref <= r.lastObjectRef { + return + } + name := r.document.ObjectTypeDefinitionNameString(ref) + objectType := r.document.ObjectTypeDefinitions[ref] + refs := objectType.FieldsDefinition.Refs + object, exists := r.sharedTypeSet[name] + if exists { + if !object.areFieldsIdentical(refs) { + r.StopWithExternalErr(operationreport.ErrSharedTypesMustBeIdenticalToFederate(name)) + return + } + r.rootNodesToRemove = append(r.rootNodesToRemove, ast.Node{Kind: ast.NodeKindObjectTypeDefinition, Ref: ref}) + } else { + r.sharedTypeSet[name] = newFieldedSharedType(r.document, ast.NodeKindFieldDefinition, refs) + } + r.lastObjectRef = ref +} + +func (r *removeDuplicateFieldedSharedTypesVisitor) LeaveDocument(_, _ *ast.Document) { + if r.rootNodesToRemove != nil { + r.document.DeleteRootNodes(r.rootNodesToRemove) + } +} diff --git a/sdlmerge/remove_duplicate_fieldless_shared_types.go b/sdlmerge/remove_duplicate_fieldless_shared_types.go new file mode 100644 index 0000000..3dac1fc --- /dev/null +++ b/sdlmerge/remove_duplicate_fieldless_shared_types.go @@ -0,0 +1,98 @@ +package sdlmerge + +import ( + "github.com/wundergraph/graphql-go-tools/v2/pkg/ast" + "github.com/wundergraph/graphql-go-tools/v2/pkg/astvisitor" + "github.com/wundergraph/graphql-go-tools/v2/pkg/operationreport" +) + +type removeDuplicateFieldlessSharedTypesVisitor struct { + *astvisitor.Walker + document *ast.Document + sharedTypeSet map[string]fieldlessSharedType + rootNodesToRemove []ast.Node + lastEnumRef int + lastUnionRef int + lastScalarRef int +} + +func newRemoveDuplicateFieldlessSharedTypesVisitor() *removeDuplicateFieldlessSharedTypesVisitor { + return &removeDuplicateFieldlessSharedTypesVisitor{ + nil, + nil, + make(map[string]fieldlessSharedType), + nil, + ast.InvalidRef, + ast.InvalidRef, + ast.InvalidRef, + } +} + +func (r *removeDuplicateFieldlessSharedTypesVisitor) Register(walker *astvisitor.Walker) { + r.Walker = walker + walker.RegisterEnterDocumentVisitor(r) + walker.RegisterEnterEnumTypeDefinitionVisitor(r) + walker.RegisterEnterScalarTypeDefinitionVisitor(r) + walker.RegisterEnterUnionTypeDefinitionVisitor(r) + walker.RegisterLeaveDocumentVisitor(r) +} + +func (r *removeDuplicateFieldlessSharedTypesVisitor) EnterDocument(operation, _ *ast.Document) { + r.document = operation +} + +func (r *removeDuplicateFieldlessSharedTypesVisitor) EnterEnumTypeDefinition(ref int) { + if ref <= r.lastEnumRef { + return + } + name := r.document.EnumTypeDefinitionNameString(ref) + enum, exists := r.sharedTypeSet[name] + if exists { + if !enum.areValuesIdentical(r.document.EnumTypeDefinitions[ref].EnumValuesDefinition.Refs) { + r.StopWithExternalErr(operationreport.ErrSharedTypesMustBeIdenticalToFederate(name)) + return + } + r.rootNodesToRemove = append(r.rootNodesToRemove, ast.Node{Kind: ast.NodeKindEnumTypeDefinition, Ref: ref}) + } else { + r.sharedTypeSet[name] = newEnumSharedType(r.document, ref) + } + r.lastEnumRef = ref +} + +func (r *removeDuplicateFieldlessSharedTypesVisitor) EnterScalarTypeDefinition(ref int) { + if ref <= r.lastScalarRef { + return + } + name := r.document.ScalarTypeDefinitionNameString(ref) + _, exists := r.sharedTypeSet[name] + if exists { + r.rootNodesToRemove = append(r.rootNodesToRemove, ast.Node{Kind: ast.NodeKindScalarTypeDefinition, Ref: ref}) + } else { + r.sharedTypeSet[name] = scalarSharedType{} + } + r.lastScalarRef = ref +} + +func (r *removeDuplicateFieldlessSharedTypesVisitor) EnterUnionTypeDefinition(ref int) { + if ref <= r.lastUnionRef { + return + } + name := r.document.UnionTypeDefinitionNameString(ref) + union, exists := r.sharedTypeSet[name] + if exists { + if !union.areValuesIdentical(r.document.UnionTypeDefinitions[ref].UnionMemberTypes.Refs) { + r.StopWithExternalErr(operationreport.ErrSharedTypesMustBeIdenticalToFederate(name)) + return + } + r.rootNodesToRemove = append(r.rootNodesToRemove, ast.Node{Kind: ast.NodeKindUnionTypeDefinition, Ref: ref}) + } else { + r.sharedTypeSet[name] = newUnionSharedType(r.document, ref) + } + r.lastUnionRef = ref +} + +func (r *removeDuplicateFieldlessSharedTypesVisitor) LeaveDocument(_, _ *ast.Document) { + if r.rootNodesToRemove != nil { + r.document.DeleteRootNodes(r.rootNodesToRemove) + } +} diff --git a/sdlmerge/remove_empty_object_type_definition.go b/sdlmerge/remove_empty_object_type_definition.go new file mode 100644 index 0000000..8c7e08d --- /dev/null +++ b/sdlmerge/remove_empty_object_type_definition.go @@ -0,0 +1,32 @@ +package sdlmerge + +import ( + "github.com/wundergraph/graphql-go-tools/v2/pkg/ast" + "github.com/wundergraph/graphql-go-tools/v2/pkg/astvisitor" +) + +func newRemoveEmptyObjectTypeDefinition() *removeEmptyObjectTypeDefinition { + return &removeEmptyObjectTypeDefinition{} +} + +type removeEmptyObjectTypeDefinition struct{} + +func (r *removeEmptyObjectTypeDefinition) Register(walker *astvisitor.Walker) { + walker.RegisterLeaveDocumentVisitor(r) +} + +func (r *removeEmptyObjectTypeDefinition) LeaveDocument(operation, _ *ast.Document) { + for ref := range operation.ObjectTypeDefinitions { + if operation.ObjectTypeDefinitions[ref].HasFieldDefinitions { + continue + } + + name := operation.ObjectTypeDefinitionNameString(ref) + node, ok := operation.Index.FirstNodeByNameStr(name) + if !ok { + return + } + + operation.RemoveRootNode(node) + } +} diff --git a/sdlmerge/remove_field_definition_by_directive.go b/sdlmerge/remove_field_definition_by_directive.go new file mode 100644 index 0000000..40530ee --- /dev/null +++ b/sdlmerge/remove_field_definition_by_directive.go @@ -0,0 +1,46 @@ +package sdlmerge + +import ( + "github.com/wundergraph/graphql-go-tools/v2/pkg/ast" + "github.com/wundergraph/graphql-go-tools/v2/pkg/astvisitor" +) + +func newRemoveFieldDefinitions(directives ...string) *removeFieldDefinitionByDirective { + directivesSet := make(map[string]struct{}, len(directives)) + for _, directive := range directives { + directivesSet[directive] = struct{}{} + } + + return &removeFieldDefinitionByDirective{ + directives: directivesSet, + } +} + +type removeFieldDefinitionByDirective struct { + operation *ast.Document + directives map[string]struct{} +} + +func (r *removeFieldDefinitionByDirective) Register(walker *astvisitor.Walker) { + walker.RegisterEnterDocumentVisitor(r) + walker.RegisterLeaveObjectTypeDefinitionVisitor(r) +} + +func (r *removeFieldDefinitionByDirective) EnterDocument(operation, _ *ast.Document) { + r.operation = operation +} + +func (r *removeFieldDefinitionByDirective) LeaveObjectTypeDefinition(ref int) { + var refsForDeletion []int + // select fields for deletion + for _, fieldRef := range r.operation.ObjectTypeDefinitions[ref].FieldsDefinition.Refs { + for _, directiveRef := range r.operation.FieldDefinitions[fieldRef].Directives.Refs { + directiveName := r.operation.DirectiveNameString(directiveRef) + if _, ok := r.directives[directiveName]; ok { + refsForDeletion = append(refsForDeletion, fieldRef) + } + } + } + // delete fields + r.operation.RemoveFieldDefinitionsFromObjectTypeDefinition(refsForDeletion, ref) +} diff --git a/sdlmerge/remove_field_definition_directive.go b/sdlmerge/remove_field_definition_directive.go new file mode 100644 index 0000000..c3c0756 --- /dev/null +++ b/sdlmerge/remove_field_definition_directive.go @@ -0,0 +1,44 @@ +package sdlmerge + +import ( + "github.com/wundergraph/graphql-go-tools/v2/pkg/ast" + "github.com/wundergraph/graphql-go-tools/v2/pkg/astvisitor" +) + +func newRemoveFieldDefinitionDirective(directives ...string) *removeFieldDefinitionDirective { + directivesSet := make(map[string]struct{}, len(directives)) + for _, directive := range directives { + directivesSet[directive] = struct{}{} + } + + return &removeFieldDefinitionDirective{ + directives: directivesSet, + } +} + +type removeFieldDefinitionDirective struct { + operation *ast.Document + directives map[string]struct{} +} + +func (r *removeFieldDefinitionDirective) Register(walker *astvisitor.Walker) { + walker.RegisterEnterDocumentVisitor(r) + walker.RegisterEnterFieldDefinitionVisitor(r) +} + +func (r *removeFieldDefinitionDirective) EnterDocument(operation, _ *ast.Document) { + r.operation = operation +} + +func (r *removeFieldDefinitionDirective) EnterFieldDefinition(ref int) { + var refsForDeletion []int + // select directives for deletion + for _, directiveRef := range r.operation.FieldDefinitions[ref].Directives.Refs { + directiveName := r.operation.DirectiveNameString(directiveRef) + if _, ok := r.directives[directiveName]; ok { + refsForDeletion = append(refsForDeletion, directiveRef) + } + } + // delete directives + r.operation.RemoveDirectivesFromNode(ast.Node{Kind: ast.NodeKindFieldDefinition, Ref: ref}, refsForDeletion) +} diff --git a/sdlmerge/remove_interface_definition_directive.go b/sdlmerge/remove_interface_definition_directive.go new file mode 100644 index 0000000..29988a8 --- /dev/null +++ b/sdlmerge/remove_interface_definition_directive.go @@ -0,0 +1,45 @@ +package sdlmerge + +import ( + "github.com/wundergraph/graphql-go-tools/v2/pkg/ast" + "github.com/wundergraph/graphql-go-tools/v2/pkg/astvisitor" +) + +func newRemoveInterfaceDefinitionDirective(directives ...string) *removeInterfaceDefinitionDirective { + directivesSet := make(map[string]struct{}, len(directives)) + for _, directive := range directives { + directivesSet[directive] = struct{}{} + } + + return &removeInterfaceDefinitionDirective{ + directives: directivesSet, + } +} + +type removeInterfaceDefinitionDirective struct { + *astvisitor.Walker + operation *ast.Document + directives map[string]struct{} +} + +func (r *removeInterfaceDefinitionDirective) Register(walker *astvisitor.Walker) { + walker.RegisterEnterDocumentVisitor(r) + walker.RegisterEnterInterfaceTypeDefinitionVisitor(r) +} + +func (r *removeInterfaceDefinitionDirective) EnterDocument(operation, _ *ast.Document) { + r.operation = operation +} + +func (r *removeInterfaceDefinitionDirective) EnterInterfaceTypeDefinition(ref int) { + var refsForDeletion []int + // select fields for deletion + for _, directiveRef := range r.operation.InterfaceTypeDefinitions[ref].Directives.Refs { + directiveName := r.operation.DirectiveNameString(directiveRef) + if _, ok := r.directives[directiveName]; ok { + refsForDeletion = append(refsForDeletion, directiveRef) + } + } + // delete directives + r.operation.RemoveDirectivesFromNode(ast.Node{Kind: ast.NodeKindInterfaceTypeDefinition, Ref: ref}, refsForDeletion) +} diff --git a/sdlmerge/remove_object_type_definition_directive.go b/sdlmerge/remove_object_type_definition_directive.go new file mode 100644 index 0000000..0db0aac --- /dev/null +++ b/sdlmerge/remove_object_type_definition_directive.go @@ -0,0 +1,44 @@ +package sdlmerge + +import ( + "github.com/wundergraph/graphql-go-tools/v2/pkg/ast" + "github.com/wundergraph/graphql-go-tools/v2/pkg/astvisitor" +) + +func newRemoveObjectTypeDefinitionDirective(directives ...string) *removeObjectTypeDefinitionDirective { + directivesSet := make(map[string]struct{}, len(directives)) + for _, directive := range directives { + directivesSet[directive] = struct{}{} + } + + return &removeObjectTypeDefinitionDirective{ + directives: directivesSet, + } +} + +type removeObjectTypeDefinitionDirective struct { + operation *ast.Document + directives map[string]struct{} +} + +func (r *removeObjectTypeDefinitionDirective) Register(walker *astvisitor.Walker) { + walker.RegisterEnterDocumentVisitor(r) + walker.RegisterEnterObjectTypeDefinitionVisitor(r) +} + +func (r *removeObjectTypeDefinitionDirective) EnterDocument(operation, _ *ast.Document) { + r.operation = operation +} + +func (r *removeObjectTypeDefinitionDirective) EnterObjectTypeDefinition(ref int) { + var refsForDeletion []int + // select fields for deletion + for _, directiveRef := range r.operation.ObjectTypeDefinitions[ref].Directives.Refs { + directiveName := r.operation.DirectiveNameString(directiveRef) + if _, ok := r.directives[directiveName]; ok { + refsForDeletion = append(refsForDeletion, directiveRef) + } + } + // delete directives + r.operation.RemoveDirectivesFromNode(ast.Node{Kind: ast.NodeKindObjectTypeDefinition, Ref: ref}, refsForDeletion) +} diff --git a/sdlmerge/remove_type_extensions.go b/sdlmerge/remove_type_extensions.go new file mode 100644 index 0000000..51445f8 --- /dev/null +++ b/sdlmerge/remove_type_extensions.go @@ -0,0 +1,20 @@ +package sdlmerge + +import ( + "github.com/wundergraph/graphql-go-tools/v2/pkg/ast" + "github.com/wundergraph/graphql-go-tools/v2/pkg/astvisitor" +) + +func newRemoveMergedTypeExtensions() *removeMergedTypeExtensionsVisitor { + return &removeMergedTypeExtensionsVisitor{} +} + +type removeMergedTypeExtensionsVisitor struct{} + +func (r *removeMergedTypeExtensionsVisitor) Register(walker *astvisitor.Walker) { + walker.RegisterLeaveDocumentVisitor(r) +} + +func (r *removeMergedTypeExtensionsVisitor) LeaveDocument(operation, definition *ast.Document) { + operation.RemoveMergedTypeExtensions() +} diff --git a/sdlmerge/scalar_type_extending.go b/sdlmerge/scalar_type_extending.go new file mode 100644 index 0000000..cb4761b --- /dev/null +++ b/sdlmerge/scalar_type_extending.go @@ -0,0 +1,49 @@ +package sdlmerge + +import ( + "github.com/wundergraph/graphql-go-tools/v2/pkg/ast" + "github.com/wundergraph/graphql-go-tools/v2/pkg/astvisitor" + "github.com/wundergraph/graphql-go-tools/v2/pkg/operationreport" +) + +func newExtendScalarTypeDefinition() *extendScalarTypeDefinitionVisitor { + return &extendScalarTypeDefinitionVisitor{} +} + +type extendScalarTypeDefinitionVisitor struct { + *astvisitor.Walker + document *ast.Document +} + +func (e *extendScalarTypeDefinitionVisitor) Register(walker *astvisitor.Walker) { + e.Walker = walker + walker.RegisterEnterDocumentVisitor(e) + walker.RegisterEnterScalarTypeExtensionVisitor(e) +} + +func (e *extendScalarTypeDefinitionVisitor) EnterDocument(operation, _ *ast.Document) { + e.document = operation +} + +func (e *extendScalarTypeDefinitionVisitor) EnterScalarTypeExtension(ref int) { + nodes, exists := e.document.Index.NodesByNameBytes(e.document.ScalarTypeExtensionNameBytes(ref)) + if !exists { + return + } + + hasExtended := false + for i := range nodes { + if nodes[i].Kind != ast.NodeKindScalarTypeDefinition { + continue + } + if hasExtended { + e.StopWithExternalErr(operationreport.ErrSharedTypesMustNotBeExtended(e.document.ScalarTypeExtensionNameString(ref))) + return + } + e.document.ExtendScalarTypeDefinitionByScalarTypeExtension(nodes[i].Ref, ref) + hasExtended = true + } + if !hasExtended { + e.StopWithExternalErr(operationreport.ErrExtensionOrphansMustResolveInSupergraph(e.document.ScalarTypeExtensionNameBytes(ref))) + } +} diff --git a/sdlmerge/sdlmerge.go b/sdlmerge/sdlmerge.go new file mode 100644 index 0000000..44d6751 --- /dev/null +++ b/sdlmerge/sdlmerge.go @@ -0,0 +1,205 @@ +package sdlmerge + +import ( + "fmt" + "strings" + + "github.com/wundergraph/graphql-go-tools/v2/pkg/asttransform" + "github.com/wundergraph/graphql-go-tools/v2/pkg/astvalidation" + + "github.com/wundergraph/graphql-go-tools/v2/pkg/ast" + "github.com/wundergraph/graphql-go-tools/v2/pkg/astnormalization" + "github.com/wundergraph/graphql-go-tools/v2/pkg/astparser" + "github.com/wundergraph/graphql-go-tools/v2/pkg/astprinter" + "github.com/wundergraph/graphql-go-tools/v2/pkg/astvisitor" + "github.com/wundergraph/graphql-go-tools/v2/pkg/operationreport" +) + +const ( + rootOperationTypeDefinitions = ` + type Query {} + type Mutation {} + type Subscription {} + ` + + parseDocumentError = "parse graphql document string: %w" +) + +type Visitor interface { + Register(walker *astvisitor.Walker) +} + +func MergeAST(ast *ast.Document) error { + normalizer := normalizer{} + normalizer.setupWalkers() + + return normalizer.normalize(ast) +} + +func MergeSDLs(SDLs ...string) (string, error) { + rawDocs := make([]string, 0, len(SDLs)+1) + rawDocs = append(rawDocs, rootOperationTypeDefinitions) + rawDocs = append(rawDocs, SDLs...) + if validationError := validateSubgraphs(rawDocs[1:]); validationError != nil { + return "", validationError + } + if normalizationError := normalizeSubgraphs(rawDocs[1:]); normalizationError != nil { + return "", normalizationError + } + + doc, report := astparser.ParseGraphqlDocumentString(strings.Join(rawDocs, "\n")) + if report.HasErrors() { + return "", fmt.Errorf("parse graphql document string: %w", report) + } + + astnormalization.NormalizeSubgraphSDL(&doc, &report) + if report.HasErrors() { + return "", fmt.Errorf("merge ast: %w", report) + } + + if err := MergeAST(&doc); err != nil { + return "", fmt.Errorf("merge ast: %w", err) + } + + out, err := astprinter.PrintString(&doc) + if err != nil { + return "", fmt.Errorf("stringify schema: %w", err) + } + + return out, nil +} + +func validateSubgraphs(subgraphs []string) error { + validator := astvalidation.NewDefinitionValidator( + astvalidation.PopulatedTypeBodies(), astvalidation.KnownTypeNames(), + ) + for _, subgraph := range subgraphs { + doc, report := astparser.ParseGraphqlDocumentString(subgraph) + if err := asttransform.MergeDefinitionWithBaseSchema(&doc); err != nil { + return err + } + if report.HasErrors() { + return fmt.Errorf(parseDocumentError, report) + } + validator.Validate(&doc, &report) + if report.HasErrors() { + return fmt.Errorf("validate schema: %w", report) + } + } + return nil +} + +func normalizeSubgraphs(subgraphs []string) error { + subgraphNormalizer := astnormalization.NewSubgraphDefinitionNormalizer() + for i, subgraph := range subgraphs { + doc, report := astparser.ParseGraphqlDocumentString(subgraph) + if report.HasErrors() { + return fmt.Errorf(parseDocumentError, report) + } + subgraphNormalizer.NormalizeDefinition(&doc, &report) + if report.HasErrors() { + return fmt.Errorf("normalize schema: %w", report) + } + out, err := astprinter.PrintString(&doc) + if err != nil { + return fmt.Errorf("stringify schema: %w", err) + } + subgraphs[i] = out + } + return nil +} + +type normalizer struct { + walkers []*astvisitor.Walker +} + +type entitySet map[string]struct{} + +func (m *normalizer) setupWalkers() { + collectedEntities := make(entitySet) + visitorGroups := [][]Visitor{ + { + newCollectEntitiesVisitor(collectedEntities), + }, + { + newExtendEnumTypeDefinition(), + newExtendInputObjectTypeDefinition(), + newExtendInterfaceTypeDefinition(collectedEntities), + newExtendScalarTypeDefinition(), + newExtendUnionTypeDefinition(), + newExtendObjectTypeDefinition(collectedEntities), + newRemoveEmptyObjectTypeDefinition(), + newRemoveMergedTypeExtensions(), + }, + // visitors for cleaning up federated duplicated fields and directives + { + newRemoveFieldDefinitions("external"), + newRemoveDuplicateFieldedSharedTypesVisitor(), + newRemoveDuplicateFieldlessSharedTypesVisitor(), + newMergeDuplicatedFieldsVisitor(), + newRemoveInterfaceDefinitionDirective("key"), + newRemoveObjectTypeDefinitionDirective("key"), + newRemoveFieldDefinitionDirective("provides", "requires"), + }, + } + + for _, visitorGroup := range visitorGroups { + walker := astvisitor.NewWalker(48) + for _, visitor := range visitorGroup { + visitor.Register(&walker) + m.walkers = append(m.walkers, &walker) + } + } +} + +func (m *normalizer) normalize(operation *ast.Document) error { + report := operationreport.Report{} + + for _, walker := range m.walkers { + walker.Walk(operation, nil, &report) + if report.HasErrors() { + return fmt.Errorf("walk: %w", report) + } + } + + return nil +} + +func (e entitySet) isExtensionForEntity(nameBytes []byte, directiveRefs []int, document *ast.Document) (bool, *operationreport.ExternalError) { + name := string(nameBytes) + hasDirectives := len(directiveRefs) > 0 + if _, exists := e[name]; !exists { + if !hasDirectives || !isEntityExtension(directiveRefs, document) { + return false, nil + } + err := operationreport.ErrExtensionWithKeyDirectiveMustExtendEntity(name) + return false, &err + } + if !hasDirectives { + err := operationreport.ErrEntityExtensionMustHaveKeyDirective(name) + return false, &err + } + if isEntityExtension(directiveRefs, document) { + return true, nil + } + err := operationreport.ErrEntityExtensionMustHaveKeyDirective(name) + return false, &err +} + +func isEntityExtension(directiveRefs []int, document *ast.Document) bool { + for _, directiveRef := range directiveRefs { + if document.DirectiveNameString(directiveRef) == "key" { + return true + } + } + return false +} + +func multipleExtensionError(isEntity bool, nameBytes []byte) *operationreport.ExternalError { + if isEntity { + err := operationreport.ErrEntitiesMustNotBeDuplicated(string(nameBytes)) + return &err + } + err := operationreport.ErrSharedTypesMustNotBeExtended(string(nameBytes)) + return &err +} diff --git a/sdlmerge/shared_types.go b/sdlmerge/shared_types.go new file mode 100644 index 0000000..4fdfb64 --- /dev/null +++ b/sdlmerge/shared_types.go @@ -0,0 +1,167 @@ +package sdlmerge + +import "github.com/wundergraph/graphql-go-tools/v2/pkg/ast" + +type fieldlessSharedType interface { + areValuesIdentical(valueRefsToCompare []int) bool + valueRefs() []int + valueName(ref int) string +} + +func createValueSet(f fieldlessSharedType) map[string]bool { + valueSet := make(map[string]bool) + for _, valueRef := range f.valueRefs() { + valueSet[f.valueName(valueRef)] = true + } + return valueSet +} + +type fieldedSharedType struct { + document *ast.Document + fieldKind ast.NodeKind + fieldRefs []int + fieldSet map[string]int +} + +func newFieldedSharedType(document *ast.Document, fieldKind ast.NodeKind, fieldRefs []int) fieldedSharedType { + f := fieldedSharedType{ + document, + fieldKind, + fieldRefs, + nil, + } + f.createFieldSet() + return f +} + +func (f fieldedSharedType) areFieldsIdentical(fieldRefsToCompare []int) bool { + if len(f.fieldRefs) != len(fieldRefsToCompare) { + return false + } + for _, fieldRef := range fieldRefsToCompare { + actualFieldName := f.fieldName(fieldRef) + expectedTypeRef, exists := f.fieldSet[actualFieldName] + if !exists { + return false + } + actualTypeRef := f.fieldTypeRef(fieldRef) + if !f.document.TypesAreCompatibleDeep(expectedTypeRef, actualTypeRef) { + return false + } + } + return true +} + +func (f *fieldedSharedType) createFieldSet() { + fieldSet := make(map[string]int) + for _, fieldRef := range f.fieldRefs { + fieldSet[f.fieldName(fieldRef)] = f.fieldTypeRef(fieldRef) + } + f.fieldSet = fieldSet +} + +func (f fieldedSharedType) fieldName(ref int) string { + switch f.fieldKind { + case ast.NodeKindInputValueDefinition: + return f.document.InputValueDefinitionNameString(ref) + default: + return f.document.FieldDefinitionNameString(ref) + } +} + +func (f fieldedSharedType) fieldTypeRef(ref int) int { + switch f.fieldKind { + case ast.NodeKindInputValueDefinition: + return f.document.InputValueDefinitions[ref].Type + default: + return f.document.FieldDefinitions[ref].Type + } +} + +type enumSharedType struct { + *ast.EnumTypeDefinition + document *ast.Document + valueSet map[string]bool +} + +func newEnumSharedType(document *ast.Document, ref int) enumSharedType { + e := enumSharedType{ + &document.EnumTypeDefinitions[ref], + document, + nil, + } + e.valueSet = createValueSet(e) + return e +} + +func (e enumSharedType) areValuesIdentical(valueRefsToCompare []int) bool { + if len(e.valueRefs()) != len(valueRefsToCompare) { + return false + } + for _, valueRefToCompare := range valueRefsToCompare { + name := e.valueName(valueRefToCompare) + if !e.valueSet[name] { + return false + } + } + return true +} + +func (e enumSharedType) valueRefs() []int { + return e.EnumValuesDefinition.Refs +} + +func (e enumSharedType) valueName(ref int) string { + return e.document.EnumValueDefinitionNameString(ref) +} + +type unionSharedType struct { + *ast.UnionTypeDefinition + document *ast.Document + valueSet map[string]bool +} + +func newUnionSharedType(document *ast.Document, ref int) unionSharedType { + u := unionSharedType{ + &document.UnionTypeDefinitions[ref], + document, + nil, + } + u.valueSet = createValueSet(u) + return u +} + +func (u unionSharedType) areValuesIdentical(valueRefsToCompare []int) bool { + if len(u.valueRefs()) != len(valueRefsToCompare) { + return false + } + for _, refToCompare := range valueRefsToCompare { + name := u.valueName(refToCompare) + if !u.valueSet[name] { + return false + } + } + return true +} + +func (u unionSharedType) valueRefs() []int { + return u.UnionMemberTypes.Refs +} + +func (u unionSharedType) valueName(ref int) string { + return u.document.TypeNameString(ref) +} + +type scalarSharedType struct{} + +func (_ scalarSharedType) areValuesIdentical(_ []int) bool { + return true +} + +func (_ scalarSharedType) valueRefs() []int { + return nil +} + +func (_ scalarSharedType) valueName(_ int) string { + return "" +} diff --git a/sdlmerge/union_type_extending.go b/sdlmerge/union_type_extending.go new file mode 100644 index 0000000..d17615d --- /dev/null +++ b/sdlmerge/union_type_extending.go @@ -0,0 +1,50 @@ +package sdlmerge + +import ( + "github.com/wundergraph/graphql-go-tools/v2/pkg/ast" + "github.com/wundergraph/graphql-go-tools/v2/pkg/astvisitor" + "github.com/wundergraph/graphql-go-tools/v2/pkg/operationreport" +) + +func newExtendUnionTypeDefinition() *extendUnionTypeDefinitionVisitor { + return &extendUnionTypeDefinitionVisitor{} +} + +type extendUnionTypeDefinitionVisitor struct { + *astvisitor.Walker + document *ast.Document +} + +func (e *extendUnionTypeDefinitionVisitor) Register(walker *astvisitor.Walker) { + e.Walker = walker + walker.RegisterEnterDocumentVisitor(e) + walker.RegisterEnterUnionTypeExtensionVisitor(e) +} + +func (e *extendUnionTypeDefinitionVisitor) EnterDocument(operation, _ *ast.Document) { + e.document = operation +} + +func (e *extendUnionTypeDefinitionVisitor) EnterUnionTypeExtension(ref int) { + nodes, exists := e.document.Index.NodesByNameBytes(e.document.UnionTypeExtensionNameBytes(ref)) + if !exists { + return + } + + hasExtended := false + for i := range nodes { + if nodes[i].Kind != ast.NodeKindUnionTypeDefinition { + continue + } + if hasExtended { + e.StopWithExternalErr(operationreport.ErrSharedTypesMustNotBeExtended(e.document.UnionTypeExtensionNameString(ref))) + return + } + e.document.ExtendUnionTypeDefinitionByUnionTypeExtension(nodes[i].Ref, ref) + hasExtended = true + } + + if !hasExtended { + e.StopWithExternalErr(operationreport.ErrExtensionOrphansMustResolveInSupergraph(e.document.UnionTypeExtensionNameBytes(ref))) + } +}