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): adds @default directive for setting default field values at create and update #8017

Merged
merged 12 commits into from
Nov 26, 2021
Next Next commit
feat(graphql): adds @created and @Updated directives for server gener…
…ated mutation timestamps

Adds support for @created and @Updated directives on GraphQL input schema.
Schema gen validates that field is of type `DateTime!`
Schema gen does not add @created and @Updated fields to add/update input types, preventing
modification over GraphQL
Rewrite mutation sets @created fields with current DateTime when node is new, and @Updated fields
any time node is changed.
Non null input checks ignore fields with @created/@Updated metadata
dpeek committed Sep 7, 2021

Verified

This commit was signed with the committer’s verified signature.
dpeek David Peek
commit b13defafc762ddadd1ff83c0f2e0315507830b92
98 changes: 98 additions & 0 deletions graphql/resolve/add_mutation_test.yaml
Original file line number Diff line number Diff line change
@@ -5504,3 +5504,101 @@
implementing types of same interfaces: T1 and T2"
}

-
name: "Add mutation with @created and @updated directive"
gqlmutation: |
mutation($input: [AddBookingInput!]!) {
addBooking(input: $input) {
booking {
name
created
updated
}
}
}
gqlvariables: |
{
"input": [
{
"name": "Holiday to Bermuda"
}
]
}
explanation: "As booking has @created and @updated directives, these DateTime fields should be populated"
dgmutations:
- setjson: |
{
"Booking.created": "2019-10-12T07:20:50.52Z",
"Booking.name": "Holiday to Bermuda",
"Booking.updated": "2019-10-12T07:20:50.52Z",
"dgraph.type": [
"Booking"
],
"uid":"_:Booking_1"
}

-
name: "Upsert mutation with @created and @updated directives where only one of the nodes exists"
explanation: "Booking1 should only have updated timestamp as it exists, Booking2 should have created and updated timestamps"
gqlmutation: |
mutation addBookingXID($input: [AddBookingXIDInput!]!) {
addBookingXID(input: $input, upsert: true) {
bookingXID {
name
}
}
}
gqlvariables: |
{ "input":
[
{
"id": "Booking1",
"name": "Trip to Bermuda"
},
{
"id": "Booking2",
"name": "Trip to Antigua"
}
]
}
dgquery: |-
query {
BookingXID_1(func: eq(BookingXID.id, "Booking1")) {
uid
dgraph.type
}
BookingXID_2(func: eq(BookingXID.id, "Booking2")) {
uid
dgraph.type
}
}
qnametouid: |-
{
"BookingXID_1": "0x11"
}
dgquerysec: |-
query {
BookingXID_1 as BookingXID_1(func: uid(0x11)) @filter(type(BookingXID)) {
uid
}
}
dgmutations:
- setjson: |
{
"uid" : "uid(BookingXID_1)",
"BookingXID.id": "Booking1",
"BookingXID.name": "Trip to Bermuda",
"BookingXID.updated": "2019-10-12T07:20:50.52Z"
}
cond: "@if(gt(len(BookingXID_1), 0))"
- setjson: |
{
"uid": "_:BookingXID_2",
"BookingXID.id": "Booking2",
"BookingXID.name": "Trip to Antigua",
"BookingXID.created": "2019-10-12T07:20:50.52Z",
"BookingXID.updated": "2019-10-12T07:20:50.52Z",
"dgraph.type": [
"BookingXID"
]
}
15 changes: 15 additions & 0 deletions graphql/resolve/mutation_rewriter.go
Original file line number Diff line number Diff line change
@@ -1598,6 +1598,8 @@ func rewriteObject(
}
}

var isNewNode = false;

// This is not an XID reference. This is also not a UID reference.
// This is definitely a new node.
// Create new node
@@ -1646,7 +1648,20 @@ func rewriteObject(
// "_:Project2" . myUID will store the variable generated to reference this node.
newObj["dgraph.type"] = dgraphTypes
newObj["uid"] = myUID
isNewNode = true
}

var timestamp = "2019-10-12T07:20:50.52Z"
// var timestamp = time.Now().Format(time.RFC3339)
for _, field := range typ.Fields() {
if field.HasCreatedDirective() && isNewNode {
newObj[field.DgraphPredicate()] = timestamp
}
if field.HasUpdatedDirective() {
newObj[field.DgraphPredicate()] = timestamp
}
}


// Add Inverse Link if necessary
deleteInverseObject(obj, srcField)
15 changes: 15 additions & 0 deletions graphql/resolve/schema.graphql
Original file line number Diff line number Diff line change
@@ -175,6 +175,21 @@ type Student implements People {
taughtBy: [Teacher] @hasInverse(field: "teaches")
}

# For testing created/updated timestamps
type Booking {
id: ID!
name: String!
created: DateTime! @created
updated: DateTime! @updated
}

type BookingXID {
id: String! @id
name: String!
created: DateTime! @created
updated: DateTime! @updated
}

type Comment {
id: ID!
author: String!
35 changes: 35 additions & 0 deletions graphql/resolve/update_mutation_test.yaml
Original file line number Diff line number Diff line change
@@ -2405,3 +2405,38 @@
"uid": "uid(x)"
}
cond: "@if(gt(len(x), 0))"

-
name: "Update with @created and @updated directives"
gqlmutation: |
mutation updateBooking($patch: UpdateBookingInput!) {
updateBooking(input: $patch) {
booking {
id
}
}
}
gqlvariables: |
{ "patch":
{ "filter": {
"id": ["0x123", "0x124"]
},
"set": {
"name": "Flight to Antigua"
}
}
}
explanation: "The update patch should include a timestamp on the field with the @updated directive"
dgquerysec: |-
query {
x as updateBooking(func: uid(0x123, 0x124)) @filter(type(Booking)) {
uid
}
}
dgmutations:
- setjson: |
{ "uid" : "uid(x)",
"Booking.name": "Flight to Antigua",
"Booking.updated": "2019-10-12T07:20:50.52Z"
}
cond: "@if(gt(len(x), 0))"
26 changes: 22 additions & 4 deletions graphql/schema/gqlschema.go
Original file line number Diff line number Diff line change
@@ -48,6 +48,8 @@ const (
remoteResponseDirective = "remoteResponse"
lambdaDirective = "lambda"
lambdaOnMutateDirective = "lambdaOnMutate"
createdDirective = "created"
updatedDirective = "updated"

generateDirective = "generate"
generateQueryArg = "query"
@@ -278,6 +280,8 @@ directive @hasInverse(field: String!) on FIELD_DEFINITION
directive @search(by: [DgraphIndex!]) on FIELD_DEFINITION
directive @dgraph(type: String, pred: String) on OBJECT | INTERFACE | FIELD_DEFINITION
directive @id(interface: Boolean) on FIELD_DEFINITION
directive @created on FIELD_DEFINITION
directive @updated on FIELD_DEFINITION
directive @withSubscription on OBJECT | INTERFACE | FIELD_DEFINITION
directive @secret(field: String!, pred: String) on OBJECT | INTERFACE
directive @auth(
@@ -309,6 +313,8 @@ directive @hasInverse(field: String!) on FIELD_DEFINITION
directive @search(by: [DgraphIndex!]) on FIELD_DEFINITION
directive @dgraph(type: String, pred: String) on OBJECT | INTERFACE | FIELD_DEFINITION
directive @id(interface: Boolean) on FIELD_DEFINITION
directive @created on FIELD_DEFINITION
directive @updated on FIELD_DEFINITION
directive @withSubscription on OBJECT | INTERFACE | FIELD_DEFINITION
directive @secret(field: String!, pred: String) on OBJECT | INTERFACE
directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM
@@ -570,6 +576,8 @@ var directiveValidators = map[string]directiveValidator{
remoteDirective: ValidatorNoOp,
deprecatedDirective: ValidatorNoOp,
lambdaDirective: lambdaDirectiveValidation,
createdDirective: timestampDirectiveValidation,
updatedDirective: timestampDirectiveValidation,
lambdaOnMutateDirective: ValidatorNoOp,
generateDirective: ValidatorNoOp,
apolloKeyDirective: ValidatorNoOp,
@@ -1284,7 +1292,7 @@ func addPatchType(schema *ast.Schema, defn *ast.Definition, providesTypeMap map[
return
}

nonIDFields := getNonIDFields(schema, defn, providesTypeMap)
nonIDFields := getPatchFields(schema, defn, providesTypeMap)
if len(nonIDFields) == 0 {
// The user might just have an predicate with reverse edge id field and nothing else.
// We don't generate patch type in that case.
@@ -2208,7 +2216,7 @@ func createField(schema *ast.Schema, fld *ast.FieldDefinition) *ast.FieldDefinit
return &newFld
}

func getNonIDFields(schema *ast.Schema, defn *ast.Definition, providesTypeMap map[string]bool) ast.FieldList {
func getPatchFields(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) {
@@ -2226,6 +2234,11 @@ func getNonIDFields(schema *ast.Schema, defn *ast.Definition, providesTypeMap ma
if hasCustomOrLambda(fld) {
continue
}
// Fields with @created/@updated directive should not be part of mutation input,
// hence we skip them.
if hasCreatedOrUpdated(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
@@ -2279,7 +2292,12 @@ func getFieldsWithoutIDType(schema *ast.Schema, defn *ast.Definition,
if hasCustomOrLambda(fld) {
continue
}
// see the comment in getNonIDFields as well.
// Fields with @created/@updated directive should not be part of mutation input,
// hence we skip them.
if hasCreatedOrUpdated(fld) {
continue
}
// see the comment in getPatchFields as well.
if isMultiLangField(fld, true) && isAddingInput {
continue
}
@@ -2290,7 +2308,7 @@ func getFieldsWithoutIDType(schema *ast.Schema, defn *ast.Definition,
continue
}

// see also comment in getNonIDFields
// see also comment in getPatchFields
if schema.Types[fld.Type.Name()].Kind == ast.Interface &&
(!hasID(schema.Types[fld.Type.Name()]) && !hasXID(schema.Types[fld.Type.Name()])) {
continue
20 changes: 20 additions & 0 deletions graphql/schema/gqlschema_test.yml
Original file line number Diff line number Diff line change
@@ -2935,6 +2935,26 @@ invalid_schemas:
"locations": [ { "line": 2, "column": 18 } ] },
]

- name: "@created field must be of type DateTime!"
input: |
type Person {
created: String @created
}
errlist: [
{ "message": "Type Person; Field created: with @created directive must be of type DateTime!, not String",
"locations": [ { "line": 2, "column": 20 } ] },
]

- name: "@updated field must be of type DateTime!"
input: |
type Person {
updated: String @updated
}
errlist: [
{ "message": "Type Person; Field updated: with @updated directive must be of type DateTime!, not String",
"locations": [ { "line": 2, "column": 20 } ] },
]

valid_schemas:
- name: "Multiple fields with @id directive should be allowed"
input: |
15 changes: 15 additions & 0 deletions graphql/schema/rules.go
Original file line number Diff line number Diff line change
@@ -1291,6 +1291,21 @@ func lambdaDirectiveValidation(sch *ast.Schema,
return errs
}

func timestampDirectiveValidation(sch *ast.Schema,
typ *ast.Definition,
field *ast.FieldDefinition,
dir *ast.Directive,
secrets map[string]x.Sensitive) gqlerror.List {
if field.Type.NamedType == "DateTime" && field.Type.NonNull {
return nil
}
return []*gqlerror.Error{gqlerror.ErrorPosf(
dir.Position,
"Type %s; Field %s: with @%s directive must be of type DateTime!, not %s",
typ.Name, field.Name, dir.Name, field.Type.String())}

}

func lambdaOnMutateValidation(sch *ast.Schema, typ *ast.Definition) gqlerror.List {
dir := typ.Directives.ForName(lambdaOnMutateDirective)
if dir == nil {
Original file line number Diff line number Diff line change
@@ -193,6 +193,8 @@ directive @hasInverse(field: String!) on FIELD_DEFINITION
directive @search(by: [DgraphIndex!]) on FIELD_DEFINITION
directive @dgraph(type: String, pred: String) on OBJECT | INTERFACE | FIELD_DEFINITION
directive @id(interface: Boolean) on FIELD_DEFINITION
directive @created on FIELD_DEFINITION
directive @updated on FIELD_DEFINITION
directive @withSubscription on OBJECT | INTERFACE | FIELD_DEFINITION
directive @secret(field: String!, pred: String) on OBJECT | INTERFACE
directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM
Original file line number Diff line number Diff line change
@@ -185,6 +185,8 @@ directive @hasInverse(field: String!) on FIELD_DEFINITION
directive @search(by: [DgraphIndex!]) on FIELD_DEFINITION
directive @dgraph(type: String, pred: String) on OBJECT | INTERFACE | FIELD_DEFINITION
directive @id(interface: Boolean) on FIELD_DEFINITION
directive @created on FIELD_DEFINITION
directive @updated on FIELD_DEFINITION
directive @withSubscription on OBJECT | INTERFACE | FIELD_DEFINITION
directive @secret(field: String!, pred: String) on OBJECT | INTERFACE
directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM
Original file line number Diff line number Diff line change
@@ -199,6 +199,8 @@ directive @hasInverse(field: String!) on FIELD_DEFINITION
directive @search(by: [DgraphIndex!]) on FIELD_DEFINITION
directive @dgraph(type: String, pred: String) on OBJECT | INTERFACE | FIELD_DEFINITION
directive @id(interface: Boolean) on FIELD_DEFINITION
directive @created on FIELD_DEFINITION
directive @updated on FIELD_DEFINITION
directive @withSubscription on OBJECT | INTERFACE | FIELD_DEFINITION
directive @secret(field: String!, pred: String) on OBJECT | INTERFACE
directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM
Original file line number Diff line number Diff line change
@@ -195,6 +195,8 @@ directive @hasInverse(field: String!) on FIELD_DEFINITION
directive @search(by: [DgraphIndex!]) on FIELD_DEFINITION
directive @dgraph(type: String, pred: String) on OBJECT | INTERFACE | FIELD_DEFINITION
directive @id(interface: Boolean) on FIELD_DEFINITION
directive @created on FIELD_DEFINITION
directive @updated on FIELD_DEFINITION
directive @withSubscription on OBJECT | INTERFACE | FIELD_DEFINITION
directive @secret(field: String!, pred: String) on OBJECT | INTERFACE
directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM
Original file line number Diff line number Diff line change
@@ -180,6 +180,8 @@ directive @hasInverse(field: String!) on FIELD_DEFINITION
directive @search(by: [DgraphIndex!]) on FIELD_DEFINITION
directive @dgraph(type: String, pred: String) on OBJECT | INTERFACE | FIELD_DEFINITION
directive @id(interface: Boolean) on FIELD_DEFINITION
directive @created on FIELD_DEFINITION
directive @updated on FIELD_DEFINITION
directive @withSubscription on OBJECT | INTERFACE | FIELD_DEFINITION
directive @secret(field: String!, pred: String) on OBJECT | INTERFACE
directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
type Booking {
id: ID!
name: String!
created: DateTime! @created
updated: DateTime! @updated
}

type BookingXID {
id: String! @id
name: String!
created: DateTime! @created
updated: DateTime! @updated
}
Original file line number Diff line number Diff line change
@@ -215,6 +215,8 @@ directive @hasInverse(field: String!) on FIELD_DEFINITION
directive @search(by: [DgraphIndex!]) on FIELD_DEFINITION
directive @dgraph(type: String, pred: String) on OBJECT | INTERFACE | FIELD_DEFINITION
directive @id(interface: Boolean) on FIELD_DEFINITION
directive @created on FIELD_DEFINITION
directive @updated on FIELD_DEFINITION
directive @withSubscription on OBJECT | INTERFACE | FIELD_DEFINITION
directive @secret(field: String!, pred: String) on OBJECT | INTERFACE
directive @auth(
Loading