Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: 👽 Added support for nodes query #217

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion config.go
Original file line number Diff line number Diff line change
Expand Up @@ -331,9 +331,16 @@ func (c *Config) Init() error {
return fmt.Errorf("error building service list: %w", err)
}

serviceClientOptions := []ClientOpt{
WithMaxResponseSize(c.MaxServiceResponseSize),
}
if c.QueryHTTPClient != nil {
serviceClientOptions = append(serviceClientOptions, WithHTTPClient(c.QueryHTTPClient))
}

var services []*Service
for _, s := range c.Services {
services = append(services, NewService(s))
services = append(services, NewService(s, serviceClientOptions...))
}

queryClientOptions := []ClientOpt{WithMaxResponseSize(c.MaxServiceResponseSize), WithUserAgent(GenerateUserAgent("query"))}
Expand Down
2 changes: 1 addition & 1 deletion executable_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func (s *ExecutableSchema) UpdateServiceList(services []string) error {
if svc, ok := s.Services[svcURL]; ok {
newServices[svcURL] = svc
} else {
newServices[svcURL] = NewService(svcURL)
newServices[svcURL] = NewService(svcURL, WithHTTPClient(s.GraphqlClient.HTTPClient))
}
}
s.Services = newServices
Expand Down
5 changes: 3 additions & 2 deletions introspection.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ type Service struct {
}

// NewService returns a new Service.
func NewService(serviceURL string) *Service {
func NewService(serviceURL string, opts ...ClientOpt) *Service {
opts = append(opts, WithUserAgent(GenerateUserAgent("update")))
s := &Service{
ServiceURL: serviceURL,
client: NewClientWithoutKeepAlive(WithUserAgent(GenerateUserAgent("update"))),
client: NewClientWithoutKeepAlive(opts...),
}
return s
}
Expand Down
14 changes: 11 additions & 3 deletions merge.go
Original file line number Diff line number Diff line change
Expand Up @@ -405,13 +405,21 @@ func hasIDField(t *ast.Definition) bool {
}

func isNodeField(f *ast.FieldDefinition) bool {
if f.Name != nodeRootFieldName || len(f.Arguments) != 1 {
if (f.Name != nodeRootFieldName && f.Name != nodesRootFieldName) || len(f.Arguments) != 1 {
return false
}
arg := f.Arguments[0]
return arg.Name == IdFieldName &&

isNode := (arg.Name == IdFieldName &&
isIDType(arg.Type) &&
isNullableTypeNamed(f.Type, nodeInterfaceName)
isNullableTypeNamed(f.Type, nodeInterfaceName))

isNodes := (arg.Name == IdsFieldName &&
isIDsType(arg.Type) &&
f.Type != nil &&
isNullableTypeNamed(f.Type.Elem, nodeInterfaceName))

return isNode || isNodes
}

func isIDField(f *ast.FieldDefinition) bool {
Expand Down
10 changes: 9 additions & 1 deletion schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@ import (
"github.com/vektah/gqlparser/v2/ast"
)

var IdFieldName = "id"
var (
IdFieldName = "id"
IdsFieldName = "ids"
)

const (
nodeRootFieldName = "node"
nodesRootFieldName = "nodes"
nodeInterfaceName = "Node"
serviceObjectName = "Service"
serviceRootFieldName = "service"
Expand All @@ -31,6 +35,10 @@ func isIDType(t *ast.Type) bool {
return isNonNullableTypeNamed(t, "ID")
}

func isIDsType(t *ast.Type) bool {
return t.Elem != nil && isNonNullableTypeNamed(t, "ID") && isNonNullableTypeNamed(t.Elem, "ID")
}

func isNonNullableTypeNamed(t *ast.Type, typename string) bool {
return t.Name() == typename && t.NonNull
}
Expand Down
92 changes: 78 additions & 14 deletions validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,17 +54,21 @@ func validateBoundaryObjects(schema *ast.Schema) error {
return err
}
}
} else {
if err := validateNodeInterface(schema); err != nil {
}

if hasNodeQuery(schema) {
if err := validateNodeQuery(schema); err != nil {
return err
}
if err := validateImplementsNode(schema); err != nil {
}
if hasNodesQuery(schema) {
if err := validateNodesQuery(schema); err != nil {
return err
}
}

if hasNodeQuery(schema) {
if err := validateNodeQuery(schema); err != nil {
if hasNodesQuery(schema) {
if err := validateNodesQuery(schema); err != nil {
return err
}
}
Expand Down Expand Up @@ -108,10 +112,16 @@ func validateServiceObject(schema *ast.Schema) error {
switch field.Name {
case "name", "version", "schema":
if !isNonNullableTypeNamed(field.Type, "String") {
return fmt.Errorf("the Service object should have a field called '%s' of type 'String!'", field.Name)
return fmt.Errorf(
"the Service object should have a field called '%s' of type 'String!'",
field.Name,
)
}
default:
return fmt.Errorf("the Service object should not have a field called %s", field.Name)
return fmt.Errorf(
"the Service object should not have a field called %s",
field.Name,
)
}
}
return nil
Expand All @@ -138,6 +148,32 @@ func validateServiceQuery(schema *ast.Schema) error {
return fmt.Errorf("the Query type is missing the 'service' field")
}

func validateNodesQuery(schema *ast.Schema) error {
if schema.Query == nil {
return fmt.Errorf("the schema is missing a Query type")
}
for _, f := range schema.Query.Fields {
if f.Name != nodesRootFieldName {
continue
}
if len(f.Arguments) != 1 {
return fmt.Errorf("the 'nodes' field of Query must take a single argument")
}
arg := f.Arguments[0]
if arg.Name != IdsFieldName {
return fmt.Errorf("the 'nodes' field of Query must take a single argument called 'ids'")
}
if !isIDsType(arg.Type) {
return fmt.Errorf("the 'nodes' field of Query must take a single argument of type 'ID!'")
}
if f.Type == nil || f.Type.Elem == nil || !isNullableTypeNamed(f.Type.Elem, nodeInterfaceName) {
return fmt.Errorf("the 'nodes' field of Query must be of type '[Node]!'")
}
return nil
}
return fmt.Errorf("the Query type is missing the 'nodes' field")
}

func validateNodeQuery(schema *ast.Schema) error {
if schema.Query == nil {
return fmt.Errorf("the schema is missing a Query type")
Expand Down Expand Up @@ -198,7 +234,10 @@ func validateImplementsNode(schema *ast.Schema) error {
if implementsNode(schema, t) {
continue
}
return fmt.Errorf("object '%s' has the boundary directive but doesn't implement Node", t.Name)
return fmt.Errorf(
"object '%s' has the boundary directive but doesn't implement Node",
t.Name,
)
}
return nil
}
Expand All @@ -212,6 +251,10 @@ func implementsNode(schema *ast.Schema, def *ast.Definition) bool {
return false
}

func hasNodesQuery(schema *ast.Schema) bool {
return schema.Query.Fields.ForName(nodesRootFieldName) != nil
}

func hasNodeQuery(schema *ast.Schema) bool {
return schema.Query.Fields.ForName(nodeRootFieldName) != nil
}
Expand Down Expand Up @@ -247,7 +290,11 @@ func validateNamespaceDirective(schema *ast.Schema) error {
return fmt.Errorf("@namespace directive not found")
}

func validateNamespacesFields(schema *ast.Schema, currentType *ast.Definition, rootType string) error {
func validateNamespacesFields(
schema *ast.Schema,
currentType *ast.Definition,
rootType string,
) error {
if currentType == nil {
return nil
}
Expand All @@ -256,7 +303,11 @@ func validateNamespacesFields(schema *ast.Schema, currentType *ast.Definition, r
ft := schema.Types[f.Type.Name()]
if isNamespaceObject(ft) {
if !f.Type.NonNull {
return fmt.Errorf("namespace return type should be non nullable on %s.%s", currentType.Name, f.Name)
return fmt.Errorf(
"namespace return type should be non nullable on %s.%s",
currentType.Name,
f.Name,
)
}

err := validateNamespacesFields(schema, ft, rootType)
Expand All @@ -272,14 +323,20 @@ func validateNamespacesFields(schema *ast.Schema, currentType *ast.Definition, r
// validateNamespaceTypesAscendence validates that namespace types are only used in other namespaces type or Query/Mutation/Subscription
func validateNamespaceTypesAscendence(schema *ast.Schema) error {
for _, t := range schema.Types {
if isNamespaceObject(t) || t.Name == queryObjectName || t.Name == mutationObjectName || t.Name == subscriptionObjectName {
if isNamespaceObject(t) || t.Name == queryObjectName || t.Name == mutationObjectName ||
t.Name == subscriptionObjectName {
continue
}

for _, f := range t.Fields {
ft := schema.Types[f.Type.Name()]
if isNamespaceObject(ft) {
return fmt.Errorf("type %q (namespace type) is used for field %q in non-namespace object %q", ft.Name, f.Name, t.Name)
return fmt.Errorf(
"type %q (namespace type) is used for field %q in non-namespace object %q",
ft.Name,
f.Name,
t.Name,
)
}
}
}
Expand Down Expand Up @@ -370,7 +427,10 @@ func validateBoundaryFields(schema *ast.Schema) error {
}

if len(missingBoundaryQueries) > 0 {
return fmt.Errorf("missing boundary fields for the following types: %v", missingBoundaryQueries)
return fmt.Errorf(
"missing boundary fields for the following types: %v",
missingBoundaryQueries,
)
}

return nil
Expand All @@ -388,7 +448,11 @@ func validateBoundaryObjectsFormat(schema *ast.Schema) error {
}

if idField.Type.String() != "ID!" {
return fmt.Errorf(`%q field should have type "ID!" in boundary type %q`, IdFieldName, t.Name)
return fmt.Errorf(
`%q field should have type "ID!" in boundary type %q`,
IdFieldName,
t.Name,
)
}
}

Expand Down
82 changes: 81 additions & 1 deletion validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ func TestSchemaIsValid(t *testing.T) {
}
type Query {
node(id: ID!): Node
nodes(ids: [ID!]!): [Node]!
service: Service!
}
`).assertValid(ValidateSchema)
Expand Down Expand Up @@ -276,6 +277,86 @@ func TestNodeQuery(t *testing.T) {
})
}

func TestNodesQuery(t *testing.T) {
t.Run("query type missing", func(t *testing.T) {
withSchema(t, "").assertInvalid("the schema is missing a Query type", validateNodesQuery)
})
t.Run("nodes query missing", func(t *testing.T) {
withSchema(t, `
type Query {
other: String
}
`).assertInvalid("the Query type is missing the 'nodes' field", validateNodesQuery)
})
t.Run("query with no arguments", func(t *testing.T) {
withSchema(t, `
type Query {
nodes: ID!
}
`).assertInvalid("the 'nodes' field of Query must take a single argument", validateNodesQuery)
})
t.Run("query with wrong argument name", func(t *testing.T) {
withSchema(t, `
type Query {
nodes(incorrect: ID!): ID!
}
`).assertInvalid("the 'nodes' field of Query must take a single argument called 'ids'", validateNodesQuery)
})
t.Run("query with extra argument", func(t *testing.T) {
withSchema(t, `
type Query {
nodes(ids: [ID!]!, incorrect: String): ID!
}
`).assertInvalid("the 'nodes' field of Query must take a single argument", validateNodesQuery)
})

t.Run("query with wrong nullable array type", func(t *testing.T) {
withSchema(t, `
type Query {
nodes(ids: [ID!]): ID!
}
`).assertInvalid("the 'nodes' field of Query must take a single argument of type 'ID!'", validateNodesQuery)
})
t.Run("query with wrong nullable array type", func(t *testing.T) {
withSchema(t, `
type Query {
nodes(ids: [ID]!): ID!
}
`).assertInvalid("the 'nodes' field of Query must take a single argument of type 'ID!'", validateNodesQuery)
})
t.Run("query with wrong type", func(t *testing.T) {
withSchema(t, `
type Query {
nodes(ids: [ID!]!): ID!
}
`).assertInvalid("the 'nodes' field of Query must be of type '[Node]!'", validateNodesQuery)
})
t.Run("query is correct", func(t *testing.T) {
withSchema(t, `
interface Node {
id: ID!
}
type Query {
nodes(ids: [ID!]!): [Node]!
}
`).assertValid(validateNodesQuery)
})
t.Run("Query is checked if @boundary is used", func(t *testing.T) {
withSchema(t, `
directive @boundary on OBJECT
type Query {
nodes(ids: [ID!]!): ID!
node(id: ID!): ID!
}
interface Node {
id: ID!
}
type Gizmo implements Node @boundary {
id: ID!
}`).assertInvalid("the 'nodes' field of Query must be of type '[Node]!'", validateNodesQuery)
})
}

func TestUnions(t *testing.T) {
t.Run("Unions are supported", func(t *testing.T) {
withSchema(t, `
Expand Down Expand Up @@ -425,7 +506,6 @@ func TestServiceQuery(t *testing.T) {
}
`).assertInvalid("the Query type is missing the 'service' field", ValidateSchema)
})

}

func TestRootObjectNaming(t *testing.T) {
Expand Down
Loading