Skip to content

Commit

Permalink
WIP - Prevent mutations from secondary side of relation
Browse files Browse the repository at this point in the history
  • Loading branch information
AndrewSisley committed Oct 10, 2024
1 parent bc68f57 commit 05e0cec
Show file tree
Hide file tree
Showing 22 changed files with 284 additions and 973 deletions.
32 changes: 31 additions & 1 deletion client/document.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,11 +241,30 @@ func validateFieldSchema(val any, field FieldDefinition) (NormalValue, error) {
if err != nil {
return nil, err
}

_, err = NewDocIDFromString(v)
if err != nil {
return nil, err
}

return NewNormalString(v), nil
}

switch field.Kind {
case FieldKind_DocID, FieldKind_NILLABLE_STRING, FieldKind_NILLABLE_BLOB:
case FieldKind_DocID:
v, err := getString(val)
if err != nil {
return nil, err
}

_, err = NewDocIDFromString(v)
if err != nil {
return nil, err
}

return NewNormalString(v), nil

case FieldKind_NILLABLE_STRING, FieldKind_NILLABLE_BLOB:
v, err := getString(val)
if err != nil {
return nil, err
Expand Down Expand Up @@ -692,6 +711,17 @@ func (doc *Document) Set(field string, value any) error {
if !exists {
return NewErrFieldNotExist(field)
}

if fd.Kind == FieldKind_DocID && strings.HasSuffix(field, request.RelatedObjectID) {
objFieldName := strings.TrimSuffix(field, request.RelatedObjectID)
ofd, exists := doc.collectionDefinition.GetFieldByName(objFieldName)
if exists && !ofd.IsPrimaryRelation {
return NewErrCannotSetRelationFromSecondarySide(field)
}
} else if fd.Kind.IsObject() && !fd.IsPrimaryRelation {
return NewErrCannotSetRelationFromSecondarySide(field)
}

if fd.Kind.IsObject() && !fd.Kind.IsArray() {
if !strings.HasSuffix(field, request.RelatedObjectID) {
field = field + request.RelatedObjectID
Expand Down
5 changes: 5 additions & 0 deletions client/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const (
errCanNotTurnNormalValueIntoArray string = "can not turn normal value into array"
errCanNotMakeNormalNilFromFieldKind string = "can not make normal nil from field kind"
errFailedToParseKind string = "failed to parse kind"
errCannotSetRelationFromSecondarySide string = "cannot set relation from secondary side"
)

// Errors returnable from this package.
Expand Down Expand Up @@ -190,3 +191,7 @@ func ReviveError(message string) error {
return fmt.Errorf("%s", message)
}
}

func NewErrCannotSetRelationFromSecondarySide(name string) error {
return errors.New(errCannotSetRelationFromSecondarySide, errors.NewKV("Name", name))
}
25 changes: 0 additions & 25 deletions internal/db/collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -655,31 +655,6 @@ func (c *collection) save(
// that it's set to the same as the field description CRDT type.
val.SetType(fieldDescription.Typ)

relationFieldDescription, isSecondaryRelationID := fieldDescription.GetSecondaryRelationField(c.Definition())
if isSecondaryRelationID {
if val.Value() == nil {
// If the value (relation) is nil, we don't need to check for any documents already linked to it
continue
}

primaryId := val.Value().(string)

err = c.patchPrimaryDoc(
ctx,
c.Name().Value(),
relationFieldDescription,
primaryKey.DocID,
primaryId,
)
if err != nil {
return cid.Undef, err
}

// If this field was a secondary relation ID the related document will have been
// updated instead and we should discard this value
continue
}

err = c.validateOneToOneLinkDoesntAlreadyExist(
ctx,
doc.ID().String(),
Expand Down
87 changes: 0 additions & 87 deletions internal/db/collection_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,11 @@ package db
import (
"context"

ds "github.com/ipfs/go-datastore"
"github.com/sourcenetwork/immutable"
"github.com/valyala/fastjson"

"github.com/sourcenetwork/defradb/client"
"github.com/sourcenetwork/defradb/client/request"
"github.com/sourcenetwork/defradb/errors"
"github.com/sourcenetwork/defradb/internal/planner"
)

Expand Down Expand Up @@ -133,91 +131,6 @@ func (c *collection) updateWithFilter(
return results, nil
}

// patchPrimaryDoc patches the (primary) document linked to from the document of the given DocID via the
// given (secondary) relationship field description (hosted on the collection of the document matching the
// given DocID).
//
// The given field value should be the string representation of the DocID of the primary document to be
// patched.
func (c *collection) patchPrimaryDoc(
ctx context.Context,
secondaryCollectionName string,
relationFieldDescription client.FieldDefinition,
docID string,
fieldValue string,
) error {
primaryDocID, err := client.NewDocIDFromString(fieldValue)
if err != nil {
return err
}

primaryDef, _, err := client.GetDefinitionFromStore(ctx, c.db, c.Definition(), relationFieldDescription.Kind)
if err != nil {
return err
}

primaryField, ok := primaryDef.Description.GetFieldByRelation(
relationFieldDescription.RelationName,
secondaryCollectionName,
relationFieldDescription.Name,
)
if !ok {
return client.NewErrFieldNotExist(relationFieldDescription.RelationName)
}

primaryIDField, ok := primaryDef.GetFieldByName(primaryField.Name + request.RelatedObjectID)
if !ok {
return client.NewErrFieldNotExist(primaryField.Name + request.RelatedObjectID)
}

primaryCol := c.db.newCollection(primaryDef.Description, primaryDef.Schema)
doc, err := primaryCol.Get(
ctx,
primaryDocID,
false,
)

if err != nil && !errors.Is(err, ds.ErrNotFound) {
return err
}

// If the document doesn't exist then there is nothing to update.
if doc == nil {
return nil
}

err = primaryCol.validateOneToOneLinkDoesntAlreadyExist(
ctx,
primaryDocID.String(),
primaryIDField,
docID,
)
if err != nil {
return err
}

existingVal, err := doc.GetValue(primaryIDField.Name)
if err != nil && !errors.Is(err, client.ErrFieldNotExist) {
return err
}

if existingVal != nil && existingVal.Value() != "" && existingVal.Value() != docID {
return NewErrOneOneAlreadyLinked(docID, fieldValue, relationFieldDescription.RelationName)
}

err = doc.Set(primaryIDField.Name, docID)
if err != nil {
return err
}

err = primaryCol.Update(ctx, doc)
if err != nil {
return err
}

return nil
}

// makeSelectionPlan constructs a simple read-only plan of the collection using the given filter.
// currently it doesn't support any other operations other than filters.
// (IE: No limit, order, etc)
Expand Down
8 changes: 8 additions & 0 deletions internal/request/graphql/schema/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,14 @@ func (g *Generator) buildMutationInputTypes(collections []client.CollectionDefin
continue
}

if field.Kind == client.FieldKind_DocID && strings.HasSuffix(field.Name, request.RelatedObjectID) {
objFieldName := strings.TrimSuffix(field.Name, request.RelatedObjectID)
ofd, exists := collection.GetFieldByName(objFieldName)
if exists && !ofd.IsPrimaryRelation {
continue
}
}

var ttype gql.Type
if field.Kind.IsObject() {
if field.Kind.IsArray() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,35 +94,6 @@ func TestMutationCreateOneToMany_AliasedRelationNameNonExistingRelationManySide_
}
executeTestCase(t, test)
}
func TestMutationCreateOneToMany_AliasedRelationNameInvalidIDManySide_CreatedDoc(t *testing.T) {
test := testUtils.TestCase{
Description: "One to many create mutation, invalid id, from the many side, with alias",
Actions: []any{
testUtils.CreateDoc{
CollectionID: 0,
Doc: `{
"name": "Painted House",
"author": "ValueDoesntMatter"
}`,
},
testUtils.Request{
Request: `query {
Book {
name
}
}`,
Results: map[string]any{
"Book": []map[string]any{
{
"name": "Painted House",
},
},
},
},
},
}
executeTestCase(t, test)
}

func TestMutationCreateOneToMany_AliasedRelationNameToLinkFromManySide(t *testing.T) {
test := testUtils.TestCase{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,23 +73,6 @@ func TestMutationCreateOneToOne_UseAliasWithNonExistingRelationPrimarySide_Creat
executeTestCase(t, test)
}

func TestMutationCreateOneToOne_UseAliasWithNonExistingRelationSecondarySide_Error(t *testing.T) {
test := testUtils.TestCase{
Description: "One to one create mutation, alias relation, from the secondary side",
Actions: []any{
testUtils.CreateDoc{
CollectionID: 0,
Doc: `{
"name": "Painted House",
"author": "bae-be6d8024-4953-5a92-84b4-f042d25230c6"
}`,
ExpectedError: "document not found or not authorized to access",
},
},
}
executeTestCase(t, test)
}

func TestMutationCreateOneToOne_UseAliasedRelationNameToLink_QueryFromPrimarySide(t *testing.T) {
test := testUtils.TestCase{
Description: "One to one create mutation with an alias relation.",
Expand Down Expand Up @@ -153,7 +136,7 @@ func TestMutationCreateOneToOne_UseAliasedRelationNameToLink_QueryFromPrimarySid
executeTestCase(t, test)
}

func TestMutationCreateOneToOne_UseAliasedRelationNameToLink_QueryFromSecondarySide(t *testing.T) {
func TestMutationCreateOneToOne_UseAliasedRelationNameToLink_Errors(t *testing.T) {
test := testUtils.TestCase{
Description: "One to one create mutation from secondary side with alias relation.",
Actions: []any{
Expand All @@ -169,46 +152,7 @@ func TestMutationCreateOneToOne_UseAliasedRelationNameToLink_QueryFromSecondaryS
"name": "Painted House",
"author": testUtils.NewDocIndex(1, 0),
},
},
testUtils.Request{
Request: `query {
Author {
name
published {
name
}
}
}`,
Results: map[string]any{
"Author": []map[string]any{
{
"name": "John Grisham",
"published": map[string]any{
"name": "Painted House",
},
},
},
},
},
testUtils.Request{
Request: `query {
Book {
name
author {
name
}
}
}`,
Results: map[string]any{
"Book": []map[string]any{
{
"name": "Painted House",
"author": map[string]any{
"name": "John Grisham",
},
},
},
},
ExpectedError: "cannot set relation from secondary side",
},
},
}
Expand Down
Loading

0 comments on commit 05e0cec

Please sign in to comment.