From 283fe639dd4ab39bb5412646331bdf0e283f28f8 Mon Sep 17 00:00:00 2001 From: Harshil Goel <54325286+harshil-goel@users.noreply.github.com> Date: Sat, 14 Oct 2023 01:28:00 +0800 Subject: [PATCH] Added lang support (#8924) Added lang support to GraphQL --- graphql/e2e/common/common.go | 2 + graphql/e2e/common/mutation.go | 64 ++++++++++++ graphql/e2e/common/query.go | 97 +++++++++++++++++++ graphql/e2e/directives/schema.graphql | 12 +++ graphql/e2e/directives/schema_response.json | 25 +++++ graphql/e2e/normal/schema.graphql | 11 ++- graphql/e2e/normal/schema_response.json | 29 ++++-- graphql/resolve/add_mutation_test.yaml | 21 ++++ graphql/resolve/query_test.yaml | 27 ++++++ graphql/resolve/schema.graphql | 5 + graphql/schema/dgraph_schemagen_test.yml | 92 +++++++++++------- graphql/schema/gqlschema.go | 62 +++++++++--- graphql/schema/gqlschema_test.yml | 75 ++++++++++++++ graphql/schema/rules.go | 41 ++++++++ graphql/schema/schemagen.go | 28 ++++-- .../schemagen/input/language-tags.graphql | 27 ++++++ .../schemagen/output/language-tags.graphql | 2 +- 17 files changed, 552 insertions(+), 68 deletions(-) create mode 100644 graphql/schema/testdata/schemagen/input/language-tags.graphql diff --git a/graphql/e2e/common/common.go b/graphql/e2e/common/common.go index a2884bcf4e4..635fe388437 100644 --- a/graphql/e2e/common/common.go +++ b/graphql/e2e/common/common.go @@ -868,6 +868,7 @@ func RunAll(t *testing.T) { t.Run("query id directive with int", idDirectiveWithInt) t.Run("query id directive with int64", idDirectiveWithInt64) t.Run("query filter ID values coercion to List", queryFilterWithIDInputCoercion) + t.Run("query multiple language Fields", queryMultipleLangFields) t.Run("query @id field with interface arg on interface", queryWithIDFieldAndInterfaceArg) // mutation tests @@ -928,6 +929,7 @@ func RunAll(t *testing.T) { t.Run("input coercion to list", inputCoerciontoList) t.Run("multiple external Id's tests", multipleXidsTests) t.Run("Upsert Mutation Tests", upsertMutationTests) + t.Run("Update language tag fields", updateLangTagFields) t.Run("add mutation with @id field and interface arg", addMutationWithIDFieldHavingInterfaceArg) // error tests diff --git a/graphql/e2e/common/mutation.go b/graphql/e2e/common/mutation.go index b468c858f97..07c1675519d 100644 --- a/graphql/e2e/common/mutation.go +++ b/graphql/e2e/common/mutation.go @@ -6038,6 +6038,70 @@ func upsertMutationTests(t *testing.T) { deleteState(t, filter, 2, nil) } +func updateLangTagFields(t *testing.T) { + addPersonParams := &GraphQLParams{ + Query: ` + mutation addPerson($person: [AddPersonInput!]!) { + addPerson(input: $person) { + numUids + } + }`, + } + addPersonParams.Variables = map[string]interface{}{"person": []interface{}{ + map[string]interface{}{ + "name": "Juliet", + "nameHi": "जूलियट", + "nameZh": "朱丽叶", + }, + }, + } + gqlResponse := addPersonParams.ExecuteAsPost(t, GraphqlURL) + RequireNoGQLErrors(t, gqlResponse) + // update Person using language tag field + updatePersonParams := &GraphQLParams{ + Query: ` + mutation updatePerson { + updatePerson( + input: { + filter: { nameHi: { eq: "जूलियट" } } + set: { nameHi: "जूली", nameZh: "朱丽叶" } + } + ) { + numUids + } + }`, + } + gqlResponse = updatePersonParams.ExecuteAsPost(t, GraphqlURL) + RequireNoGQLErrors(t, gqlResponse) + + queryPerson := &GraphQLParams{ + Query: ` + query { + queryPerson(filter: { name: { eq: "Juliet" } }) { + name + nameZh + nameHi + } + }`, + } + gqlResponse = queryPerson.ExecuteAsPost(t, GraphqlURL) + RequireNoGQLErrors(t, gqlResponse) + + queryPersonExpected := ` + { + "queryPerson": [ + { + "name": "Juliet", + "nameZh": "朱丽叶", + "nameHi": "जूली" + } + ] + }` + + testutil.CompareJSON(t, queryPersonExpected, string(gqlResponse.Data)) + DeleteGqlType(t, "Person", map[string]interface{}{}, 1, nil) +} + func addMutationWithIDFieldHavingInterfaceArg(t *testing.T) { // add data successfully for different implementing types tcases := []struct { diff --git a/graphql/e2e/common/query.go b/graphql/e2e/common/query.go index f1704408d1d..5bb4c1136bc 100644 --- a/graphql/e2e/common/query.go +++ b/graphql/e2e/common/query.go @@ -3928,6 +3928,103 @@ func idDirectiveWithInt(t *testing.T) { require.JSONEq(t, expected, string(response.Data)) } +func queryMultipleLangFields(t *testing.T) { + // add three Persons + addPersonParams := &GraphQLParams{ + Query: ` + mutation addPerson($person: [AddPersonInput!]!) { + addPerson(input: $person) { + numUids + } + }`, + Variables: map[string]interface{}{"person": []interface{}{ + map[string]interface{}{ + "name": "Bob", + "professionEn": "writer", + }, + map[string]interface{}{ + "name": "Alice", + "nameHi": "ऐलिस", + "professionEn": "cricketer", + }, + map[string]interface{}{ + "name": "Juliet", + "nameHi": "जूलियट", + "nameZh": "朱丽叶", + "professionEn": "singer", + }, + }}, + } + + gqlResponse := addPersonParams.ExecuteAsPost(t, GraphqlURL) + RequireNoGQLErrors(t, gqlResponse) + + queryPerson := &GraphQLParams{ + Query: ` + query { + queryPerson( + filter: { + or: [ + { name: { eq: "Bob" } } + { nameHi: { eq: "ऐलिस" } } + { nameZh: { eq: "朱丽叶" } } + ] + } + order: { desc: nameHi } + ) { + name + nameZh + nameHi + nameHiZh + nameZhHi + nameHi_Zh_Untag + name_Untag_AnyLang + professionEn + } + }`, + } + gqlResponse = queryPerson.ExecuteAsPost(t, GraphqlURL) + RequireNoGQLErrors(t, gqlResponse) + queryPersonExpected := ` + { + "queryPerson": [ + { + "name":"Juliet", + "nameZh":"朱丽叶", + "nameHi":"जूलियट", + "nameHiZh":"जूलियट", + "nameZhHi":"朱丽叶", + "nameHi_Zh_Untag":"जूलियट", + "name_Untag_AnyLang":"Juliet", + "professionEn":"singer" + }, + { + "name":"Alice", + "nameZh":null, + "nameHi":"ऐलिस", + "nameHiZh":"ऐलिस", + "nameZhHi":"ऐलिस", + "nameHi_Zh_Untag":"ऐलिस", + "name_Untag_AnyLang":"Alice", + "professionEn":"cricketer" + }, + { "name":"Bob", + "nameZh":null, + "nameHi":null, + "nameHiZh":null, + "nameZhHi":null, + "nameHi_Zh_Untag":"Bob", + "name_Untag_AnyLang":"Bob", + "professionEn":"writer" + } + ] + }` + + JSONEqGraphQL(t, queryPersonExpected, string(gqlResponse.Data)) + // Cleanup + DeleteGqlType(t, "Person", map[string]interface{}{}, 3, nil) +} + func queryWithIDFieldAndInterfaceArg(t *testing.T) { // add library member addLibraryMemberParams := &GraphQLParams{ diff --git a/graphql/e2e/directives/schema.graphql b/graphql/e2e/directives/schema.graphql index 01529ca4e6c..25b6342feb3 100644 --- a/graphql/e2e/directives/schema.graphql +++ b/graphql/e2e/directives/schema.graphql @@ -211,6 +211,18 @@ type Person1 { friends: [Person1] @hasInverse(field: friends) } +type Person { + id: ID! + name: String! @search(by: [hash]) + nameHi: String @dgraph(pred:"Person.name@hi") @search(by: [hash]) + nameZh: String @dgraph(pred:"Person.name@zh") @search(by: [hash]) + nameHiZh: String @dgraph(pred:"Person.name@hi:zh") + nameZhHi: String @dgraph(pred:"Person.name@zh:hi") + nameHi_Zh_Untag: String @dgraph(pred:"Person.name@hi:zh:.") + name_Untag_AnyLang: String @dgraph(pred:"Person.name@.") @search(by: [hash]) + professionEn: String @dgraph(pred:"Person.profession@en") +} + # union testing - start enum AnimalCategory { Fish diff --git a/graphql/e2e/directives/schema_response.json b/graphql/e2e/directives/schema_response.json index edda361e792..683460f7da3 100644 --- a/graphql/e2e/directives/schema_response.json +++ b/graphql/e2e/directives/schema_response.json @@ -178,6 +178,20 @@ "predicate": "Employer.worker", "type": "uid" }, + { + "lang": true, + "predicate": "Person.profession", + "type": "string" + }, + { + "index": true, + "lang": true, + "predicate": "Person.name", + "tokenizer": [ + "hash" + ], + "type": "string" + }, { "predicate": "Book.chapters", "type": "uid", @@ -1003,6 +1017,17 @@ ], "name": "Home" }, + { + "fields": [ + { + "name": "Person.name" + }, + { + "name": "Person.profession" + } + ], + "name": "Person" + }, { "fields": [ { diff --git a/graphql/e2e/normal/schema.graphql b/graphql/e2e/normal/schema.graphql index 1876dd77f6c..78d0011ca30 100644 --- a/graphql/e2e/normal/schema.graphql +++ b/graphql/e2e/normal/schema.graphql @@ -163,8 +163,15 @@ type Student implements People { } type Person @withSubscription{ - id: ID! - name: String! + id: ID! + name: String! @search(by: [hash]) + nameHi: String @dgraph(pred:"Person.name@hi") @search(by: [hash]) + nameZh: String @dgraph(pred:"Person.name@zh") @search(by: [hash]) + nameHiZh: String @dgraph(pred:"Person.name@hi:zh") + nameZhHi: String @dgraph(pred:"Person.name@zh:hi") + nameHi_Zh_Untag: String @dgraph(pred:"Person.name@hi:zh:.") + name_Untag_AnyLang: String @dgraph(pred:"Person.name@.") @search(by: [hash]) + professionEn: String @dgraph(pred:"Person.profession@en") } """ diff --git a/graphql/e2e/normal/schema_response.json b/graphql/e2e/normal/schema_response.json index defb3113383..d1befcdc844 100644 --- a/graphql/e2e/normal/schema_response.json +++ b/graphql/e2e/normal/schema_response.json @@ -498,7 +498,17 @@ "upsert": true }, { + "lang": true, + "predicate": "Person.profession", + "type": "string" + }, + { + "index": true, + "lang": true, "predicate": "Person.name", + "tokenizer": [ + "hash" + ], "type": "string" }, { @@ -1191,14 +1201,6 @@ ], "name": "People" }, - { - "fields": [ - { - "name": "Person.name" - } - ], - "name": "Person" - }, { "fields": [ { @@ -1619,6 +1621,17 @@ } ], "name": "LibraryMember" + }, + { + "fields": [ + { + "name": "Person.name" + }, + { + "name": "Person.profession" + } + ], + "name": "Person" } ] } diff --git a/graphql/resolve/add_mutation_test.yaml b/graphql/resolve/add_mutation_test.yaml index 68959e4ee28..1f85a553e98 100644 --- a/graphql/resolve/add_mutation_test.yaml +++ b/graphql/resolve/add_mutation_test.yaml @@ -5183,3 +5183,24 @@ "dgraph.type":["Friend1"], "uid":"_:Friend1_1" } + +- + name: "Add mutation with language tag fields" + gqlmutation: | + mutation { + addPerson(input: { name: "Alice", nameHi: "ऐलिस",nameZh: "爱丽丝"}) { + person { + name + nameZh + nameHi + } + } + } + dgmutations: + - setjson: | + { "Person.name":"Alice", + "Person.name@hi":"ऐलिस", + "Person.name@zh":"爱丽丝", + "dgraph.type": ["Person"], + "uid": "_:Person_1" + } diff --git a/graphql/resolve/query_test.yaml b/graphql/resolve/query_test.yaml index 20f983722db..c281bc5932a 100644 --- a/graphql/resolve/query_test.yaml +++ b/graphql/resolve/query_test.yaml @@ -3303,6 +3303,33 @@ } } +- + name: "query language tag fields with filter and order" + gqlquery: | + query { + queryPerson(filter:{or:[{name:{eq:"Alice"}},{nameHi:{eq:"ऐलिस"}},{nameZh:{eq:"爱丽丝"}},{name_Untag_AnyLang:{eq:"Alice"}}]}, order: { asc: nameHi }) + { + name + nameZh + nameHi + nameHiZh + nameHi_Zh_Untag + name_Untag_AnyLang + } + } + dgquery: |- + query { + queryPerson(func: type(Person), orderasc: Person.name@hi) @filter((eq(Person.name, "Alice") OR eq(Person.name@hi, "ऐलिस") OR eq(Person.name@zh, "爱丽丝") OR eq(Person.name@., "Alice"))) { + Person.name : Person.name + Person.nameZh : Person.name@zh + Person.nameHi : Person.name@hi + Person.nameHiZh : Person.name@hi:zh + Person.nameHi_Zh_Untag : Person.name@hi:zh:. + Person.name_Untag_AnyLang : Person.name@. + dgraph.uid : uid + } + } + - name: "get query on interface with @id field having interface argument set" gqlquery: | query { diff --git a/graphql/resolve/schema.graphql b/graphql/resolve/schema.graphql index cc5ffa25db9..fe94fd0cbef 100644 --- a/graphql/resolve/schema.graphql +++ b/graphql/resolve/schema.graphql @@ -310,6 +310,11 @@ type ThingTwo implements Thing { type Person { id: ID! name: String @search(by: [hash]) + nameHi: String @dgraph(pred:"Person.name@hi") @search(by: [hash]) + nameZh: String @dgraph(pred:"Person.name@zh") @search(by: [hash]) + nameHiZh: String @dgraph(pred:"Person.name@hi:zh") + nameHi_Zh_Untag: String @dgraph(pred:"Person.name@hi:zh:.") + name_Untag_AnyLang: String @dgraph(pred:"Person.name@.") @search(by: [hash]) friends: [Person] @hasInverse(field: friends) } diff --git a/graphql/schema/dgraph_schemagen_test.yml b/graphql/schema/dgraph_schemagen_test.yml index 4e9e703b4ad..32bb64b3b58 100644 --- a/graphql/schema/dgraph_schemagen_test.yml +++ b/graphql/schema/dgraph_schemagen_test.yml @@ -1,6 +1,5 @@ schemas: - - - name: "Object data type" + - name: "Object data type" input: | type A { id: ID! @@ -20,8 +19,7 @@ schemas: } P.q: uid . - - - name: "Scalar list" + - name: "Scalar list" input: | type X { id: ID! @@ -33,8 +31,7 @@ schemas: } X.names: [string] . - - - name: "Password type" + - name: "Password type" input: | type X @secret(field: "pwd"){ id: ID! @@ -49,8 +46,7 @@ schemas: X.pwd: password . - - - name: "Object list" + - name: "Object list" input: | type X { p: [P!]! @@ -69,8 +65,7 @@ schemas: } P.name: string . - - - name: "Scalar types" + - name: "Scalar types" input: | type X { p: Int @@ -112,8 +107,7 @@ schemas: X.v: int . X.vList: [int] . - - - name: "enum - always gets an index" + - name: "enum - always gets an index" input: | type X { e: E @@ -129,8 +123,7 @@ schemas: X.f: [string] @index(hash) . - - - name: "Search indexes are correct" + - name: "Search indexes are correct" input: | type X { i1: Int @search @@ -226,8 +219,7 @@ schemas: X.e6: string @index(hash, trigram) . X.e7: string @index(exact, trigram) . - - - name: "interface and types interact properly" + - name: "interface and types interact properly" input: | interface A { id: ID! @@ -255,8 +247,7 @@ schemas: } C.dob: dateTime . - - - name: "interface using other interface generate type in dgraph" + - name: "interface using other interface generate type in dgraph" input: | interface A { id: ID! @@ -346,8 +337,7 @@ schemas: data: [uid] . V.f6: uid . - - - name: "Schema with @dgraph directive." + - name: "Schema with @dgraph directive." input: | type A @dgraph(type: "dgraph.type.A") { id: ID! @@ -434,8 +424,45 @@ schemas: dgraph.pList: [int] . f: float . - - - name: "Field with @id directive but no search directive gets hash index." + - name: "Schema with multiple language tags, indexes on language tag fields got merged on language untagged field" + input: | + interface Node { + f1: String + } + type Person implements Node { + f1Hi: String @dgraph(pred: "Node.f1@hi") + f2: String @dgraph(pred: "T.f@no") + f3: String @dgraph(pred: "f3@en") + name: String! @id + nameHi: String @dgraph(pred: "Person.name@hi") @search(by: [term, exact]) + nameEn: String @dgraph(pred: "Person.name@en") @search(by: [regexp]) + nameHiEn: String @dgraph(pred: "Person.name@hi:en") + nameHi_En_Untag: String @dgraph(pred: "Person.name@hi:en:.") + name_Untag_AnyLang: String @dgraph(pred: "Person.name@.") + address: String @search(by: [fulltext]) + addressHi: String @dgraph(pred: "Person.address@hi") + professionEn: String @dgraph(pred: "Person.profession@en") + } + output: | + type Node { + Node.f1 + } + Node.f1: string @lang . + type Person { + Node.f1 + T.f + f3 + Person.name + Person.address + Person.profession + } + T.f: string @lang . + f3: string @lang . + Person.name: string @index(exact, hash, term, trigram) @lang @upsert . + Person.address: string @index(fulltext) @lang . + Person.profession: string @lang . + + - name: "Field with @id directive but no search directive gets hash index." input: | interface A { id: String! @id @@ -454,8 +481,7 @@ schemas: } B.correct: bool @index(bool) . - - - name: "Field with @id directive gets hash index." + - name: "Field with @id directive gets hash index." input: | interface A { id: String! @id @search(by: [trigram]) @@ -474,8 +500,7 @@ schemas: } B.correct: bool @index(bool) . - - - name: "Field with @id directive and a hash arg in search directive generates correct schema." + - name: "Field with @id directive and a hash arg in search directive generates correct schema." input: | interface A { id: String! @id @search(by: [hash, term]) @@ -513,8 +538,7 @@ schemas: } B.correct: bool @index(bool) . - - - name: "Field with reverse predicate in dgraph directive adds @reverse to predicate." + - name: "Field with reverse predicate in dgraph directive adds @reverse to predicate." input: | type Movie { director: [Person] @dgraph(pred: "~directed.movies") @@ -530,8 +554,7 @@ schemas: } directed.movies: [uid] @reverse . - - - name: "Field with reverse predicate in dgraph directive where actual predicate comes first." + - name: "Field with reverse predicate in dgraph directive where actual predicate comes first." input: | type Person { directed: [Movie] @dgraph(pred: "directed.movies") @@ -547,8 +570,7 @@ schemas: type Movie { } - - - name: "deprecated fields get included in Dgraph schema" + - name: "deprecated fields get included in Dgraph schema" input: | type A { id: ID! @@ -653,8 +675,7 @@ schemas: post: string @index(exact, term) . <公司>: string @index(exact, term) . - - - name: "custom query and mutation shouldn't be part of Dgraph schema" + - name: "custom query and mutation shouldn't be part of Dgraph schema" input: | type User @remote { id: ID! @@ -675,8 +696,7 @@ schemas: }) } - - - name: "custom field shouldn't be part of dgraph schema" + - name: "custom field shouldn't be part of dgraph schema" input: | type User { id: ID! diff --git a/graphql/schema/gqlschema.go b/graphql/schema/gqlschema.go index c2398514260..185611e50be 100644 --- a/graphql/schema/gqlschema.go +++ b/graphql/schema/gqlschema.go @@ -872,7 +872,6 @@ func postGQLValidation(schema *ast.Schema, definitions []string, } } } - errs = append(errs, applySchemaValidations(schema, definitions)...) return errs @@ -1206,7 +1205,7 @@ func addUnionMemberTypeEnum(schema *ast.Schema, defn *ast.Definition) { // it should be present in the addTypeInput as it should not be generated automatically by dgraph // but determined by the value of field in the GraphQL service where the type is defined. func addInputType(schema *ast.Schema, defn *ast.Definition, providesTypeMap map[string]bool) { - field := getFieldsWithoutIDType(schema, defn, providesTypeMap) + field := getFieldsWithoutIDType(schema, defn, providesTypeMap, true) if hasExtends(defn) { idField := getIDField(defn, providesTypeMap) field = append(idField, field...) @@ -1229,7 +1228,8 @@ func addReferenceType(schema *ast.Schema, defn *ast.Definition, providesTypeMap } flds = append(getIDField(defn, providesTypeMap), getXIDField(defn, providesTypeMap)...) } else { - flds = append(getIDField(defn, providesTypeMap), getFieldsWithoutIDType(schema, defn, providesTypeMap)...) + flds = append(getIDField(defn, providesTypeMap), + getFieldsWithoutIDType(schema, defn, providesTypeMap, true)...) } if len(flds) == 1 && (hasID(defn) || hasXID(defn)) { @@ -1331,7 +1331,7 @@ func addFieldFilters( for _, fld := range defn.Fields { // Filtering and ordering for fields with @custom/@lambda directive is handled by the remote // endpoint. - if hasCustomOrLambda(fld) { + if hasCustomOrLambda(fld) || isMultiLangField(fld, false) { continue } @@ -1419,7 +1419,7 @@ func addTypeHasFilter(schema *ast.Schema, defn *ast.Definition, providesTypeMap } for _, fld := range defn.Fields { - if isID(fld) || hasCustomOrLambda(fld) { + if isID(fld) || hasCustomOrLambda(fld) || isMultiLangField(fld, false) { continue } // Ignore Fields with @external directives also excluding those which are present @@ -1584,7 +1584,7 @@ func addFilterType(schema *ast.Schema, defn *ast.Definition, providesTypeMap map } // Has filter makes sense only if there is atleast one non ID field in the defn - if len(getFieldsWithoutIDType(schema, defn, providesTypeMap)) > 0 { + if len(getFieldsWithoutIDType(schema, defn, providesTypeMap, false)) > 0 { filter.Fields = append(filter.Fields, &ast.FieldDefinition{Name: "has", Type: &ast.Type{Elem: &ast.Type{NamedType: defn.Name + "HasFilter"}}}, ) @@ -1616,7 +1616,8 @@ func addFilterType(schema *ast.Schema, defn *ast.Definition, providesTypeMap map func hasFilterable(defn *ast.Definition) bool { return fieldAny(defn.Fields, func(fld *ast.FieldDefinition) bool { - return len(getSearchArgs(fld)) != 0 || isID(fld) || !hasCustomOrLambda(fld) + return len(getSearchArgs(fld)) != 0 || isID(fld) || + !hasCustomOrLambda(fld) || !isMultiLangField(fld, false) }) } @@ -1639,13 +1640,17 @@ func hasOrderables(defn *ast.Definition, providesTypeMap map[string]bool) bool { }) } -func isOrderable(fld *ast.FieldDefinition, defn *ast.Definition, providesTypeMap map[string]bool) bool { +func isOrderable(fld *ast.FieldDefinition, defn *ast.Definition, + providesTypeMap map[string]bool) bool { // lists can't be ordered and NamedType will be empty for lists, // so it will return false for list fields // External field can't be ordered except when it is a @key field or // the field is an argument in `@provides` directive. + // Multiple language fields(i.e. of type name@hi:en) are not orderable + // We allow to generate aggregate fields for multi language fields if !hasExternal(fld) { - return orderable[fld.Type.NamedType] && !hasCustomOrLambda(fld) + return orderable[fld.Type.NamedType] && !hasCustomOrLambda(fld) && + !isMultiLangField(fld, false) } return isKeyField(fld, defn) || providesTypeMap[fld.Name] } @@ -1905,7 +1910,7 @@ func addAggregationResultType(schema *ast.Schema, defn *ast.Definition, provides } // Adds titleMax, titleMin fields for a field of name title. - if isOrderable(fld, defn, providesTypeMap) { + if isOrderable(fld, defn, providesTypeMap) || isMultiLangField(fld, false) { minField := &ast.FieldDefinition{ Name: fld.Name + "Min", Type: aggregateFieldType, @@ -2237,7 +2242,7 @@ func getNonIDFields(schema *ast.Schema, defn *ast.Definition, providesTypeMap ma // Ignore Fields with @external directives also as they shouldn't be present // in the Patch Type also. If the field is an argument to `@provides` directive - // then it should be presnt. + // then it should be present. if externalAndNonKeyField(fld, defn, providesTypeMap) { continue } @@ -2246,7 +2251,12 @@ func getNonIDFields(schema *ast.Schema, defn *ast.Definition, providesTypeMap ma if hasCustomOrLambda(fld) { continue } - + // We don't include fields in update patch, which corresponds to multiple language tags in dgraph + // Example, nameHi_En: String @dgraph(pred:"Person.name@hi:en") + // We don't add above field in update patch because it corresponds to multiple languages + if isMultiLangField(fld, true) { + continue + } // Remove edges which have a reverse predicate as they should only be updated through their // forward edge. fname := fieldName(fld, defn.Name) @@ -2275,7 +2285,8 @@ func getNonIDFields(schema *ast.Schema, defn *ast.Definition, providesTypeMap ma return append(fldList, pd) } -func getFieldsWithoutIDType(schema *ast.Schema, defn *ast.Definition, providesTypeMap map[string]bool) ast.FieldList { +func getFieldsWithoutIDType(schema *ast.Schema, defn *ast.Definition, + providesTypeMap map[string]bool, isAddingInput bool) ast.FieldList { fldList := make([]*ast.FieldDefinition, 0) for _, fld := range defn.Fields { if isIDField(defn, fld) { @@ -2293,7 +2304,10 @@ func getFieldsWithoutIDType(schema *ast.Schema, defn *ast.Definition, providesTy if hasCustomOrLambda(fld) { continue } - + // see the comment in getNonIDFields as well. + if isMultiLangField(fld, true) && isAddingInput { + continue + } // Remove edges which have a reverse predicate as they should only be updated through their // forward edge. fname := fieldName(fld, defn.Name) @@ -2316,6 +2330,26 @@ func getFieldsWithoutIDType(schema *ast.Schema, defn *ast.Definition, providesTy return append(fldList, pd) } +// This function check if given gql field has multiple language tags +func isMultiLangField(fld *ast.FieldDefinition, isMutationInput bool) bool { + dgDirective := fld.Directives.ForName(dgraphDirective) + if dgDirective == nil { + return false + } + pred := dgDirective.Arguments.ForName("pred") + if pred == nil { + return false + } + if strings.Contains(pred.Value.Raw, "@") { + langs := strings.Split(pred.Value.Raw, "@")[1] + if isMutationInput { + return strings.Contains(langs, ":") || langs == "." + } + return strings.Contains(langs, ":") + } + return false +} + func getIDField(defn *ast.Definition, providesTypeMap map[string]bool) ast.FieldList { fldList := make([]*ast.FieldDefinition, 0) for _, fld := range defn.Fields { diff --git a/graphql/schema/gqlschema_test.yml b/graphql/schema/gqlschema_test.yml index a7458e1d616..b31b6e86705 100644 --- a/graphql/schema/gqlschema_test.yml +++ b/graphql/schema/gqlschema_test.yml @@ -2889,6 +2889,61 @@ invalid_schemas: { "message": "Type TwitterUser; @lambdaOnMutate directive not allowed along with @remote directive.", "locations": [{"line": 1, "column": 27}]} ] + - name: "language tag field can't contain more than on @" + input: | + type Person { + name: String! + nameHi: String @dgraph(pred:"Person.name@hi@en") + } + errlist: [ + { "message": "Type Person; Field nameHi: multiple language tag not supported", + "locations": [ { "line": 3, "column": 19 } ] }, + ] + + - name: "language tag field should be of String type" + input: | + type Person { + name: String! + nameHi: Int @dgraph(pred:"Person.name@hi") + } + errlist: [ + { "message": "Type Person; Field nameHi: Expected type `String` for language tag field but got `Int`", + "locations": [ { "line": 3, "column": 3 } ] }, + ] + + - name: "@id directive not supported on language tag field" + input: | + type Person { + name: String! + nameHi: String! @dgraph(pred:"Person.name@hi") @id + } + errlist: [ + { "message": "Type Person; Field nameHi: @id directive not supported on language tag fields", + "locations": [ { "line": 3, "column": 51 } ] }, + ] + + - name: "@search directive not supported on multiple language tag field" + input: | + type Person { + name: String! + nameHiEn: String! @dgraph(pred:"Person.name@hi:en") @search(by: [exact]) + } + errlist: [ + { "message": "Type Person; Field nameHiEn: @search directive not applicable on language tag + field with multiple languages", + "locations": [ { "line": 3, "column": 56 } ] }, + ] + + - name: "unsupported `*` language tag in graphql" + input: | + type Person { + name: String! + nameHi: String @dgraph(pred:"Person.name@*") + } + errlist: [ + { "message": "Type Person; Field nameHi: `*` language tag not supported in GraphQL", + "locations": [ { "line": 3, "column": 19 } ] }, + ] - name: "@id field can't have interface argument when it's defined inside a type" input: | type Person { @@ -3392,6 +3447,26 @@ valid_schemas: f4: [X] @dgraph(pred: "link") } + - name: "valid schema with multiple language tag fields" + input: | + interface Node { + f1: String + } + type Person implements Node { + f1Hi: String @dgraph(pred: "Node.f1@hi") + f2: String @dgraph(pred: "T.f@no") + name: String! @id + f3: String @dgraph(pred: "f3@en") + nameHi: String @dgraph(pred: "Person.name@hi") @search(by: [term, exact]) + nameEn: String @dgraph(pred: "Person.name@en") @search(by: [regexp]) + nameHiEn: String @dgraph(pred: "Person.name@hi:en") + nameHi_En_Untag: String @dgraph(pred: "Person.name@hi:en:.") + name_Untag_AnyLang: String @dgraph(pred: "Person.name@.") + address: String @search(by: [fulltext]) + addressHi: String @dgraph(pred: "Person.address@hi") + professionEn: String @dgraph(pred: "Person.profession@en") + } + - name: "valid schema with @id directive having interface argument in interface" input: | interface Member { diff --git a/graphql/schema/rules.go b/graphql/schema/rules.go index 9b2e6d86aa2..1647911ba65 100644 --- a/graphql/schema/rules.go +++ b/graphql/schema/rules.go @@ -1226,6 +1226,47 @@ func dgraphDirectiveValidation(sch *ast.Schema, typ *ast.Definition, field *ast. return errs } } + + if strings.Contains(predArg.Value.String(), "@") { + if field.Type.Name() != "String" { + errs = append(errs, gqlerror.ErrorPosf(field.Position, + "Type %s; Field %s: Expected type `String`"+ + " for language tag field but got `%s`", typ.Name, field.Name, field.Type.Name())) + return errs + } + if field.Directives.ForName(idDirective) != nil { + errs = append(errs, gqlerror.ErrorPosf(field.Directives.ForName(idDirective).Position, + "Type %s; Field %s: @id "+ + "directive not supported on language tag fields", typ.Name, field.Name)) + return errs + } + + if field.Directives.ForName(searchDirective) != nil && isMultiLangField(field, false) { + errs = append(errs, gqlerror.ErrorPosf(field.Directives.ForName(searchDirective).Position, + "Type %s; Field %s: @search directive not applicable"+ + " on language tag field with multiple languages", typ.Name, field.Name)) + return errs + } + + allTags := strings.Split(predArg.Value.Raw, "@") + tags := allTags[1] + if tags == "*" { + errs = append(errs, gqlerror.ErrorPosf(dir.Position, "Type %s; Field %s: `*` language tag not"+ + " supported in GraphQL", typ.Name, field.Name)) + return errs + } + if tags == "" { + errs = append(errs, gqlerror.ErrorPosf(dir.Position, "Type %s; Field %s: empty language"+ + " tag not supported", typ.Name, field.Name)) + return errs + } + if len(allTags) > 2 { + errs = append(errs, gqlerror.ErrorPosf(dir.Position, "Type %s; Field %s: multiple language"+ + " tag not supported", typ.Name, field.Name)) + return errs + } + + } return nil } diff --git a/graphql/schema/schemagen.go b/graphql/schema/schemagen.go index 730583afd1c..76b9a30fd96 100644 --- a/graphql/schema/schemagen.go +++ b/graphql/schema/schemagen.go @@ -524,6 +524,7 @@ func genDgSchema(gqlSch *ast.Schema, definitions []string, indexes map[string]bool upsert string reverse string + lang bool } type field struct { @@ -541,7 +542,7 @@ func genDgSchema(gqlSch *ast.Schema, definitions []string, dgTypes := make([]dgType, 0, len(definitions)) dgPreds := make(map[string]dgPred) - getUpdatedPred := func(fname, typStr, upsertStr string, indexes []string) dgPred { + getUpdatedPred := func(fname, typStr, upsertStr string, indexes []string, lang bool) dgPred { pred, ok := dgPreds[fname] if !ok { pred = dgPred{ @@ -553,6 +554,7 @@ func genDgSchema(gqlSch *ast.Schema, definitions []string, for _, index := range indexes { pred.indexes[index] = true } + pred.lang = lang return pred } @@ -610,7 +612,7 @@ func genDgSchema(gqlSch *ast.Schema, definitions []string, indexes = append(indexes, supportedSearches[defaultSearches[f.Type. Name()]].dgIndex) } - dgPreds[fname] = getUpdatedPred(fname, typStr, "", indexes) + dgPreds[fname] = getUpdatedPred(fname, typStr, "", indexes, false) } else { typStr = fmt.Sprintf("%suid%s", prefix, suffix) } @@ -668,7 +670,13 @@ func genDgSchema(gqlSch *ast.Schema, definitions []string, } if parentInt == nil { - dgPreds[fname] = getUpdatedPred(fname, typStr, upsertStr, indexes) + // if field name contains @ then it is a language tagged field. + isLang := false + if strings.Contains(fname, "@") { + fname = strings.Split(fname, "@")[0] + isLang = true + } + dgPreds[fname] = getUpdatedPred(fname, typStr, upsertStr, indexes, isLang) } typ.fields = append(typ.fields, field{fname, parentInt != nil}) case ast.Enum: @@ -683,7 +691,7 @@ func genDgSchema(gqlSch *ast.Schema, definitions []string, } } if parentInt == nil { - dgPreds[fname] = getUpdatedPred(fname, typStr, "", indexes) + dgPreds[fname] = getUpdatedPred(fname, typStr, "", indexes, false) } typ.fields = append(typ.fields, field{fname, parentInt != nil}) } @@ -707,16 +715,20 @@ func genDgSchema(gqlSch *ast.Schema, definitions []string, predWritten := make(map[string]bool, len(dgPreds)) for _, typ := range dgTypes { + // fieldAdded keeps track of whether a field has been added to typeDef + fieldAdded := make(map[string]bool, len(typ.fields)) var typeDef, preds strings.Builder fmt.Fprintf(&typeDef, "type %s {\n", typ.name) for _, fld := range typ.fields { f, ok := dgPreds[fld.name] - if !ok { + if !ok || fieldAdded[fld.name] { continue } fmt.Fprintf(&typeDef, " %s\n", fld.name) + fieldAdded[fld.name] = true if !fld.inherited && !predWritten[fld.name] { indexStr := "" + langStr := "" if len(f.indexes) > 0 { indexes := make([]string, 0) for index := range f.indexes { @@ -725,8 +737,10 @@ func genDgSchema(gqlSch *ast.Schema, definitions []string, sort.Strings(indexes) indexStr = fmt.Sprintf(" @index(%s)", strings.Join(indexes, ", ")) } - fmt.Fprintf(&preds, "%s: %s%s %s%s.\n", fld.name, f.typ, indexStr, f.upsert, - f.reverse) + if f.lang { + langStr = " @lang" + } + fmt.Fprintf(&preds, "%s: %s%s%s %s%s.\n", fld.name, f.typ, indexStr, langStr, f.upsert, f.reverse) predWritten[fld.name] = true } } diff --git a/graphql/schema/testdata/schemagen/input/language-tags.graphql b/graphql/schema/testdata/schemagen/input/language-tags.graphql new file mode 100644 index 00000000000..97257697b46 --- /dev/null +++ b/graphql/schema/testdata/schemagen/input/language-tags.graphql @@ -0,0 +1,27 @@ +interface Node { + f1: String +} + +type Person implements Node { + # untagged field for the below is defined in other type + f1Hi: String @dgraph(pred: "Node.f1@hi") + # type T doesn't exist for untagged field corresponding to below field + # it could have been an already existing type in user's DQL internally + f2: String @dgraph(pred: "T.f@no") + # no typename.pred syntax, directly pred is given + f3: String @dgraph(pred: "f3@en") + name: String! @id + # We can have exact index on language tagged field while having hash index on language untagged field + nameHi: String @dgraph(pred: "Person.name@hi") @search(by: [term, exact]) + nameEn: String @dgraph(pred: "Person.name@en") @search(by: [regexp]) + # Below Fields nameHiEn,nameHi_En_Untag won't be added to update/add mutation/ref type + # and also to filters, order as they corresponds to multiple language tags + nameHiEn: String @dgraph(pred: "Person.name@hi:en") + nameHi_En_Untag: String @dgraph(pred: "Person.name@hi:en:.") + name_Untag_AnyLang: String @dgraph(pred: "Person.name@.") + address: String @search(by: [fulltext]) + addressHi: String @dgraph(pred: "Person.address@hi") + # We can have language tag field without corresponding language untagged field + # We will generate the correct DQL schema + professionEn: String @dgraph(pred: "Person.profession@en") +} diff --git a/graphql/schema/testdata/schemagen/output/language-tags.graphql b/graphql/schema/testdata/schemagen/output/language-tags.graphql index e217582cce7..09bbce78f52 100755 --- a/graphql/schema/testdata/schemagen/output/language-tags.graphql +++ b/graphql/schema/testdata/schemagen/output/language-tags.graphql @@ -34,7 +34,7 @@ scalar Int64 """ The DateTime scalar type represents date and time as a string in RFC3339 format. -For example: "1985-04-12T23:20:50.52Z" represents 20 minutes and 50.52 seconds after the 23rd hour of April 12th, 1985 in UTC. +For example: "1985-04-12T23:20:50.52Z" represents 20 mins 50.52 secs after the 23rd hour of Apr 12th 1985 in UTC. """ scalar DateTime