Skip to content

Commit

Permalink
fix: 👽 Added support for nodes query
Browse files Browse the repository at this point in the history
Ent (sometimes?) generates a "nodes" query to fetch multiple nodes in batch in the latest version . This was not compatible with Bramble so I fixed that

Maybe related to movio#96, not sure
  • Loading branch information
robinminso committed Jul 23, 2023
1 parent 6c49772 commit 40f5e79
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 19 deletions.
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

0 comments on commit 40f5e79

Please sign in to comment.