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

feat(GraphQL): Extend Support For Apollo Federation #7275

Merged
merged 16 commits into from
Jan 28, 2021
Merged
Show file tree
Hide file tree
Changes from 14 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
22 changes: 20 additions & 2 deletions graphql/admin/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ var (
)

func SchemaValidate(sch string) error {
schHandler, err := schema.NewHandler(sch, true)
schHandler, err := schema.NewHandler(sch, true, false)
if err != nil {
return err
}
Expand Down Expand Up @@ -638,7 +638,7 @@ func getCurrentGraphQLSchema() (*gqlSchema, error) {
}

func generateGQLSchema(sch *gqlSchema) (schema.Schema, error) {
schHandler, err := schema.NewHandler(sch.Schema, false)
schHandler, err := schema.NewHandler(sch.Schema, false, false)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -836,6 +836,7 @@ func resolverFactoryWithErrorMsg(msg string) resolve.ResolverFactory {
return resolve.NewResolverFactory(qErr, mErr)
}

// Todo(Minhaj): Fetch NewHandler for service query only once
func (as *adminServer) resetSchema(gqlSchema schema.Schema) {
// set status as updating schema
mainHealthStore.updatingSchema()
Expand All @@ -849,6 +850,23 @@ func (as *adminServer) resetSchema(gqlSchema schema.Schema) {
} else {
resolverFactory = resolverFactoryWithErrorMsg(errResolverNotFound).
WithConventionResolvers(gqlSchema, as.fns)
// If the schema is a Federated Schema then attach "_service" resolver
if gqlSchema.IsFederated() {
resolverFactory.WithQueryResolver("_service", func(s schema.Query) resolve.QueryResolver {
return resolve.QueryResolverFunc(func(ctx context.Context, query schema.Query) *resolve.Resolved {
as.mux.RLock()
defer as.mux.RUnlock()
sch := as.schema.Schema
handler, _ := schema.NewHandler(sch, false, true)
data := handler.GQLSchemaWithoutApolloExtras()
return &resolve.Resolved{
Data: map[string]interface{}{"_service": map[string]interface{}{"sdl": data}},
Field: query,
}
})
})
}

if as.withIntrospection {
resolverFactory.WithSchemaIntrospection()
}
Expand Down
2 changes: 1 addition & 1 deletion graphql/admin/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func (usr *updateSchemaResolver) Resolve(ctx context.Context, m schema.Mutation)

// We just need to validate the schema. Schema is later set in `resetSchema()` when the schema
// is returned from badger.
schHandler, err := schema.NewHandler(input.Set.Schema, true)
schHandler, err := schema.NewHandler(input.Set.Schema, true, false)
if err != nil {
return resolve.EmptyResult(m, err), false
}
Expand Down
9 changes: 5 additions & 4 deletions graphql/dgraph/graphquery.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ func writeUIDFunc(b *strings.Builder, uids []uint64, args []gql.Arg) {
// writeRoot writes the root function as well as any ordering and paging
// specified in q.
//
// Only uid(0x123, 0x124) and type(...) functions are supported at root.
// Only uid(0x123, 0x124), type(...) and eq(Type.Predicate, ...) functions are supported at root.
func writeRoot(b *strings.Builder, q *gql.GraphQuery) {
if q.Func == nil {
return
Expand All @@ -149,9 +149,10 @@ func writeRoot(b *strings.Builder, q *gql.GraphQuery) {
writeUIDFunc(b, q.Func.UID, q.Func.Args)
case q.Func.Name == "type" && len(q.Func.Args) == 1:
x.Check2(b.WriteString(fmt.Sprintf("(func: type(%s)", q.Func.Args[0].Value)))
case q.Func.Name == "eq" && len(q.Func.Args) == 2:
x.Check2(b.WriteString(fmt.Sprintf("(func: eq(%s, %s)", q.Func.Args[0].Value,
q.Func.Args[1].Value)))
case q.Func.Name == "eq":
x.Check2(b.WriteString("(func: eq("))
writeFilterArguments(b, q.Func.Args)
x.Check2(b.WriteRune(')'))
}
writeOrderAndPage(b, q, true)
x.Check2(b.WriteRune(')'))
Expand Down
28 changes: 28 additions & 0 deletions graphql/e2e/auth/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -775,3 +775,31 @@ type Book @auth(
name: String!
desc: String!
}


type Mission @key(fields: "id") @auth(
query:{ rule: """
query($USER: String!) {
queryMission(filter: { supervisorName: {eq: $USER} } ) {
id
}
}""" }
){
id: String! @id
crew: [Astronaut]
supervisorName: String @search(by: [exact])
designation: String!
startDate: String
endDate: String
}

type Astronaut @key(fields: "id") @extends @auth(
query: { rule: "{$ROLE: { eq: \"admin\" } }"},
add: { rule: "{$USER: { eq: \"foo\" } }"},
delete: { rule: "{$USER: { eq: \"foo\" } }"},
update: { rule: "{$USER: { eq: \"foo\" } }"}
){
id: ID! @external
missions: [Mission]
}

8 changes: 8 additions & 0 deletions graphql/e2e/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,11 @@ type country struct {
States []*state `json:"states,omitempty"`
}

type mission struct {
ID string `json:"id,omitempty"`
Designation string `json:"designation,omitempty"`
}

type author struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Expand Down Expand Up @@ -630,6 +635,7 @@ func RunAll(t *testing.T) {
t.Run("query only typename", queryOnlyTypename)
t.Run("query nested only typename", querynestedOnlyTypename)
t.Run("test onlytypename for interface types", onlytypenameForInterface)
t.Run("entitites Query on extended type", entitiesQuery)

t.Run("get state by xid", getStateByXid)
t.Run("get state without args", getStateWithoutArgs)
Expand Down Expand Up @@ -713,6 +719,8 @@ func RunAll(t *testing.T) {
t.Run("mutation id directive with int", idDirectiveWithIntMutation)
t.Run("mutation id directive with int64", idDirectiveWithInt64Mutation)
t.Run("mutation id directive with float", idDirectiveWithFloatMutation)
t.Run("add mutation on extended type with field of ID type as key field", addMutationOnExtendedTypeWithIDasKeyField)
t.Run("add mutation with deep extended type objects", addMutationWithDeepExtendedTypeObjects)

// error tests
t.Run("graphql completion on", graphQLCompletionOn)
Expand Down
146 changes: 146 additions & 0 deletions graphql/e2e/common/mutation.go
Original file line number Diff line number Diff line change
Expand Up @@ -4674,3 +4674,149 @@ func idDirectiveWithFloatMutation(t *testing.T) {

DeleteGqlType(t, "Section", map[string]interface{}{}, 4, nil)
}

func addMutationWithDeepExtendedTypeObjects(t *testing.T) {
varMap1 := map[string]interface{}{
"missionId": "Mission1",
"astronautId": "Astronaut1",
"des": "Apollo1",
}
addMissionParams := &GraphQLParams{
Query: `mutation addMission($missionId: String!, $astronautId: ID!, $des: String!) {
addMission(input: [{id: $missionId, designation: $des, crew: [{id: $astronautId}]}]) {
mission{
id
crew {
id
missions(order: {asc: id}){
id
}
}
}
}
}
`,
Variables: varMap1,
}
gqlResponse := addMissionParams.ExecuteAsPost(t, GraphqlURL)
RequireNoGQLErrors(t, gqlResponse)

expectedJSON := `{
"addMission": {
"mission": [
{
"id": "Mission1",
"crew": [
{
"id": "Astronaut1",
"missions": [
{
"id": "Mission1"
}
]
}
]
}
]
}
}`
testutil.CompareJSON(t, expectedJSON, string(gqlResponse.Data))

varMap2 := map[string]interface{}{
"missionId": "Mission2",
"astronautId": "Astronaut1",
"des": "Apollo2",
}
addMissionParams.Variables = varMap2

gqlResponse1 := addMissionParams.ExecuteAsPost(t, GraphqlURL)
RequireNoGQLErrors(t, gqlResponse)

expectedJSON = `{
"addMission": {
"mission": [
{
"id": "Mission2",
"crew": [
{
"id": "Astronaut1",
"missions": [
{
"id": "Mission1"
},
{
"id": "Mission2"
}
]
}
]
}
]
}
}`
testutil.CompareJSON(t, expectedJSON, string(gqlResponse1.Data))

astronautDeleteFilter := map[string]interface{}{"id": []string{"Astronaut1"}}
DeleteGqlType(t, "Astronaut", astronautDeleteFilter, 1, nil)

missionDeleteFilter := map[string]interface{}{"id": map[string]interface{}{"in": []string{"Mission1", "Mission2"}}}
DeleteGqlType(t, "Mission", missionDeleteFilter, 2, nil)
}

func addMutationOnExtendedTypeWithIDasKeyField(t *testing.T) {
addAstronautParams := &GraphQLParams{
Query: `mutation addAstronaut($id1: ID!, $missionId1: String!, $id2: ID!, $missionId2: String! ) {
addAstronaut(input: [{id: $id1, missions: [{id: $missionId1, designation: "Apollo1"}]}, {id: $id2, missions: [{id: $missionId2, designation: "Apollo2"}]}]) {
astronaut(order: {asc: id}){
id
missions {
id
designation
}
}
}
}`,
Variables: map[string]interface{}{
"id1": "Astronaut1",
"missionId1": "Mission1",
"id2": "Astronaut2",
"missionId2": "Mission2",
},
}

gqlResponse := addAstronautParams.ExecuteAsPost(t, GraphqlURL)
RequireNoGQLErrors(t, gqlResponse)

expectedJSON := `{
"addAstronaut": {
"astronaut": [
{
"id": "Astronaut1",
"missions": [
{
"id": "Mission1",
"designation": "Apollo1"
}
]
},
{
"id": "Astronaut2",
"missions": [
{
"id": "Mission2",
"designation": "Apollo2"
}
]
}
]
}
}`

testutil.CompareJSON(t, expectedJSON, string(gqlResponse.Data))

astronautDeleteFilter := map[string]interface{}{"id": []string{"Astronaut1", "Astronaut2"}}
DeleteGqlType(t, "Astronaut", astronautDeleteFilter, 2, nil)

missionDeleteFilter := map[string]interface{}{"id": map[string]interface{}{"in": []string{"Mission1", "Mission2"}}}
DeleteGqlType(t, "Mission", missionDeleteFilter, 2, nil)
}
68 changes: 67 additions & 1 deletion graphql/e2e/common/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ package common
import (
"encoding/json"
"fmt"
"github.com/spf13/cast"
"io/ioutil"
"math/rand"
"net/http"
Expand All @@ -29,6 +28,8 @@ import (
"testing"
"time"

"github.com/spf13/cast"

"github.com/google/go-cmp/cmp/cmpopts"

"github.com/dgraph-io/dgraph/graphql/schema"
Expand Down Expand Up @@ -374,6 +375,71 @@ func allPosts(t *testing.T) []*post {
return result.QueryPost
}

func entitiesQuery(t *testing.T) {
addSpaceShipParams := &GraphQLParams{
Query: `mutation addSpaceShip($id1: String!, $missionId1: String! ) {
addSpaceShip(input: [{id: $id1, missions: [{id: $missionId1, designation: "Apollo1"}]} ]) {
spaceShip {
id
missions {
id
designation
}
}
}
}`,
Variables: map[string]interface{}{
"id1": "SpaceShip1",
"missionId1": "Mission1",
},
}

gqlResponse := addSpaceShipParams.ExecuteAsPost(t, GraphqlURL)
RequireNoGQLErrors(t, gqlResponse)

entitiesQueryParams := &GraphQLParams{
Query: `query _entities($typeName: String!, $id1: String!){
_entities(representations: [{__typename: $typeName, id: $id1}]) {
... on SpaceShip {
missions(order: {asc: id}){
id
designation
}
}
}
}`,
Variables: map[string]interface{}{
"typeName": "SpaceShip",
"id1": "SpaceShip1",
},
}

entitiesResp := entitiesQueryParams.ExecuteAsPost(t, GraphqlURL)
RequireNoGQLErrors(t, entitiesResp)

expectedJSON := `{
"_entities": [
{
"missions": [
{
"id": "Mission1",
"designation": "Apollo1"
}
]
}
]
}`

testutil.CompareJSON(t, expectedJSON, string(entitiesResp.Data))

spaceShipDeleteFilter := map[string]interface{}{"id": map[string]interface{}{"in": []string{"SpaceShip1"}}}
DeleteGqlType(t, "SpaceShip", spaceShipDeleteFilter, 1, nil)

missionDeleteFilter := map[string]interface{}{"id": map[string]interface{}{"in": []string{"Mission1"}}}
DeleteGqlType(t, "Mission", missionDeleteFilter, 1, nil)

}

func inFilterOnString(t *testing.T) {
addStateParams := &GraphQLParams{
Query: `mutation addState($name1: String!, $code1: String!, $name2: String!, $code2: String! ) {
Expand Down
Loading