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: Add blob scalar type #2091

Merged
merged 18 commits into from
Dec 7, 2023
Merged
5 changes: 4 additions & 1 deletion client/descriptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ func (f FieldKind) String() string {
return "[String]"
case FieldKind_STRING_ARRAY:
return "[String!]"
case FieldKind_BLOB:
return "Blob"
default:
return fmt.Sprint(uint8(f))
}
Expand All @@ -166,7 +168,7 @@ const (
FieldKind_DATETIME FieldKind = 10
FieldKind_STRING FieldKind = 11
FieldKind_STRING_ARRAY FieldKind = 12
_ FieldKind = 13 // safe to repurpose (was never used)
FieldKind_BLOB FieldKind = 13
_ FieldKind = 14 // safe to repurpose (was never used)
_ FieldKind = 15 // safe to repurpose (was never used)

Expand Down Expand Up @@ -204,6 +206,7 @@ var FieldKindStringToEnumMapping = map[string]FieldKind{
"String": FieldKind_STRING,
"[String]": FieldKind_NILLABLE_STRING_ARRAY,
"[String!]": FieldKind_STRING_ARRAY,
"Blob": FieldKind_BLOB,
}

// RelationType describes the type of relation between two types.
Expand Down
3 changes: 3 additions & 0 deletions db/collection_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,9 @@ func validateFieldSchema(val *fastjson.Value, field client.FieldDescription) (an

case client.FieldKind_FOREIGN_OBJECT, client.FieldKind_FOREIGN_OBJECT_ARRAY:
return nil, NewErrFieldOrAliasToFieldNotExist(field.Name)

case client.FieldKind_BLOB:
return getString(val)
}

return nil, client.NewErrUnhandledType("FieldKind", field.Kind)
Expand Down
9 changes: 9 additions & 0 deletions db/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/sourcenetwork/defradb/core"
"github.com/sourcenetwork/defradb/datastore"
"github.com/sourcenetwork/defradb/errors"
"github.com/sourcenetwork/defradb/request/graphql/schema/types"
)

// CollectionIndex is an interface for collection indexes
Expand Down Expand Up @@ -51,6 +52,14 @@ func getValidateIndexFieldFunc(kind client.FieldKind) func(any) bool {
return canConvertIndexFieldValue[float64]
case client.FieldKind_BOOL:
return canConvertIndexFieldValue[bool]
case client.FieldKind_BLOB:
return func(val any) bool {
blobStrVal, ok := val.(string)
if !ok {
return false
}
return types.BlobPattern.MatchString(blobStrVal)
}
case client.FieldKind_DATETIME:
return func(val any) bool {
timeStrVal, ok := val.(string)
Expand Down
3 changes: 3 additions & 0 deletions db/indexed_docs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -421,13 +421,16 @@ func TestNonUnique_StoringIndexedFieldValueOfDifferentTypes(t *testing.T) {
{Name: "invalid bool", FieldKind: client.FieldKind_BOOL, FieldVal: "invalid", ShouldFail: true},
{Name: "invalid datetime", FieldKind: client.FieldKind_DATETIME, FieldVal: nowStr[1:], ShouldFail: true},
{Name: "invalid datetime type", FieldKind: client.FieldKind_DATETIME, FieldVal: 1, ShouldFail: true},
{Name: "invalid blob", FieldKind: client.FieldKind_BLOB, FieldVal: "invalid", ShouldFail: true},
{Name: "invalid blob type", FieldKind: client.FieldKind_BLOB, FieldVal: 1, ShouldFail: true},

{Name: "valid int", FieldKind: client.FieldKind_INT, FieldVal: 12},
{Name: "valid float", FieldKind: client.FieldKind_FLOAT, FieldVal: 36.654},
{Name: "valid bool true", FieldKind: client.FieldKind_BOOL, FieldVal: true},
{Name: "valid bool false", FieldKind: client.FieldKind_BOOL, FieldVal: false},
{Name: "valid datetime string", FieldKind: client.FieldKind_DATETIME, FieldVal: nowStr},
{Name: "valid empty string", FieldKind: client.FieldKind_STRING, FieldVal: ""},
{Name: "valid blob type", FieldKind: client.FieldKind_BLOB, FieldVal: "00ff"},
}

for i, tc := range testCase {
Expand Down
3 changes: 3 additions & 0 deletions request/graphql/schema/collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ func astTypeToKind(t ast.Type) (client.FieldKind, error) {
typeFloat string = "Float"
typeDateTime string = "DateTime"
typeString string = "String"
typeBlob string = "Blob"
)

switch astTypeVal := t.(type) {
Expand Down Expand Up @@ -379,6 +380,8 @@ func astTypeToKind(t ast.Type) (client.FieldKind, error) {
return client.FieldKind_DATETIME, nil
case typeString:
return client.FieldKind_STRING, nil
case typeBlob:
return client.FieldKind_BLOB, nil
default:
return client.FieldKind_FOREIGN_OBJECT, nil
}
Expand Down
6 changes: 5 additions & 1 deletion request/graphql/schema/descriptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
gql "github.com/sourcenetwork/graphql-go"

"github.com/sourcenetwork/defradb/client"
schemaTypes "github.com/sourcenetwork/defradb/request/graphql/schema/types"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought: Seeing this make me wonder if the type should be added to the graphql-go package.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm open to that. Should we move the other types there as well?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes me nervous, I think we added DateTime to that package, but graphql-go is supposed to be a general purpose gql package, not a defra package. And we may/do want to get rid of it/replace it at somepoint

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we added DateTime to that package

No, DateTime was already defined as a scalar in the graphql-go package, but there were some problems with it as it relates to Defra integration, there was PR that got merged on the graphql-go package related to the DateTime stuff, but it wasn't the implementation/definition of the DateTime scalar. (reference: #931 and sourcenetwork/graphql-go#8)

I do agree though that we shouldn't have to define the scalar in the graphql-go package, it exposes the necessary types/functions for us to define our own scalars outside that package, exactly as they're being used here. Moreover, the fact that we have our own defined custom scalars implies that w.e package we may replace the graphql-go package with, should implement the "base" scalars as defined in the GQL spec, and all we have to worry about is the specific "custom" scalars that are neatly organized in a single place.

)

var (
Expand All @@ -31,9 +32,10 @@ var (
gql.String: client.FieldKind_STRING,
&gql.Object{}: client.FieldKind_FOREIGN_OBJECT,
&gql.List{}: client.FieldKind_FOREIGN_OBJECT_ARRAY,
// Custom scalars
schemaTypes.BlobScalarType: client.FieldKind_BLOB,
// More custom ones to come
// - JSON
// - ByteArray
// - Counters
}

Expand All @@ -52,6 +54,7 @@ var (
client.FieldKind_STRING: gql.String,
client.FieldKind_STRING_ARRAY: gql.NewList(gql.NewNonNull(gql.String)),
client.FieldKind_NILLABLE_STRING_ARRAY: gql.NewList(gql.String),
client.FieldKind_BLOB: schemaTypes.BlobScalarType,
}

// This map is fine to use
Expand All @@ -70,6 +73,7 @@ var (
client.FieldKind_STRING: client.LWW_REGISTER,
client.FieldKind_STRING_ARRAY: client.LWW_REGISTER,
client.FieldKind_NILLABLE_STRING_ARRAY: client.LWW_REGISTER,
client.FieldKind_BLOB: client.LWW_REGISTER,
client.FieldKind_FOREIGN_OBJECT: client.NONE_CRDT,
client.FieldKind_FOREIGN_OBJECT_ARRAY: client.NONE_CRDT,
}
Expand Down
3 changes: 3 additions & 0 deletions request/graphql/schema/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@ func defaultTypes() []gql.Type {
gql.Int,
gql.String,

// Custom Scalar types
schemaTypes.BlobScalarType,

// Base Query types

// Sort/Order enum
Expand Down
65 changes: 65 additions & 0 deletions request/graphql/schema/types/scalars.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright 2023 Democratized Data Foundation
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

package types

import (
"encoding/hex"
"regexp"

"github.com/sourcenetwork/graphql-go"
"github.com/sourcenetwork/graphql-go/language/ast"
)

// BlobPattern is a regex for validating blob hex strings
var BlobPattern = regexp.MustCompile("^[0-9a-fA-F]+$")

// coerceBlob converts the given value into a valid hex string.
// If the value cannot be converted nil is returned.
func coerceBlob(value any) any {
switch value := value.(type) {
case []byte:
return hex.EncodeToString(value)

case *[]byte:
return coerceBlob(*value)

case string:
if !BlobPattern.MatchString(value) {
return nil
Copy link
Contributor

@AndrewSisley AndrewSisley Nov 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Why does this return nil instead of []byte{}?

EDIT:
question: Is this a silent failure? Like the errors above:

if err != nil {
	return nil
}

todo: If so, please document

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm just following how the other types work in the graphql-go library. It seems like nil is the return value when the type cannot be parsed.

}
return value

case *string:
return coerceBlob(*value)

default:
return nil
}
}

var BlobScalarType = graphql.NewScalar(graphql.ScalarConfig{
Name: "Blob",
Description: "The `Blob` scalar type represents a binary large object.",
// Serialize converts the value to a hex string
Serialize: coerceBlob,
// ParseValue converts the value to a hex string
ParseValue: coerceBlob,
// ParseLiteral converts the ast value to a hex string
ParseLiteral: func(valueAST ast.Value) any {
switch valueAST := valueAST.(type) {
case *ast.StringValue:
return coerceBlob(valueAST.Value)
default:
// return nil if the value cannot be parsed
return nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

todo: Similar to another comment, please document this - it looks like a silent error, but I only know that by guessing given the:

if err != nil {
	return nil
}

stuff in other code blocks

}
},
})
88 changes: 88 additions & 0 deletions request/graphql/schema/types/scalars_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright 2023 Democratized Data Foundation
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

package types

import (
"testing"

"github.com/sourcenetwork/graphql-go/language/ast"
"github.com/stretchr/testify/assert"
)

func TestBlobScalarTypeSerialize(t *testing.T) {
stringInput := "00ff"
bytesInput := []byte{0, 255}

cases := []struct {
input any
expect any
}{
{stringInput, "00ff"},
{&stringInput, "00ff"},
{bytesInput, "00ff"},
{&bytesInput, "00ff"},
{nil, nil},
{0, nil},
{false, nil},
}
for _, c := range cases {
result := BlobScalarType.Serialize(c.input)
assert.Equal(t, c.expect, result)
}
}

func TestBlobScalarTypeParseValue(t *testing.T) {
stringInput := "00ff"
bytesInput := []byte{0, 255}
// invalid string containing non-hex characters
invalidHexString := "!@#$%^&*"

cases := []struct {
input any
expect any
}{
{stringInput, "00ff"},
{&stringInput, "00ff"},
{bytesInput, "00ff"},
{&bytesInput, "00ff"},
{invalidHexString, nil},
{&invalidHexString, nil},
{nil, nil},
{0, nil},
{false, nil},
}
for _, c := range cases {
result := BlobScalarType.ParseValue(c.input)
assert.Equal(t, c.expect, result)
}
}

func TestBlobScalarTypeParseLiteral(t *testing.T) {
cases := []struct {
input ast.Value
expect any
}{
{&ast.StringValue{Value: "00ff"}, "00ff"},
{&ast.StringValue{Value: "00!@#$%^&*"}, nil},
{&ast.StringValue{Value: "!@#$%^&*00"}, nil},
{&ast.IntValue{}, nil},
{&ast.BooleanValue{}, nil},
{&ast.NullValue{}, nil},
{&ast.EnumValue{}, nil},
{&ast.FloatValue{}, nil},
{&ast.ListValue{}, nil},
{&ast.ObjectValue{}, nil},
}
for _, c := range cases {
result := BlobScalarType.ParseLiteral(c.input)
assert.Equal(t, c.expect, result)
}
}
60 changes: 60 additions & 0 deletions tests/integration/mutation/update/field_kinds/blob_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright 2023 Democratized Data Foundation
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

package field_kinds

import (
"testing"

testUtils "github.com/sourcenetwork/defradb/tests/integration"
)

func TestMutationUpdate_WithBlobField(t *testing.T) {
test := testUtils.TestCase{
Description: "Simple update of blob field",
Actions: []any{
testUtils.SchemaUpdate{
Schema: `
type Users {
name: String
data: Blob
}
`,
},
testUtils.CreateDoc{
Doc: `{
"name": "John",
"data": "00FE"
}`,
},
testUtils.UpdateDoc{
Doc: `{
"data": "00FF"
}`,
},
testUtils.Request{
Request: `
query {
Users {
data
}
}
`,
Results: []map[string]any{
{
"data": "00FF",
},
},
},
},
}

testUtils.ExecuteTestCase(t, test)
}
Loading
Loading