From ee378dc6a3d8ef2fa330201174309daa3e7d733c Mon Sep 17 00:00:00 2001 From: Joakim Olsson Date: Fri, 28 Feb 2025 13:10:07 +0100 Subject: [PATCH] feat(sdlmerge): add shared types for GraphQL schema handling Introduce types for managing fielded, enum, union, and scalar shared types. Implement functionality for comparing values and creating field sets. Enhance schema extensions by integrating new visitors for enum types. --- go.mod | 5 +- go.sum | 14 +- graph/schema.resolvers.go | 2 +- sdlmerge/collect_entities.go | 61 ++++++ sdlmerge/enum_type_extending.go | 50 +++++ sdlmerge/input_type_extending.go | 50 +++++ sdlmerge/interface_type_extending.go | 63 ++++++ sdlmerge/merge_duplicated_fields.go | 62 ++++++ sdlmerge/object_type_extending.go | 66 ++++++ .../remove_duplicate_fielded_shared_types.go | 107 +++++++++ ...remove_duplicate_fieldless_shared_types.go | 98 +++++++++ .../remove_empty_object_type_definition.go | 32 +++ .../remove_field_definition_by_directive.go | 46 ++++ sdlmerge/remove_field_definition_directive.go | 44 ++++ .../remove_interface_definition_directive.go | 45 ++++ ...remove_object_type_definition_directive.go | 44 ++++ sdlmerge/remove_type_extensions.go | 20 ++ sdlmerge/scalar_type_extending.go | 49 +++++ sdlmerge/sdlmerge.go | 205 ++++++++++++++++++ sdlmerge/shared_types.go | 167 ++++++++++++++ sdlmerge/union_type_extending.go | 50 +++++ 21 files changed, 1266 insertions(+), 14 deletions(-) create mode 100644 sdlmerge/collect_entities.go create mode 100644 sdlmerge/enum_type_extending.go create mode 100644 sdlmerge/input_type_extending.go create mode 100644 sdlmerge/interface_type_extending.go create mode 100644 sdlmerge/merge_duplicated_fields.go create mode 100644 sdlmerge/object_type_extending.go create mode 100644 sdlmerge/remove_duplicate_fielded_shared_types.go create mode 100644 sdlmerge/remove_duplicate_fieldless_shared_types.go create mode 100644 sdlmerge/remove_empty_object_type_definition.go create mode 100644 sdlmerge/remove_field_definition_by_directive.go create mode 100644 sdlmerge/remove_field_definition_directive.go create mode 100644 sdlmerge/remove_interface_definition_directive.go create mode 100644 sdlmerge/remove_object_type_definition_directive.go create mode 100644 sdlmerge/remove_type_extensions.go create mode 100644 sdlmerge/scalar_type_extending.go create mode 100644 sdlmerge/sdlmerge.go create mode 100644 sdlmerge/shared_types.go create mode 100644 sdlmerge/union_type_extending.go 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))) + } +}