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.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)))
|
||||
}
|
||||
}
|
||||
@@ -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)))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 ""
|
||||
}
|
||||
@@ -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)))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user