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): Add support for GraphQL Upsert Mutations #7433

Merged
merged 9 commits into from
Feb 19, 2021
Merged
Show file tree
Hide file tree
Changes from 7 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
2 changes: 1 addition & 1 deletion graphql/admin/update_group.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func (urw *updateGroupRewriter) Rewrite(
return nil, nil
}

upsertQuery := resolve.RewriteUpsertQueryFromMutation(m, nil)
upsertQuery := resolve.RewriteUpsertQueryFromMutation(m, nil, resolve.MutationQueryVar, "")
srcUID := resolve.MutationQueryVarUID

var errSet, errDel error
Expand Down
191 changes: 191 additions & 0 deletions graphql/e2e/auth/add_mutation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -958,3 +958,194 @@ func TestAddGQLOnly(t *testing.T) {
}
}
}

func TestUpsertMutationsWithRBAC(t *testing.T) {

testCases := []TestCase{{
// First Add Tweets should succeed.
user: "foo",
role: "admin",
variables: map[string]interface{}{
"upsert": true,
"tweet": common.Tweets{
Id: "tweet1",
Text: "abc",
Timestamp: "2020-10-10"},
},
result: `{"addTweets":{"tweets": [{"id":"tweet1", "text": "abc"}]}}`,
}, {
// Add Tweet with same id and upsert as false should fail.
user: "foo",
role: "admin",
variables: map[string]interface{}{
"upsert": false,
"tweet": common.Tweets{
Id: "tweet1",
Text: "abcdef",
Timestamp: "2020-10-10"},
},
expectedError: true,
}, {
// Add Tweet with same id but user, notfoo should fail authorization.
// As the failing is silent, no error is returned.
user: "notfoo",
role: "admin",
variables: map[string]interface{}{
"upsert": true,
"tweet": common.Tweets{
Id: "tweet1",
Text: "abcdef",
Timestamp: "2020-10-10"},
},
result: `{"addTweets": {"tweets": []} }`,
}, {
// Upsert should succeed.
user: "foo",
role: "admin",
variables: map[string]interface{}{
"upsert": true,
"tweet": common.Tweets{
Id: "tweet1",
Text: "abcdef",
Timestamp: "2020-10-10"},
},
result: `{"addTweets":{"tweets": [{"id": "tweet1", "text":"abcdef"}]}}`,
}}

mutation := `
mutation addTweets($tweet: AddTweetsInput!, $upsert: Boolean){
addTweets(input: [$tweet], upsert: $upsert) {
tweets {
id
text
}
}
}
`

for _, tcase := range testCases {
t.Run(tcase.role+"_"+tcase.user, func(t *testing.T) {
mutationParams := &common.GraphQLParams{
Query: mutation,
Headers: common.GetJWT(t, tcase.user, tcase.role, metaInfo),
Variables: tcase.variables,
}
gqlResponse := mutationParams.ExecuteAsPost(t, common.GraphqlURL)
if tcase.expectedError {
require.Error(t, gqlResponse.Errors)
require.Equal(t, len(gqlResponse.Errors), 1)
require.Contains(t, gqlResponse.Errors[0].Error(),
"GraphQL debug: id already exists for type Tweets")
} else {
common.RequireNoGQLErrors(t, gqlResponse)
require.JSONEq(t, tcase.result, string(gqlResponse.Data))
}
})
}

tweet := common.Tweets{
Id: "tweet1",
}
tweet.DeleteByID(t, "foo", metaInfo)
// Clear the tweet.
}

func TestUpsertWithDeepAuth(t *testing.T) {
testCases := []TestCase{{
// Initial Mutation. Should succeed.
user: "user",
variables: map[string]interface{}{"state": &State{
Code: "UK",
Name: "Uttaranchal",
OwnedBy: "user",
}},
result: `{
"addState":
{"state":
[{
"code": "UK",
"name":"Uttaranchal",
"ownedBy": "user",
"country": null
}]
}
}`,
}, {
// Upsert with wrong user. Should Fail with no error.
user: "wrong user",
variables: map[string]interface{}{"state": &State{
Code: "UK",
Name: "Uttarakhand",
Country: &Country{
Id: "IN",
Name: "India",
OwnedBy: "user",
},
}},
result: `{"addState": { "state": [] } }`,
}, {
// Upsert with correct user. Should succeed and add Country, also update
// country of state.
user: "user",
variables: map[string]interface{}{"state": &State{
Code: "UK",
Name: "Uttarakhand",
Country: &Country{
Id: "IN",
Name: "India",
OwnedBy: "user",
},
}},
result: `{
"addState":
{"state":
[{
"code": "UK",
"name": "Uttarakhand",
"ownedBy": "user",
"country":
{
"name": "India",
"id": "IN",
"ownedBy": "user"
}
}]
}
}`,
}}

query := `
mutation addState($state: AddStateInput!) {
addState(input: [$state], upsert: true) {
state {
code
name
ownedBy
country {
id
name
ownedBy
}
}
}
}
`

for _, tcase := range testCases {
getUserParams := &common.GraphQLParams{
Headers: common.GetJWT(t, tcase.user, tcase.role, metaInfo),
Query: query,
Variables: tcase.variables,
}

gqlResponse := getUserParams.ExecuteAsPost(t, common.GraphqlURL)
common.RequireNoGQLErrors(t, gqlResponse)
require.JSONEq(t, tcase.result, string(gqlResponse.Data))
}

// Clean Up
filter := map[string]interface{}{"id": map[string]interface{}{"eq": "IN"}}
common.DeleteGqlType(t, "Country", filter, 1, nil)
filter = map[string]interface{}{"code": map[string]interface{}{"eq": "UK"}}
common.DeleteGqlType(t, "State", filter, 1, nil)
}
16 changes: 15 additions & 1 deletion graphql/e2e/auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,20 @@ type Column struct {
Tickets []*Ticket `json:"tickets,omitempty"`
}

type Country struct {
Id string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
OwnedBy string `json:"ownedBy,omitempty"`
States []*State `json:"states,omitempty"`
}

type State struct {
Code string `json:"code,omitempty"`
Name string `json:"name,omitempty"`
OwnedBy string `json:"ownedBy,omitempty"`
Country *Country `json:"country,omitempty"`
}

type Project struct {
ProjID string `json:"projID,omitempty"`
Name string `json:"name,omitempty"`
Expand Down Expand Up @@ -539,7 +553,7 @@ func TestAuthRulesWithNullValuesInJWT(t *testing.T) {
}
}
}

func TestAuthOnInterfaceWithRBACPositive(t *testing.T) {
getVehicleParams := &common.GraphQLParams{
Query: `
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 @@ -818,3 +818,31 @@ type Car implements Vehicle {
id: ID!
manufacturer: String!
}

type Country @auth(
add: { rule: """
query($USER: String!) {
queryCountry(filter: { ownedBy: { eq: $USER } }) {
__typename
}
}
"""} ) {
id: String! @id
name: String!
ownedBy: String @search(by: [hash])
states: [State] @hasInverse(field: country)
}

type State @auth(
update: { rule: """
query($USER: String!) {
queryState(filter: { ownedBy: { eq: $USER } }) {
__typename
}
}
"""} ) {
code: String! @id
name: String!
ownedBy: String @search(by: [hash])
country: Country
}
1 change: 1 addition & 0 deletions graphql/e2e/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -832,6 +832,7 @@ func RunAll(t *testing.T) {
t.Run("cyclically linked mutation", cyclicMutation)
t.Run("parallel mutations", parallelMutations)
t.Run("input coercion to list", inputCoerciontoList)
t.Run("Upsert Mutation Tests", upsertMutationTests)

// error tests
t.Run("graphql completion on", graphQLCompletionOn)
Expand Down
88 changes: 88 additions & 0 deletions graphql/e2e/common/mutation.go
Original file line number Diff line number Diff line change
Expand Up @@ -5449,3 +5449,91 @@ func inputCoerciontoList(t *testing.T) {
DeleteGqlType(t, "post1", posts1DeleteFilter, 4, nil)

}

func upsertMutationTests(t *testing.T) {
newCountry := addCountry(t, postExecutor)
// State should get added.
addStateParams := &GraphQLParams{
Query: `mutation addState($xcode: String!, $upsert: Boolean, $name: String!) {
addState(input: [{ xcode: $xcode, name: $name }], upsert: $upsert) {
state {
xcode
name
country {
name
}
}
}
}`,
Variables: map[string]interface{}{
"name": "State1",
"xcode": "S1",
"upsert": true},
}

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

addStateExpected := `{
"addState": {
"state": [{
"xcode": "S1",
"name": "State1",
"country": null
}]
}
}`
testutil.CompareJSON(t, addStateExpected, string(gqlResponse.Data))

// Add Mutation with Upsert: false should fail.
addStateParams.Query = `mutation addState($xcode: String!, $upsert: Boolean, $name: String!, $countryID: ID) {
addState(input: [{ xcode: $xcode, name: $name, country: {id: $countryID }}], upsert: $upsert) {
state {
xcode
name
country {
name
}
}
}
}`
addStateParams.Variables = map[string]interface{}{
"upsert": false,
"name": "State2",
"xcode": "S1",
"countryID": newCountry.ID,
}
gqlResponse = addStateParams.ExecuteAsPost(t, GraphqlURL)
require.NotNil(t, gqlResponse.Errors)
require.Equal(t, "couldn't rewrite mutation addState because failed to rewrite mutation payload "+
"because id S1 already exists for type State", gqlResponse.Errors[0].Error())

// Add Mutation with upsert true should succeed. It should link the state to
// existing country
addStateParams.Variables = map[string]interface{}{
"upsert": true,
"name": "State2",
"xcode": "S1",
"countryID": newCountry.ID,
}
gqlResponse = addStateParams.ExecuteAsPost(t, GraphqlURL)
RequireNoGQLErrors(t, gqlResponse)
addStateExpected = `{
"addState": {
"state": [{
"xcode": "S1",
"name": "State2",
"country": {
"name": "Testland"
}
}]
}
}`
testutil.CompareJSON(t, addStateExpected, string(gqlResponse.Data))

// Clean Up
filter := map[string]interface{}{"id": []string{newCountry.ID}}
deleteCountry(t, filter, 1, nil)
filter = map[string]interface{}{"xcode": map[string]interface{}{"eq": "S1"}}
deleteState(t, filter, 1, nil)
}
6 changes: 3 additions & 3 deletions graphql/e2e/schema/apollo_service_response.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -514,13 +514,13 @@ type Query {
#######################

type Mutation {
addMission(input: [AddMissionInput!]!): AddMissionPayload
addMission(input: [AddMissionInput!]!, upsert: Boolean): AddMissionPayload
updateMission(input: UpdateMissionInput!): UpdateMissionPayload
deleteMission(filter: MissionFilter!): DeleteMissionPayload
addAstronaut(input: [AddAstronautInput!]!): AddAstronautPayload
addAstronaut(input: [AddAstronautInput!]!, upsert: Boolean): AddAstronautPayload
updateAstronaut(input: UpdateAstronautInput!): UpdateAstronautPayload
deleteAstronaut(filter: AstronautFilter!): DeleteAstronautPayload
addCar(input: [AddCarInput!]!): AddCarPayload
addCar(input: [AddCarInput!]!, upsert: Boolean): AddCarPayload
updateCar(input: UpdateCarInput!): UpdateCarPayload
deleteCar(filter: CarFilter!): DeleteCarPayload
}
Expand Down
2 changes: 1 addition & 1 deletion graphql/e2e/schema/generatedSchema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ type Query {
#######################

type Mutation {
addAuthor(input: [AddAuthorInput!]!): AddAuthorPayload
addAuthor(input: [AddAuthorInput!]!, upsert: Boolean): AddAuthorPayload
updateAuthor(input: UpdateAuthorInput!): UpdateAuthorPayload
deleteAuthor(filter: AuthorFilter!): DeleteAuthorPayload
}
Expand Down
Loading