diff --git a/graphql/e2e/common/mutation.go b/graphql/e2e/common/mutation.go index 88a32dd2e0b..f5fd816f91d 100644 --- a/graphql/e2e/common/mutation.go +++ b/graphql/e2e/common/mutation.go @@ -4981,11 +4981,12 @@ func addMutationWithDeepExtendedTypeObjects(t *testing.T) { varMap1 := map[string]interface{}{ "missionId": "Mission1", "astronautId": "Astronaut1", + "name": "Guss Garissom", "des": "Apollo1", } addMissionParams := &GraphQLParams{ - Query: `mutation addMission($missionId: String!, $astronautId: ID!, $des: String!) { - addMission(input: [{id: $missionId, designation: $des, crew: [{id: $astronautId}]}]) { + Query: `mutation addMission($missionId: String!, $astronautId: ID!, $name: String!, $des: String!) { + addMission(input: [{id: $missionId, designation: $des, crew: [{id: $astronautId, name: $name}]}]) { mission{ id crew { @@ -5027,6 +5028,7 @@ func addMutationWithDeepExtendedTypeObjects(t *testing.T) { varMap2 := map[string]interface{}{ "missionId": "Mission2", "astronautId": "Astronaut1", + "name": "Gus Garrisom", "des": "Apollo2", } addMissionParams.Variables = varMap2 @@ -5067,10 +5069,11 @@ func addMutationWithDeepExtendedTypeObjects(t *testing.T) { 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"}]}]) { + Query: `mutation addAstronaut($id1: ID!, $name1: String!, $missionId1: String!, $id2: ID!, $name2: String!, $missionId2: String! ) { + addAstronaut(input: [{id: $id1, name: $name1, missions: [{id: $missionId1, designation: "Apollo1"}]}, {id: $id2, name: $name2, missions: [{id: $missionId2, designation: "Apollo11"}]}]) { astronaut(order: {asc: id}){ id + name missions { id designation @@ -5080,8 +5083,10 @@ func addMutationOnExtendedTypeWithIDasKeyField(t *testing.T) { }`, Variables: map[string]interface{}{ "id1": "Astronaut1", + "name1": "Gus Grissom", "missionId1": "Mission1", "id2": "Astronaut2", + "name2": "Neil Armstrong", "missionId2": "Mission2", }, } @@ -5091,27 +5096,29 @@ func addMutationOnExtendedTypeWithIDasKeyField(t *testing.T) { expectedJSON := `{ "addAstronaut": { - "astronaut": [ - { - "id": "Astronaut1", - "missions": [ - { - "id": "Mission1", - "designation": "Apollo1" - } - ] - }, - { - "id": "Astronaut2", - "missions": [ - { - "id": "Mission2", - "designation": "Apollo2" - } - ] - } - ] - } + "astronaut": [ + { + "id": "Astronaut1", + "name": "Gus Grissom", + "missions": [ + { + "id": "Mission1", + "designation": "Apollo1" + } + ] + }, + { + "id": "Astronaut2", + "name": "Neil Armstrong", + "missions": [ + { + "id": "Mission2", + "designation": "Apollo11" + } + ] + } + ] + } }` testutil.CompareJSON(t, expectedJSON, string(gqlResponse.Data)) diff --git a/graphql/e2e/directives/schema.graphql b/graphql/e2e/directives/schema.graphql index 1360a294b75..7f06c40c698 100644 --- a/graphql/e2e/directives/schema.graphql +++ b/graphql/e2e/directives/schema.graphql @@ -309,7 +309,7 @@ type Section { type Mission @key(fields: "id") { id: String! @id - crew: [Astronaut] @hasInverse(field: missions) + crew: [Astronaut] @provides(fields: "name") @hasInverse(field: missions) spaceShip: [SpaceShip] designation: String! startDate: String @@ -318,6 +318,7 @@ type Mission @key(fields: "id") { type Astronaut @key(fields: "id") @extends { id: ID! @external + name: String @external missions: [Mission] } diff --git a/graphql/e2e/directives/schema_response.json b/graphql/e2e/directives/schema_response.json index 42bc41ac464..94f3e648411 100644 --- a/graphql/e2e/directives/schema_response.json +++ b/graphql/e2e/directives/schema_response.json @@ -67,6 +67,10 @@ "predicate": "author1.posts", "type": "uid" }, + { + "predicate": "Astronaut.name", + "type": "string" + }, { "predicate": "Astronaut.missions", "type": "uid", diff --git a/graphql/e2e/normal/schema.graphql b/graphql/e2e/normal/schema.graphql index 6d86dafccaa..46b40225a99 100644 --- a/graphql/e2e/normal/schema.graphql +++ b/graphql/e2e/normal/schema.graphql @@ -327,7 +327,7 @@ type Employer { type Mission @key(fields: "id") { id: String! @id - crew: [Astronaut] @hasInverse(field: missions) + crew: [Astronaut] @provides(fields: "name") @hasInverse(field: missions) spaceShip: [SpaceShip] designation: String! startDate: String @@ -336,6 +336,7 @@ type Mission @key(fields: "id") { type Astronaut @key(fields: "id") @extends { id: ID! @external + name: String! @external missions: [Mission] } diff --git a/graphql/e2e/normal/schema_response.json b/graphql/e2e/normal/schema_response.json index aff312cd174..490d2604cf2 100644 --- a/graphql/e2e/normal/schema_response.json +++ b/graphql/e2e/normal/schema_response.json @@ -45,6 +45,10 @@ ], "upsert": true }, + { + "predicate": "Astronaut.name", + "type": "string" + }, { "predicate": "Astronaut.missions", "type": "uid", diff --git a/graphql/schema/gqlschema.go b/graphql/schema/gqlschema.go index 4774c7d7923..c98bdd3738f 100644 --- a/graphql/schema/gqlschema.go +++ b/graphql/schema/gqlschema.go @@ -70,6 +70,8 @@ const ( apolloKeyArg = "fields" apolloExternalDirective = "external" apolloExtendsDirective = "extends" + apolloRequiresDirective = "requires" + apolloProvidesDirective = "provides" // custom directive args and fields dqlArg = "dql" @@ -387,6 +389,8 @@ type _Service { } directive @external on FIELD_DEFINITION +directive @requires(fields: _FieldSet!) on FIELD_DEFINITION +directive @provides(fields: _FieldSet!) on FIELD_DEFINITION directive @key(fields: _FieldSet!) on OBJECT | INTERFACE directive @extends on OBJECT | INTERFACE ` @@ -563,6 +567,8 @@ var directiveValidators = map[string]directiveValidator{ apolloKeyDirective: ValidatorNoOp, apolloExtendsDirective: ValidatorNoOp, apolloExternalDirective: apolloExternalValidation, + apolloRequiresDirective: apolloRequiresValidation, + apolloProvidesDirective: apolloProvidesValidation, remoteResponseDirective: remoteResponseValidation, } @@ -579,8 +585,10 @@ var directiveLocationMap = map[string]map[ast.DefinitionKind]bool{ customDirective: nil, remoteDirective: {ast.Object: true, ast.Interface: true, ast.Union: true, ast.InputObject: true, ast.Enum: true}, - cascadeDirective: nil, - generateDirective: {ast.Object: true, ast.Interface: true}, + cascadeDirective: nil, + generateDirective: {ast.Object: true, ast.Interface: true}, + apolloRequiresDirective: nil, + apolloProvidesDirective: nil, } // Struct to store parameters of @generate directive @@ -905,7 +913,7 @@ func applyFieldValidations(typ *ast.Definition, field *ast.FieldDefinition) gqle // query/mutation/update for all the types mentioned in the schema. // In case of Apollo service Query, input types from queries and mutations // are excluded due to the limited support currently. -func completeSchema(sch *ast.Schema, definitions []string, apolloServiceQuery bool) { +func completeSchema(sch *ast.Schema, definitions []string, providesFieldsMap map[string]map[string]bool, apolloServiceQuery bool) { query := sch.Types["Query"] if query != nil { query.Kind = ast.Object @@ -966,18 +974,19 @@ func completeSchema(sch *ast.Schema, definitions []string, apolloServiceQuery bo } params := parseGenerateDirectiveParams(defn) + providesTypeMap := providesFieldsMap[key] // Common types to both Interface and Object. - addReferenceType(sch, defn) + addReferenceType(sch, defn, providesTypeMap) if params.generateUpdateMutation { - addPatchType(sch, defn) + addPatchType(sch, defn, providesTypeMap) addUpdateType(sch, defn) - addUpdatePayloadType(sch, defn) + addUpdatePayloadType(sch, defn, providesTypeMap) } if params.generateDeleteMutation { - addDeletePayloadType(sch, defn) + addDeletePayloadType(sch, defn, providesTypeMap) } switch defn.Kind { @@ -994,23 +1003,23 @@ func completeSchema(sch *ast.Schema, definitions []string, apolloServiceQuery bo case ast.Object: // types and inputs needed for mutations if params.generateAddMutation { - addInputType(sch, defn) - addAddPayloadType(sch, defn) + addInputType(sch, defn, providesTypeMap) + addAddPayloadType(sch, defn, providesTypeMap) } addMutations(sch, defn, params) } // types and inputs needed for query and search - addFilterType(sch, defn) - addTypeOrderable(sch, defn) - addFieldFilters(sch, defn, apolloServiceQuery) - addAggregationResultType(sch, defn) + addFilterType(sch, defn, providesTypeMap) + addTypeOrderable(sch, defn, providesTypeMap) + addFieldFilters(sch, defn, providesTypeMap, apolloServiceQuery) + addAggregationResultType(sch, defn, providesTypeMap) // Don't expose queries for the @extends type to the gateway // as it is resolved through `_entities` resolver. if !(apolloServiceQuery && hasExtends(defn)) { - addQueries(sch, defn, params) + addQueries(sch, defn, providesTypeMap, params) } - addTypeHasFilter(sch, defn) + addTypeHasFilter(sch, defn, providesTypeMap) // We need to call this at last as aggregateFields // should not be part of HasFilter or UpdatePayloadType etc. addAggregateFields(sch, defn, apolloServiceQuery) @@ -1160,10 +1169,10 @@ func addUnionMemberTypeEnum(schema *ast.Schema, defn *ast.Definition) { // For extended Type definition, if Field with ID type is also field with @key directive then // 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) { - field := getFieldsWithoutIDType(schema, defn) +func addInputType(schema *ast.Schema, defn *ast.Definition, providesTypeMap map[string]bool) { + field := getFieldsWithoutIDType(schema, defn, providesTypeMap) if hasExtends(defn) { - idField := getIDField(defn) + idField := getIDField(defn, providesTypeMap) field = append(idField, field...) } @@ -1176,15 +1185,15 @@ func addInputType(schema *ast.Schema, defn *ast.Definition) { } } -func addReferenceType(schema *ast.Schema, defn *ast.Definition) { +func addReferenceType(schema *ast.Schema, defn *ast.Definition, providesTypeMap map[string]bool) { var flds ast.FieldList if defn.Kind == ast.Interface { if !hasID(defn) && !hasXID(defn) { return } - flds = append(getIDField(defn), getXIDField(defn)...) + flds = append(getIDField(defn, providesTypeMap), getXIDField(defn, providesTypeMap)...) } else { - flds = append(getIDField(defn), getFieldsWithoutIDType(schema, defn)...) + flds = append(getIDField(defn, providesTypeMap), getFieldsWithoutIDType(schema, defn, providesTypeMap)...) } if len(flds) == 1 && (hasID(defn) || hasXID(defn)) { @@ -1239,12 +1248,12 @@ func addUpdateType(schema *ast.Schema, defn *ast.Definition) { schema.Types["Update"+defn.Name+"Input"] = updType } -func addPatchType(schema *ast.Schema, defn *ast.Definition) { +func addPatchType(schema *ast.Schema, defn *ast.Definition, providesTypeMap map[string]bool) { if !hasFilterable(defn) { return } - nonIDFields := getNonIDFields(schema, defn) + nonIDFields := getNonIDFields(schema, defn, providesTypeMap) if len(nonIDFields) == 0 { // The user might just have an external id field and nothing else. We don't generate patch // type in that case. @@ -1276,7 +1285,7 @@ func addPatchType(schema *ast.Schema, defn *ast.Definition) { // ... // } // } -func addFieldFilters(schema *ast.Schema, defn *ast.Definition, apolloServiceQuery bool) { +func addFieldFilters(schema *ast.Schema, defn *ast.Definition, providesTypeMap map[string]bool, apolloServiceQuery bool) { for _, fld := range defn.Fields { // Filtering and ordering for fields with @custom/@lambda directive is handled by the remote // endpoint. @@ -1297,7 +1306,7 @@ func addFieldFilters(schema *ast.Schema, defn *ast.Definition, apolloServiceQuer // Ordering and pagination, however, only makes sense for fields of // list types (not scalar lists or enum lists). if isTypeList(fld) && !isEnumList(fld, schema) { - addOrderArgument(schema, fld) + addOrderArgument(schema, fld, providesTypeMap) // Pagination even makes sense when there's no orderables because // Dgraph will do UID order by default. @@ -1360,7 +1369,7 @@ func addFilterArgumentForField(schema *ast.Schema, fld *ast.FieldDefinition, fld // addTypeHasFilter adds `enum TypeHasFilter {...}` to the Schema // if the object/interface has a field other than the ID field -func addTypeHasFilter(schema *ast.Schema, defn *ast.Definition) { +func addTypeHasFilter(schema *ast.Schema, defn *ast.Definition, providesTypeMap map[string]bool) { filterName := defn.Name + "HasFilter" filter := &ast.Definition{ Kind: ast.Enum, @@ -1372,10 +1381,12 @@ func addTypeHasFilter(schema *ast.Schema, defn *ast.Definition) { continue } // Ignore Fields with @external directives also excluding those which are present - // as an argument in @key directive - if hasExternal(fld) && !isKeyField(fld, defn) { + // as an argument in @key directive. If the field is an argument to `@provides` directive + // then it can't be ignored. + if externalAndNonKeyField(fld, defn, providesTypeMap) { continue } + filter.EnumValues = append(filter.EnumValues, &ast.EnumValueDefinition{Name: fld.Name}) } @@ -1392,9 +1403,9 @@ func addTypeHasFilter(schema *ast.Schema, defn *ast.Definition) { } } -func addOrderArgument(schema *ast.Schema, fld *ast.FieldDefinition) { +func addOrderArgument(schema *ast.Schema, fld *ast.FieldDefinition, providesTypeMap map[string]bool) { fldType := fld.Type.Name() - if hasOrderables(schema.Types[fldType]) { + if hasOrderables(schema.Types[fldType], providesTypeMap) { fld.Arguments = append(fld.Arguments, &ast.ArgumentDefinition{ Name: "order", @@ -1487,7 +1498,7 @@ func mergeAndAddFilters(filterTypes []string, schema *ast.Schema, filterName str // f(filter: TFilter, ... ): T // ... // } -func addFilterType(schema *ast.Schema, defn *ast.Definition) { +func addFilterType(schema *ast.Schema, defn *ast.Definition, providesTypeMap map[string]bool) { filterName := defn.Name + "Filter" filter := &ast.Definition{ Kind: ast.InputObject, @@ -1496,8 +1507,9 @@ func addFilterType(schema *ast.Schema, defn *ast.Definition) { for _, fld := range defn.Fields { // Ignore Fields with @external directives also excluding those which are present - // as an argument in @key directive - if hasExternal(fld) && !isKeyField(fld, defn) { + // as an argument in @key directive. If the field is an argument to `@provides` directive + // then it can't be ignored. + if externalAndNonKeyField(fld, defn, providesTypeMap) { continue } @@ -1529,7 +1541,7 @@ func addFilterType(schema *ast.Schema, defn *ast.Definition) { } // Has filter makes sense only if there is atleast one non ID field in the defn - if len(getFieldsWithoutIDType(schema, defn)) > 0 { + if len(getFieldsWithoutIDType(schema, defn, providesTypeMap)) > 0 { filter.Fields = append(filter.Fields, &ast.FieldDefinition{Name: "has", Type: &ast.Type{Elem: &ast.Type{NamedType: defn.Name + "HasFilter"}}}, ) @@ -1578,24 +1590,28 @@ func isEnumList(fld *ast.FieldDefinition, sch *ast.Schema) bool { return typeDefn.Kind == "ENUM" && fld.Type.Elem != nil } -func hasOrderables(defn *ast.Definition) bool { +func hasOrderables(defn *ast.Definition, providesTypeMap map[string]bool) bool { return fieldAny(defn.Fields, func(fld *ast.FieldDefinition) bool { - return isOrderable(fld, defn) + return isOrderable(fld, defn, providesTypeMap) }) } -func isOrderable(fld *ast.FieldDefinition, defn *ast.Definition) 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 + // External field can't be ordered except when it is a @key field or + // the field is an argument in `@provides` directive. if !hasExternal(fld) { return orderable[fld.Type.NamedType] && !hasCustomOrLambda(fld) } - return isKeyField(fld, defn) + return isKeyField(fld, defn) || providesTypeMap[fld.Name] } // Returns true if the field is of type which can be summed. Eg: int, int64, float -func isSummable(fld *ast.FieldDefinition) bool { +func isSummable(fld *ast.FieldDefinition, defn *ast.Definition, providesTypeMap map[string]bool) bool { + if externalAndNonKeyField(fld, defn, providesTypeMap) { + return false + } return summable[fld.Type.NamedType] && !hasCustomOrLambda(fld) } @@ -1710,8 +1726,8 @@ func getSearchArgs(fld *ast.FieldDefinition) []string { // GraphQL orderings are given by the structure // `order: { asc: datePublished, then: { asc: title } }`. // a further `then` would be a third ordering, etc. -func addTypeOrderable(schema *ast.Schema, defn *ast.Definition) { - if !hasOrderables(defn) { +func addTypeOrderable(schema *ast.Schema, defn *ast.Definition, providesTypeMap map[string]bool) { + if !hasOrderables(defn, providesTypeMap) { return } @@ -1735,7 +1751,7 @@ func addTypeOrderable(schema *ast.Schema, defn *ast.Definition) { for _, fld := range defn.Fields { - if isOrderable(fld, defn) { + if isOrderable(fld, defn, providesTypeMap) { order.EnumValues = append(order.EnumValues, &ast.EnumValueDefinition{Name: fld.Name}) } @@ -1744,7 +1760,7 @@ func addTypeOrderable(schema *ast.Schema, defn *ast.Definition) { schema.Types[orderableName] = order } -func addAddPayloadType(schema *ast.Schema, defn *ast.Definition) { +func addAddPayloadType(schema *ast.Schema, defn *ast.Definition, providesTypeMap map[string]bool) { qry := &ast.FieldDefinition{ Name: CamelCase(defn.Name), Type: ast.ListType(&ast.Type{ @@ -1753,7 +1769,7 @@ func addAddPayloadType(schema *ast.Schema, defn *ast.Definition) { } addFilterArgument(schema, qry) - addOrderArgument(schema, qry) + addOrderArgument(schema, qry, providesTypeMap) addPaginationArguments(qry) if schema.Types["Add"+defn.Name+"Input"] != nil { schema.Types["Add"+defn.Name+"Payload"] = &ast.Definition{ @@ -1764,7 +1780,7 @@ func addAddPayloadType(schema *ast.Schema, defn *ast.Definition) { } } -func addUpdatePayloadType(schema *ast.Schema, defn *ast.Definition) { +func addUpdatePayloadType(schema *ast.Schema, defn *ast.Definition, providesTypeMap map[string]bool) { if !hasFilterable(defn) { return } @@ -1786,7 +1802,7 @@ func addUpdatePayloadType(schema *ast.Schema, defn *ast.Definition) { } addFilterArgument(schema, qry) - addOrderArgument(schema, qry) + addOrderArgument(schema, qry, providesTypeMap) addPaginationArguments(qry) schema.Types["Update"+defn.Name+"Payload"] = &ast.Definition{ @@ -1798,7 +1814,7 @@ func addUpdatePayloadType(schema *ast.Schema, defn *ast.Definition) { } } -func addDeletePayloadType(schema *ast.Schema, defn *ast.Definition) { +func addDeletePayloadType(schema *ast.Schema, defn *ast.Definition, providesTypeMap map[string]bool) { if !hasFilterable(defn) { return } @@ -1811,7 +1827,7 @@ func addDeletePayloadType(schema *ast.Schema, defn *ast.Definition) { } addFilterArgument(schema, qry) - addOrderArgument(schema, qry) + addOrderArgument(schema, qry, providesTypeMap) addPaginationArguments(qry) msg := &ast.FieldDefinition{ @@ -1826,7 +1842,7 @@ func addDeletePayloadType(schema *ast.Schema, defn *ast.Definition) { } } -func addAggregationResultType(schema *ast.Schema, defn *ast.Definition) { +func addAggregationResultType(schema *ast.Schema, defn *ast.Definition, providesTypeMap map[string]bool) { aggregationResultTypeName := defn.Name + "AggregateResult" var aggregateFields []*ast.FieldDefinition @@ -1852,7 +1868,7 @@ func addAggregationResultType(schema *ast.Schema, defn *ast.Definition) { } // Adds titleMax, titleMin fields for a field of name title. - if isOrderable(fld, defn) { + if isOrderable(fld, defn, providesTypeMap) { minField := &ast.FieldDefinition{ Name: fld.Name + "Min", Type: aggregateFieldType, @@ -1866,7 +1882,7 @@ func addAggregationResultType(schema *ast.Schema, defn *ast.Definition) { // Adds scoreSum and scoreAvg field for a field of name score. // The type of scoreAvg is Float irrespective of the type of score. - if isSummable(fld) { + if isSummable(fld, defn, providesTypeMap) { sumField := &ast.FieldDefinition{ Name: fld.Name + "Sum", Type: aggregateFieldType, @@ -1890,7 +1906,7 @@ func addAggregationResultType(schema *ast.Schema, defn *ast.Definition) { } } -func addGetQuery(schema *ast.Schema, defn *ast.Definition, generateSubscription bool) { +func addGetQuery(schema *ast.Schema, defn *ast.Definition, providesTypeMap map[string]bool, generateSubscription bool) { hasIDField := hasID(defn) hasXIDField := hasXID(defn) xidCount := xidsCount(defn.Fields) @@ -1907,7 +1923,7 @@ func addGetQuery(schema *ast.Schema, defn *ast.Definition, generateSubscription // If the defn, only specified one of ID/XID field, then they are mandatory. If it specified // both, then they are optional. if hasIDField { - fields := getIDField(defn) + fields := getIDField(defn, providesTypeMap) qry.Arguments = append(qry.Arguments, &ast.ArgumentDefinition{ Name: fields[0].Name, Type: &ast.Type{ @@ -1936,7 +1952,7 @@ func addGetQuery(schema *ast.Schema, defn *ast.Definition, generateSubscription } } -func addFilterQuery(schema *ast.Schema, defn *ast.Definition, generateSubscription bool) { +func addFilterQuery(schema *ast.Schema, defn *ast.Definition, providesTypeMap map[string]bool, generateSubscription bool) { qry := &ast.FieldDefinition{ Name: "query" + defn.Name, Type: &ast.Type{ @@ -1946,7 +1962,7 @@ func addFilterQuery(schema *ast.Schema, defn *ast.Definition, generateSubscripti }, } addFilterArgument(schema, qry) - addOrderArgument(schema, qry) + addOrderArgument(schema, qry, providesTypeMap) addPaginationArguments(qry) schema.Query.Fields = append(schema.Query.Fields, qry) @@ -1974,16 +1990,16 @@ func addAggregationQuery(schema *ast.Schema, defn *ast.Definition, generateSubsc } -func addPasswordQuery(schema *ast.Schema, defn *ast.Definition) { +func addPasswordQuery(schema *ast.Schema, defn *ast.Definition, providesTypeMap map[string]bool) { hasIDField := hasID(defn) hasXIDField := hasXID(defn) if !hasIDField && !hasXIDField { return } - idField := getIDField(defn) + idField := getIDField(defn, providesTypeMap) if !hasIDField { - idField = getXIDField(defn) + idField = getXIDField(defn, providesTypeMap) } passwordField := getPasswordField(defn) if passwordField == nil { @@ -2012,17 +2028,17 @@ func addPasswordQuery(schema *ast.Schema, defn *ast.Definition) { schema.Query.Fields = append(schema.Query.Fields, qry) } -func addQueries(schema *ast.Schema, defn *ast.Definition, params *GenerateDirectiveParams) { +func addQueries(schema *ast.Schema, defn *ast.Definition, providesTypeMap map[string]bool, params *GenerateDirectiveParams) { if params.generateGetQuery { - addGetQuery(schema, defn, params.generateSubscription) + addGetQuery(schema, defn, providesTypeMap, params.generateSubscription) } if params.generatePasswordQuery { - addPasswordQuery(schema, defn) + addPasswordQuery(schema, defn, providesTypeMap) } if params.generateFilterQuery { - addFilterQuery(schema, defn, params.generateSubscription) + addFilterQuery(schema, defn, providesTypeMap, params.generateSubscription) } if params.generateAggregateQuery { @@ -2150,7 +2166,7 @@ func createField(schema *ast.Schema, fld *ast.FieldDefinition) *ast.FieldDefinit return &newFld } -func getNonIDFields(schema *ast.Schema, defn *ast.Definition) ast.FieldList { +func getNonIDFields(schema *ast.Schema, defn *ast.Definition, providesTypeMap map[string]bool) ast.FieldList { fldList := make([]*ast.FieldDefinition, 0) for _, fld := range defn.Fields { if isIDField(defn, fld) || hasIDDirective(fld) { @@ -2158,8 +2174,9 @@ func getNonIDFields(schema *ast.Schema, defn *ast.Definition) ast.FieldList { } // Ignore Fields with @external directives also as they shouldn't be present - // in the Patch Type Also. - if hasExternal(fld) { + // in the Patch Type also. If the field is an argument to `@provides` directive + // then it should be presnt. + if externalAndNonKeyField(fld, defn, providesTypeMap) { continue } // Fields with @custom/@lambda directive should not be part of mutation input, @@ -2196,7 +2213,7 @@ func getNonIDFields(schema *ast.Schema, defn *ast.Definition) ast.FieldList { return append(fldList, pd) } -func getFieldsWithoutIDType(schema *ast.Schema, defn *ast.Definition) ast.FieldList { +func getFieldsWithoutIDType(schema *ast.Schema, defn *ast.Definition, providesTypeMap map[string]bool) ast.FieldList { fldList := make([]*ast.FieldDefinition, 0) for _, fld := range defn.Fields { if isIDField(defn, fld) { @@ -2205,7 +2222,7 @@ func getFieldsWithoutIDType(schema *ast.Schema, defn *ast.Definition) ast.FieldL // Ignore Fields with @external directives and excluding those which are present // as an argument in @key directive - if hasExternal(fld) && !isKeyField(fld, defn) { + if externalAndNonKeyField(fld, defn, providesTypeMap) { continue } @@ -2237,12 +2254,13 @@ func getFieldsWithoutIDType(schema *ast.Schema, defn *ast.Definition) ast.FieldL return append(fldList, pd) } -func getIDField(defn *ast.Definition) ast.FieldList { +func getIDField(defn *ast.Definition, providesTypeMap map[string]bool) ast.FieldList { fldList := make([]*ast.FieldDefinition, 0) for _, fld := range defn.Fields { if isIDField(defn, fld) { - // Excluding those fields which are external and are not @key. - if hasExternal(fld) && !isKeyField(fld, defn) { + // Excluding those fields which are external and are not @key and are not + // used as an argument in `@provides` directive. + if externalAndNonKeyField(fld, defn, providesTypeMap) { continue } newFld := *fld @@ -2269,12 +2287,13 @@ func getPasswordField(defn *ast.Definition) *ast.FieldDefinition { return fldList } -func getXIDField(defn *ast.Definition) ast.FieldList { +func getXIDField(defn *ast.Definition, providesTypeMap map[string]bool) ast.FieldList { fldList := make([]*ast.FieldDefinition, 0) for _, fld := range defn.Fields { if hasIDDirective(fld) { - // Excluding those fields which are external and are not @key. - if hasExternal(fld) && !isKeyField(fld, defn) { + // Excluding those fields which are external and are not @key and are not + // used as an argument in `@provides` directive. + if externalAndNonKeyField(fld, defn, providesTypeMap) { continue } newFld := *fld diff --git a/graphql/schema/gqlschema_test.yml b/graphql/schema/gqlschema_test.yml index a2386692115..d9fa401c169 100644 --- a/graphql/schema/gqlschema_test.yml +++ b/graphql/schema/gqlschema_test.yml @@ -2702,9 +2702,9 @@ invalid_schemas: - name: "@extends directive without @key directive" input: | type Product @extends{ - id: ID! @external - name: String! @external - reviews: [Reviews] + id: ID! @external + name: String! @external + reviews: [Reviews] } type Reviews @key(fields: "id") { @@ -2712,7 +2712,7 @@ invalid_schemas: review: String! } errlist: [ - {"message": "Type Product; Type Extension cannot be defined without @key directive", "locations": [ { "line": 11, "column": 12} ] }, + {"message": "Type Product; Type Extension cannot be defined without @key directive", "locations": [ { "line": 13, "column": 12} ] }, ] - name: "@remote directive with @key" input: | @@ -2744,7 +2744,48 @@ invalid_schemas: errlist: [ {"message": "Type Product: Field name: @search directive can not be defined on @external fields that are not @key.", "locations": [ { "line": 3, "column": 18} ] }, ] - + - name: "@requires directive defined on type definitions" + input: | + type Product @key(fields: "id"){ + id: ID! + name: String! + reviews: [Reviews] @requires(fields: "name") + } + type Reviews @key(fields: "id") { + id: ID! + review: String! + } + errlist: [ + {"message": "Type Product: Field reviews: @requires directive can only be defined on fields in type extensions. i.e., the type must have `@extends` or use `extend` keyword.", "locations": [ { "line": 4, "column": 23} ] } + ] + - name: "argument inside @requires directive is not an @external field." + input: | + extend type Product @key(fields: "id"){ + id: ID! @external + name: String! + reviews: [Reviews] @requires(fields: "name") + } + type Reviews @key(fields: "id") { + id: ID! + review: String! + } + errlist: [ + {"message": "Type Product; Field name must be @external.", "locations": [ { "line": 4, "column": 23} ] } + ] + - name: "@provides directive used on field with type that does not have a @key." + input: | + type Product @key(fields: "id"){ + id: ID! + name: String! + reviews: [Reviews] @provides(fields: "name") + } + type Reviews { + id: ID! + name: String + } + errlist: [ + {"message": "Type Product; Field reviews does not return a type that has a @key.", "locations": [ { "line": 4, "column": 23} ] } + ] diff --git a/graphql/schema/rules.go b/graphql/schema/rules.go index a15e84ef1c2..0fc0a277e7c 100644 --- a/graphql/schema/rules.go +++ b/graphql/schema/rules.go @@ -1373,8 +1373,8 @@ func customDirectiveValidation(sch *ast.Schema, } defn := sch.Types[typ.Name] - id := getIDField(defn) - xid := getXIDField(defn) + id := getIDField(defn, nil) + xid := getXIDField(defn, nil) if !isQueryOrMutationType(typ) { if len(id) == 0 && len(xid) == 0 { errs = append(errs, gqlerror.ErrorPosf( @@ -2069,6 +2069,76 @@ func apolloExtendsValidation(sch *ast.Schema, typ *ast.Definition) gqlerror.List return nil } +func apolloRequiresValidation(sch *ast.Schema, + typ *ast.Definition, + field *ast.FieldDefinition, + dir *ast.Directive, + secrets map[string]x.SensitiveByteSlice) gqlerror.List { + + extendsDirective := typ.Directives.ForName(apolloExtendsDirective) + if extendsDirective == nil { + return []*gqlerror.Error{gqlerror.ErrorPosf( + dir.Position, + "Type %s: Field %s: @requires directive can only be defined on fields in type extensions. i.e., the type must have `@extends` or use `extend` keyword.", typ.Name, field.Name)} + } + + arg := dir.Arguments.ForName(apolloKeyArg) + if arg == nil || arg.Value.Raw == "" { + return []*gqlerror.Error{gqlerror.ErrorPosf( + dir.Position, + "Type %s; Argument %s inside @requires directive must be defined.", typ.Name, apolloKeyArg)} + } + + fldList := strings.Fields(arg.Value.Raw) + for _, fld := range fldList { + fldDefn := typ.Fields.ForName(fld) + if fldDefn == nil { + return []*gqlerror.Error{gqlerror.ErrorPosf( + dir.Position, + "Type %s; @requires directive uses a field %s which is not defined inside the type.", typ.Name, fld)} + } + if !hasExternal(fldDefn) { + return []*gqlerror.Error{gqlerror.ErrorPosf( + dir.Position, + "Type %s; Field %s must be @external.", typ.Name, fld)} + } + } + return nil +} + +func apolloProvidesValidation(sch *ast.Schema, + typ *ast.Definition, + field *ast.FieldDefinition, + dir *ast.Directive, + secrets map[string]x.SensitiveByteSlice) gqlerror.List { + + fldTypeDefn := sch.Types[field.Type.Name()] + keyDirective := fldTypeDefn.Directives.ForName(apolloKeyDirective) + if keyDirective == nil { + return []*gqlerror.Error{gqlerror.ErrorPosf( + dir.Position, + "Type %s; Field %s does not return a type that has a @key.", typ.Name, field.Name)} + } + + arg := dir.Arguments.ForName(apolloKeyArg) + if arg == nil || arg.Value.Raw == "" { + return []*gqlerror.Error{gqlerror.ErrorPosf( + dir.Position, + "Type %s; Argument %s inside @provides directive must be defined.", typ.Name, apolloKeyArg)} + } + + fldList := strings.Fields(arg.Value.Raw) + for _, fld := range fldList { + fldDefn := fldTypeDefn.Fields.ForName(fld) + if fldDefn == nil { + return []*gqlerror.Error{gqlerror.ErrorPosf( + dir.Position, + "Type %s; Field %s: @provides field %s doesn't exist for type %s.", typ.Name, field.Name, fld, fldTypeDefn.Name)} + } + } + return nil +} + func apolloExternalValidation(sch *ast.Schema, typ *ast.Definition, field *ast.FieldDefinition, diff --git a/graphql/schema/schemagen.go b/graphql/schema/schemagen.go index 0d05daa8020..fc505b054b6 100644 --- a/graphql/schema/schemagen.go +++ b/graphql/schema/schemagen.go @@ -339,6 +339,7 @@ func NewHandler(input string, apolloServiceQuery bool) (Handler, error) { typesToComplete := make([]string, 0, len(doc.Definitions)) defns := make([]string, 0, len(doc.Definitions)) + providesFieldsMap := make(map[string]map[string]bool) for _, defn := range doc.Definitions { if defn.BuiltIn { continue @@ -349,6 +350,25 @@ func NewHandler(input string, apolloServiceQuery bool) (Handler, error) { if remoteDir != nil { continue } + + for _, fld := range defn.Fields { + providesDir := fld.Directives.ForName(apolloProvidesDirective) + if providesDir == nil { + continue + } + arg := providesDir.Arguments.ForName(apolloKeyArg) + providesFieldArgs := strings.Fields(arg.Value.Raw) + var typeMap map[string]bool + if existingTypeMap, ok := providesFieldsMap[fld.Type.Name()]; ok { + typeMap = existingTypeMap + } else { + typeMap = make(map[string]bool) + } + for _, fldName := range providesFieldArgs { + typeMap[fldName] = true + } + providesFieldsMap[fld.Type.Name()] = typeMap + } } typesToComplete = append(typesToComplete, defn.Name) } @@ -374,7 +394,7 @@ func NewHandler(input string, apolloServiceQuery bool) (Handler, error) { metaInfo.extraCorsHeaders = getAllowedHeaders(sch, defns, authHeader) dgSchema := genDgSchema(sch, typesToComplete) - completeSchema(sch, typesToComplete, apolloServiceQuery) + completeSchema(sch, typesToComplete, providesFieldsMap, apolloServiceQuery) cleanSchema(sch) if len(sch.Query.Fields) == 0 && len(sch.Mutation.Fields) == 0 { diff --git a/graphql/schema/testdata/apolloservice/input/extended-types.graphql b/graphql/schema/testdata/apolloservice/input/extended-types.graphql index fae8a433553..eb78c603171 100644 --- a/graphql/schema/testdata/apolloservice/input/extended-types.graphql +++ b/graphql/schema/testdata/apolloservice/input/extended-types.graphql @@ -1,6 +1,6 @@ type Mission @key(fields: "id") { id: ID! - crew: [Astronaut] + crew: [Astronaut] @provides(fields: "name age") designation: String! startDate: String endDate: String @@ -8,5 +8,15 @@ type Mission @key(fields: "id") { type Astronaut @key(fields: "id") @extends { id: ID! @external + name: String @external + age: Int @external missions: [Mission] -} \ No newline at end of file +} + + extend type Product @key(fields: "upc") { + upc: String! @id @external + price: Int @external + weight: Int @external + inStock: Boolean + shippingEstimate: Float @requires(fields: "price weight") + } \ No newline at end of file diff --git a/graphql/schema/testdata/apolloservice/output/extended-types.graphql b/graphql/schema/testdata/apolloservice/output/extended-types.graphql index 2a2524bbf84..3d375678a81 100644 --- a/graphql/schema/testdata/apolloservice/output/extended-types.graphql +++ b/graphql/schema/testdata/apolloservice/output/extended-types.graphql @@ -4,7 +4,7 @@ type Mission @key(fields: "id") { id: ID! - crew: [Astronaut] + crew: [Astronaut] @provides(fields: "name age") designation: String! startDate: String endDate: String @@ -12,10 +12,20 @@ type Mission @key(fields: "id") { type Astronaut @key(fields: "id") @extends { id: ID! @external + name: String @external + age: Int @external missions(filter: MissionFilter, order: MissionOrder, first: Int, offset: Int): [Mission] missionsAggregate(filter: MissionFilter): MissionAggregateResult } +type Product @key(fields: "upc") @extends { + upc: String! @id @external + price: Int @external + weight: Int @external + inStock: Boolean + shippingEstimate: Float @requires(fields: "price weight") +} + ####################### # Extended Definitions ####################### @@ -280,10 +290,21 @@ type AddMissionPayload { numUids: Int } +type AddProductPayload { + product(filter: ProductFilter, order: ProductOrder, first: Int, offset: Int): [Product] + numUids: Int +} + type AstronautAggregateResult { count: Int idMin: ID idMax: ID + nameMin: String + nameMax: String + ageMin: Int + ageMax: Int + ageSum: Int + ageAvg: Float } type DeleteAstronautPayload { @@ -298,6 +319,12 @@ type DeleteMissionPayload { numUids: Int } +type DeleteProductPayload { + product(filter: ProductFilter, order: ProductOrder, first: Int, offset: Int): [Product] + msg: String + numUids: Int +} + type MissionAggregateResult { count: Int designationMin: String @@ -308,6 +335,16 @@ type MissionAggregateResult { endDateMax: String } +type ProductAggregateResult { + count: Int + upcMin: String + upcMax: String + shippingEstimateMin: Float + shippingEstimateMax: Float + shippingEstimateSum: Float + shippingEstimateAvg: Float +} + type UpdateAstronautPayload { astronaut(filter: AstronautFilter, order: AstronautOrder, first: Int, offset: Int): [Astronaut] numUids: Int @@ -318,16 +355,25 @@ type UpdateMissionPayload { numUids: Int } +type UpdateProductPayload { + product(filter: ProductFilter, order: ProductOrder, first: Int, offset: Int): [Product] + numUids: Int +} + ####################### # Generated Enums ####################### enum AstronautHasFilter { + name + age missions } enum AstronautOrderable { id + name + age } enum MissionHasFilter { @@ -343,12 +389,25 @@ enum MissionOrderable { endDate } +enum ProductHasFilter { + upc + inStock + shippingEstimate +} + +enum ProductOrderable { + upc + shippingEstimate +} + ####################### # Generated Inputs ####################### input AddAstronautInput { id: ID! + name: String + age: Int missions: [MissionRef] } @@ -359,6 +418,12 @@ input AddMissionInput { endDate: String } +input AddProductInput { + upc: String! + inStock: Boolean + shippingEstimate: Float +} + input AstronautFilter { id: [ID!] has: [AstronautHasFilter] @@ -374,11 +439,15 @@ input AstronautOrder { } input AstronautPatch { + name: String + age: Int missions: [MissionRef] } input AstronautRef { id: ID + name: String + age: Int missions: [MissionRef] } @@ -411,6 +480,31 @@ input MissionRef { endDate: String } +input ProductFilter { + upc: StringHashFilter + has: [ProductHasFilter] + and: [ProductFilter] + or: [ProductFilter] + not: ProductFilter +} + +input ProductOrder { + asc: ProductOrderable + desc: ProductOrderable + then: ProductOrder +} + +input ProductPatch { + inStock: Boolean + shippingEstimate: Float +} + +input ProductRef { + upc: String + inStock: Boolean + shippingEstimate: Float +} + input UpdateAstronautInput { filter: AstronautFilter! set: AstronautPatch @@ -423,6 +517,12 @@ input UpdateMissionInput { remove: MissionPatch } +input UpdateProductInput { + filter: ProductFilter! + set: ProductPatch + remove: ProductPatch +} + ####################### # Generated Query ####################### @@ -444,5 +544,8 @@ type Mutation { addAstronaut(input: [AddAstronautInput!]!): AddAstronautPayload updateAstronaut(input: UpdateAstronautInput!): UpdateAstronautPayload deleteAstronaut(filter: AstronautFilter!): DeleteAstronautPayload + addProduct(input: [AddProductInput!]!, upsert: Boolean): AddProductPayload + updateProduct(input: UpdateProductInput!): UpdateProductPayload + deleteProduct(filter: ProductFilter!): DeleteProductPayload } diff --git a/graphql/schema/testdata/schemagen/input/apollo-federation.graphql b/graphql/schema/testdata/schemagen/input/apollo-federation.graphql index c6f4d14504e..dfa0f683cb5 100644 --- a/graphql/schema/testdata/schemagen/input/apollo-federation.graphql +++ b/graphql/schema/testdata/schemagen/input/apollo-federation.graphql @@ -1,12 +1,15 @@ extend type Product @key(fields: "id") { id: ID! @external name: String! @external - reviews: [Reviews] + price: Int @external + weight: Int @external + reviews: [Reviews] @requires(fields: "price weight") } type Reviews @key(fields: "id") { id: ID! review: String! + user: User @provides(fields: "age") } type Student @key(fields: "id"){ @@ -17,12 +20,13 @@ type Student @key(fields: "id"){ type School @key(fields: "id"){ id: ID! - students: [Student] + students: [Student] @provides(fields: "name") } extend type User @key(fields: "name") { id: ID! @external name: String! @id @external + age: Int! @external reviews: [Reviews] } diff --git a/graphql/schema/testdata/schemagen/output/apollo-federation.graphql b/graphql/schema/testdata/schemagen/output/apollo-federation.graphql index d4b40a478ba..9594dae7e42 100644 --- a/graphql/schema/testdata/schemagen/output/apollo-federation.graphql +++ b/graphql/schema/testdata/schemagen/output/apollo-federation.graphql @@ -5,6 +5,7 @@ type Reviews @key(fields: "id") { id: ID! review: String! + user(filter: UserFilter): User @provides(fields: "age") } type Student @key(fields: "id") { @@ -15,7 +16,7 @@ type Student @key(fields: "id") { type School @key(fields: "id") { id: ID! - students(filter: StudentFilter, order: StudentOrder, first: Int, offset: Int): [Student] + students(filter: StudentFilter, order: StudentOrder, first: Int, offset: Int): [Student] @provides(fields: "name") studentsAggregate(filter: StudentFilter): StudentAggregateResult } @@ -27,13 +28,16 @@ type Country { type Product @key(fields: "id") @extends { id: ID! @external name: String! @external - reviews(filter: ReviewsFilter, order: ReviewsOrder, first: Int, offset: Int): [Reviews] + price: Int @external + weight: Int @external + reviews(filter: ReviewsFilter, order: ReviewsOrder, first: Int, offset: Int): [Reviews] @requires(fields: "price weight") reviewsAggregate(filter: ReviewsFilter): ReviewsAggregateResult } type User @key(fields: "name") @extends { id: ID! @external name: String! @id @external + age: Int! @external reviews(filter: ReviewsFilter, order: ReviewsOrder, first: Int, offset: Int): [Reviews] reviewsAggregate(filter: ReviewsFilter): ReviewsAggregateResult } @@ -312,6 +316,8 @@ type _Service { } directive @external on FIELD_DEFINITION +directive @requires(fields: _FieldSet!) on FIELD_DEFINITION +directive @provides(fields: _FieldSet!) on FIELD_DEFINITION directive @key(fields: _FieldSet!) on OBJECT | INTERFACE directive @extends on OBJECT | INTERFACE @@ -453,6 +459,10 @@ type UserAggregateResult { count: Int nameMin: String nameMax: String + ageMin: Int + ageMax: Int + ageSum: Int + ageAvg: Float } ####################### @@ -479,6 +489,7 @@ enum ProductOrderable { enum ReviewsHasFilter { review + user } enum ReviewsOrderable { @@ -501,11 +512,13 @@ enum StudentOrderable { enum UserHasFilter { name + age reviews } enum UserOrderable { name + age } ####################### @@ -524,6 +537,7 @@ input AddProductInput { input AddReviewsInput { review: String! + user: UserRef } input AddSchoolInput { @@ -537,6 +551,7 @@ input AddStudentInput { input AddUserInput { name: String! + age: Int! reviews: [ReviewsRef] } @@ -602,11 +617,13 @@ input ReviewsOrder { input ReviewsPatch { review: String + user: UserRef } input ReviewsRef { id: ID review: String + user: UserRef } input SchoolFilter { @@ -702,11 +719,13 @@ input UserOrder { } input UserPatch { + age: Int reviews: [ReviewsRef] } input UserRef { name: String + age: Int reviews: [ReviewsRef] } diff --git a/graphql/schema/wrappers.go b/graphql/schema/wrappers.go index e4e27867166..aa7d7673f21 100644 --- a/graphql/schema/wrappers.go +++ b/graphql/schema/wrappers.go @@ -815,6 +815,12 @@ func nonExternalAndKeyFields(defn *ast.Definition) ast.FieldList { return fldList } +// externalAndNonKeyField returns true for those fields which have @external directive and +// are not @key fields and are not an arugment to the @provides directive. +func externalAndNonKeyField(fld *ast.FieldDefinition, defn *ast.Definition, providesTypeMap map[string]bool) bool { + return hasExternal(fld) && !isKeyField(fld, defn) && !providesTypeMap[fld.Name] +} + // buildCustomDirectiveForLambda returns custom directive for the given field to be used for @lambda // The constructed @custom looks like this: // @custom(http: {