Skip to content

Commit

Permalink
feat: Mutation typed input (sourcenetwork#2167)
Browse files Browse the repository at this point in the history
## Relevant issue(s)

Resolves sourcenetwork#2143 

## Description

This PR adds a typed input object for create and update mutations.

~~As a side effect of this change the relationship alias has been
replaced with the `<type>_id` input field.~~

Relational sub-documents cannot be created from mutation input in this
implementation.

Related SIP sourcenetwork/SIPs#10

## Tasks

- [x] I made sure the code is well commented, particularly
hard-to-understand areas.
- [x] I made sure the repository-held documentation is changed
accordingly.
- [x] I made sure the pull request title adheres to the conventional
commit style (the subset used in the project can be found in
[tools/configs/chglog/config.yml](tools/configs/chglog/config.yml)).
- [x] I made sure to discuss its limitations such as threats to
validity, vulnerability to mistake and misuse, robustness to
invalidation of assumptions, resource requirements, ...

## How has this been tested?

Make test

Specify the platform(s) on which this was tested:
- MacOS
  • Loading branch information
nasdf authored and shahzadlone committed Jan 20, 2024
1 parent d6b3772 commit 4c31df0
Show file tree
Hide file tree
Showing 46 changed files with 429 additions and 199 deletions.
4 changes: 4 additions & 0 deletions client/document.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,8 @@ func getFloat64(v any) (float64, error) {
return val.Float64()
case int:
return float64(val), nil
case int32:
return float64(val), nil
case int64:
return float64(val), nil
case float64:
Expand All @@ -266,6 +268,8 @@ func getInt64(v any) (int64, error) {
return val.Int64()
case int:
return int64(val), nil
case int32:
return int64(val), nil
case int64:
return val, nil
case float64:
Expand Down
2 changes: 1 addition & 1 deletion client/request/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const (
RelatedObjectID = "_id"

Cid = "cid"
Data = "data"
Input = "input"
FieldName = "field"
FieldIDName = "fieldId"
ShowDeleted = "showDeleted"
Expand Down
2 changes: 1 addition & 1 deletion client/request/mutation.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ type ObjectMutation struct {

IDs immutable.Option[[]string]
Filter immutable.Option[Filter]
Data string
Input map[string]any

Fields []Selection
}
Expand Down
28 changes: 8 additions & 20 deletions planner/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@
package planner

import (
"encoding/json"

"github.com/sourcenetwork/defradb/client"
"github.com/sourcenetwork/defradb/client/request"
"github.com/sourcenetwork/defradb/core"
Expand All @@ -37,9 +35,9 @@ type createNode struct {
// collection name, meta-data, etc.
collection client.Collection

// newDoc is the JSON string of the new document, unparsed
newDocStr string
doc *client.Document
// input map of fields and values
input map[string]any
doc *client.Document

err error

Expand All @@ -59,7 +57,7 @@ func (n *createNode) Kind() string { return "createNode" }
func (n *createNode) Init() error { return nil }

func (n *createNode) Start() error {
doc, err := client.NewDocFromJSON([]byte(n.newDocStr), n.collection.Schema())
doc, err := client.NewDocFromMap(n.input, n.collection.Schema())
if err != nil {
n.err = err
return err
Expand Down Expand Up @@ -135,24 +133,14 @@ func (n *createNode) Close() error {

func (n *createNode) Source() planNode { return n.results }

func (n *createNode) simpleExplain() (map[string]any, error) {
data := map[string]any{}
err := json.Unmarshal([]byte(n.newDocStr), &data)
if err != nil {
return nil, err
}

return map[string]any{
dataLabel: data,
}, nil
}

// Explain method returns a map containing all attributes of this node that
// are to be explained, subscribes / opts-in this node to be an explainablePlanNode.
func (n *createNode) Explain(explainType request.ExplainType) (map[string]any, error) {
switch explainType {
case request.SimpleExplain:
return n.simpleExplain()
return map[string]any{
inputLabel: n.input,
}, nil

case request.ExecuteExplain:
return map[string]any{
Expand All @@ -173,7 +161,7 @@ func (p *Planner) CreateDoc(parsed *mapper.Mutation) (planNode, error) {
// create a mutation createNode.
create := &createNode{
p: p,
newDocStr: parsed.Data,
input: parsed.Input,
results: results,
docMapper: docMapper{parsed.DocumentMapping},
}
Expand Down
2 changes: 1 addition & 1 deletion planner/explain.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ const (
childFieldNameLabel = "childFieldName"
collectionIDLabel = "collectionID"
collectionNameLabel = "collectionName"
dataLabel = "data"
inputLabel = "input"
fieldNameLabel = "fieldName"
filterLabel = "filter"
joinRootLabel = "root"
Expand Down
2 changes: 1 addition & 1 deletion planner/mapper/mapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -1089,7 +1089,7 @@ func ToMutation(ctx context.Context, store client.Store, mutationRequest *reques
return &Mutation{
Select: *underlyingSelect,
Type: MutationType(mutationRequest.Type),
Data: mutationRequest.Data,
Input: mutationRequest.Input,
}, nil
}

Expand Down
7 changes: 3 additions & 4 deletions planner/mapper/mutation.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,8 @@ type Mutation struct {
// The type of mutation. For example a create request.
Type MutationType

// The data to be used for the mutation. For example, during a create this
// will be the json representation of the object to be inserted.
Data string
// Input is the map of fields and values used for the mutation.
Input map[string]any
}

func (m *Mutation) CloneTo(index int) Requestable {
Expand All @@ -40,6 +39,6 @@ func (m *Mutation) cloneTo(index int) *Mutation {
return &Mutation{
Select: *m.Select.cloneTo(index),
Type: m.Type,
Data: m.Data,
Input: m.Input,
}
}
18 changes: 9 additions & 9 deletions planner/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ type updateNode struct {

docIDs []string

patch string
// input map of fields and values
input map[string]any

isUpdating bool

Expand Down Expand Up @@ -67,7 +68,11 @@ func (n *updateNode) Next() (bool, error) {
if err != nil {
return false, err
}
_, err = n.collection.UpdateWithDocID(n.p.ctx, docID, n.patch)
patch, err := json.Marshal(n.input)
if err != nil {
return false, err
}
_, err = n.collection.UpdateWithDocID(n.p.ctx, docID, string(patch))
if err != nil {
return false, err
}
Expand Down Expand Up @@ -126,12 +131,7 @@ func (n *updateNode) simpleExplain() (map[string]any, error) {
}

// Add the attribute that represents the patch to update with.
data := map[string]any{}
err := json.Unmarshal([]byte(n.patch), &data)
if err != nil {
return nil, err
}
simpleExplainMap[dataLabel] = data
simpleExplainMap[inputLabel] = n.input

return simpleExplainMap, nil
}
Expand Down Expand Up @@ -160,7 +160,7 @@ func (p *Planner) UpdateDocs(parsed *mapper.Mutation) (planNode, error) {
filter: parsed.Filter,
docIDs: parsed.DocIDs.Value(),
isUpdating: true,
patch: parsed.Data,
input: parsed.Input,
docMapper: docMapper{parsed.DocumentMapping},
}

Expand Down
50 changes: 44 additions & 6 deletions request/graphql/parser/mutation.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,9 @@ func parseMutation(schema gql.Schema, parent *gql.Object, field *ast.Field) (*re
for _, argument := range field.Arguments {
prop := argument.Name.Value
// parse each individual arg type seperately
if prop == request.Data { // parse data
raw := argument.Value.(*ast.StringValue)
if raw.Value == "" {
return nil, ErrEmptyDataPayload
}
mut.Data = raw.Value
if prop == request.Input { // parse input
raw := argument.Value.(*ast.ObjectValue)
mut.Input = parseMutationInputObject(raw)
} else if prop == request.FilterClause { // parse filter
obj := argument.Value.(*ast.ObjectValue)
filterType, ok := getArgumentType(fieldDef, request.FilterClause)
Expand Down Expand Up @@ -147,3 +144,44 @@ func parseMutation(schema gql.Schema, parent *gql.Object, field *ast.Field) (*re
mut.Fields, err = parseSelectFields(schema, request.ObjectSelection, fieldObject, field.SelectionSet)
return mut, err
}

// parseMutationInput parses the correct underlying
// value type of the given ast.Value
func parseMutationInput(val ast.Value) any {
switch t := val.(type) {
case *ast.IntValue:
return gql.Int.ParseLiteral(val)
case *ast.FloatValue:
return gql.Float.ParseLiteral(val)
case *ast.BooleanValue:
return t.Value
case *ast.StringValue:
return t.Value
case *ast.ObjectValue:
return parseMutationInputObject(t)
case *ast.ListValue:
return parseMutationInputList(t)
default:
return val.GetValue()
}
}

// parseMutationInputList parses the correct underlying
// value type for all of the values in the ast.ListValue
func parseMutationInputList(val *ast.ListValue) []any {
list := make([]any, 0)
for _, val := range val.Values {
list = append(list, parseMutationInput(val))
}
return list
}

// parseMutationInputObject parses the correct underlying
// value type for all of the fields in the ast.ObjectValue
func parseMutationInputObject(val *ast.ObjectValue) map[string]any {
obj := make(map[string]any)
for _, field := range val.Fields {
obj[field.Name.Value] = parseMutationInput(field.Value)
}
return obj
}
7 changes: 0 additions & 7 deletions request/graphql/schema/descriptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,6 @@ An optional value that specifies as to whether deleted documents may be
`
createDocumentDescription string = `
Creates a single document of this type using the data provided.
`
createDataArgDescription string = `
The json representation of the document you wish to create. Required.
`
updateDocumentsDescription string = `
Updates documents in this collection using the data provided. Only documents
Expand All @@ -148,10 +145,6 @@ An optional set of docID values that will limit the update to documents
An optional filter for this update that will limit the update to the documents
matching the given criteria. If no matching documents are found, the operation
will succeed, but no documents will be updated.
`
updateDataArgDescription string = `
The json representation of the fields to update and their new values. Required.
Fields not explicitly mentioned here will not be updated.
`
deleteDocumentsDescription string = `
Deletes documents in this collection matching any provided criteria. If no
Expand Down
61 changes: 35 additions & 26 deletions request/graphql/schema/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,35 +13,37 @@ package schema
import "github.com/sourcenetwork/defradb/errors"

const (
errDuplicateField string = "duplicate field"
errFieldMissingRelation string = "field missing associated relation"
errRelationMissingField string = "relation missing field"
errAggregateTargetNotFound string = "aggregate target not found"
errSchemaTypeAlreadyExist string = "schema type already exists"
errObjectNotFoundDuringThunk string = "object not found whilst executing fields thunk"
errTypeNotFound string = "no type found for given name"
errRelationNotFound string = "no relation found"
errNonNullForTypeNotSupported string = "NonNull variants for type are not supported"
errIndexMissingFields string = "index missing fields"
errIndexUnknownArgument string = "index with unknown argument"
errIndexInvalidArgument string = "index with invalid argument"
errIndexInvalidName string = "index with invalid name"
errDuplicateField string = "duplicate field"
errFieldMissingRelation string = "field missing associated relation"
errRelationMissingField string = "relation missing field"
errAggregateTargetNotFound string = "aggregate target not found"
errSchemaTypeAlreadyExist string = "schema type already exists"
errMutationInputTypeAlreadyExist string = "mutation input type already exists"
errObjectNotFoundDuringThunk string = "object not found whilst executing fields thunk"
errTypeNotFound string = "no type found for given name"
errRelationNotFound string = "no relation found"
errNonNullForTypeNotSupported string = "NonNull variants for type are not supported"
errIndexMissingFields string = "index missing fields"
errIndexUnknownArgument string = "index with unknown argument"
errIndexInvalidArgument string = "index with invalid argument"
errIndexInvalidName string = "index with invalid name"
)

var (
ErrDuplicateField = errors.New(errDuplicateField)
ErrFieldMissingRelation = errors.New(errFieldMissingRelation)
ErrRelationMissingField = errors.New(errRelationMissingField)
ErrAggregateTargetNotFound = errors.New(errAggregateTargetNotFound)
ErrSchemaTypeAlreadyExist = errors.New(errSchemaTypeAlreadyExist)
ErrObjectNotFoundDuringThunk = errors.New(errObjectNotFoundDuringThunk)
ErrTypeNotFound = errors.New(errTypeNotFound)
ErrRelationNotFound = errors.New(errRelationNotFound)
ErrNonNullForTypeNotSupported = errors.New(errNonNullForTypeNotSupported)
ErrRelationMutlipleTypes = errors.New("relation type can only be either One or Many, not both")
ErrRelationMissingTypes = errors.New("relation is missing its defined types and fields")
ErrRelationInvalidType = errors.New("relation has an invalid type to be finalize")
ErrMultipleRelationPrimaries = errors.New("relation can only have a single field set as primary")
ErrDuplicateField = errors.New(errDuplicateField)
ErrFieldMissingRelation = errors.New(errFieldMissingRelation)
ErrRelationMissingField = errors.New(errRelationMissingField)
ErrAggregateTargetNotFound = errors.New(errAggregateTargetNotFound)
ErrSchemaTypeAlreadyExist = errors.New(errSchemaTypeAlreadyExist)
ErrMutationInputTypeAlreadyExist = errors.New(errMutationInputTypeAlreadyExist)
ErrObjectNotFoundDuringThunk = errors.New(errObjectNotFoundDuringThunk)
ErrTypeNotFound = errors.New(errTypeNotFound)
ErrRelationNotFound = errors.New(errRelationNotFound)
ErrNonNullForTypeNotSupported = errors.New(errNonNullForTypeNotSupported)
ErrRelationMutlipleTypes = errors.New("relation type can only be either One or Many, not both")
ErrRelationMissingTypes = errors.New("relation is missing its defined types and fields")
ErrRelationInvalidType = errors.New("relation has an invalid type to be finalize")
ErrMultipleRelationPrimaries = errors.New("relation can only have a single field set as primary")
// NonNull is the literal name of the GQL type, so we have to disable the linter
//nolint:revive
ErrNonNullNotSupported = errors.New("NonNull fields are not currently supported")
Expand Down Expand Up @@ -94,6 +96,13 @@ func NewErrSchemaTypeAlreadyExist(name string) error {
)
}

func NewErrMutationInputTypeAlreadyExist(name string) error {
return errors.New(
errMutationInputTypeAlreadyExist,
errors.NewKV("Name", name),
)
}

func NewErrObjectNotFoundDuringThunk(object string) error {
return errors.New(
errObjectNotFoundDuringThunk,
Expand Down
Loading

0 comments on commit 4c31df0

Please sign in to comment.