Skip to content
Permalink

Comparing changes

This is a direct comparison between two commits made in this repository or its related repositories. View the default comparison for this range or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: sourcenetwork/defradb
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 19c54389b3ec1a593d34fd508394346e56302cde
Choose a base ref
..
head repository: sourcenetwork/defradb
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 77bdcdb6a60f1a6d870dcb1a28ac555008a755da
Choose a head ref
3 changes: 3 additions & 0 deletions docs/data_format_changes/i3137-default-value-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Default Value Fix

Default value parsing has changed slightly and causes the change detector to fail.
57 changes: 0 additions & 57 deletions http/middleware.go
Original file line number Diff line number Diff line change
@@ -26,63 +26,6 @@ import (
"github.com/sourcenetwork/defradb/internal/db"
)

const (
// txHeaderName is the name of the transaction header.
// This header should contain a valid transaction id.
txHeaderName = "x-defradb-tx"
)

type contextKey string

var (
// txsContextKey is the context key for the transaction *sync.Map
txsContextKey = contextKey("txs")
// dbContextKey is the context key for the client.DB
dbContextKey = contextKey("db")
// colContextKey is the context key for the client.Collection
//
// If a transaction exists, all operations will be executed
// in the current transaction context.
colContextKey = contextKey("col")
)

// mustGetContextClientCollection returns the client collection from the http request context or panics.
//
// This should only be called from functions within the http package.
func mustGetContextClientCollection(req *http.Request) client.Collection {
return req.Context().Value(colContextKey).(client.Collection) //nolint:forcetypeassert
}

// mustGetContextSyncMap returns the sync map from the http request context or panics.
//
// This should only be called from functions within the http package.
func mustGetContextSyncMap(req *http.Request) *sync.Map {
return req.Context().Value(txsContextKey).(*sync.Map) //nolint:forcetypeassert
}

// mustGetContextClientDB returns the client DB from the http request context or panics.
//
// This should only be called from functions within the http package.
func mustGetContextClientDB(req *http.Request) client.DB {
return req.Context().Value(dbContextKey).(client.DB) //nolint:forcetypeassert
}

// mustGetContextClientStore returns the client store from the http request context or panics.
//
// This should only be called from functions within the http package.
func mustGetContextClientStore(req *http.Request) client.Store {
return req.Context().Value(dbContextKey).(client.Store) //nolint:forcetypeassert
}

// tryGetContextClientP2P returns the P2P client from the http request context and a boolean
// indicating if p2p was enabled.
//
// This should only be called from functions within the http package.
func tryGetContextClientP2P(req *http.Request) (client.P2P, bool) {
p2p, ok := req.Context().Value(dbContextKey).(client.P2P)
return p2p, ok
}

// CorsMiddleware handles cross origin request
func CorsMiddleware(allowedOrigins []string) func(http.Handler) http.Handler {
return cors.Handler(cors.Options{
60 changes: 60 additions & 0 deletions http/utils.go
Original file line number Diff line number Diff line change
@@ -14,8 +14,68 @@ import (
"encoding/json"
"io"
"net/http"
"sync"

"github.com/sourcenetwork/defradb/client"
)

const (
// txHeaderName is the name of the transaction header.
// This header should contain a valid transaction id.
txHeaderName = "x-defradb-tx"
)

type contextKey string

var (
// txsContextKey is the context key for the transaction *sync.Map
txsContextKey = contextKey("txs")
// dbContextKey is the context key for the client.DB
dbContextKey = contextKey("db")
// colContextKey is the context key for the client.Collection
//
// If a transaction exists, all operations will be executed
// in the current transaction context.
colContextKey = contextKey("col")
)

// mustGetContextClientCollection returns the client collection from the http request context or panics.
//
// This should only be called from functions within the http package.
func mustGetContextClientCollection(req *http.Request) client.Collection {
return req.Context().Value(colContextKey).(client.Collection) //nolint:forcetypeassert
}

// mustGetContextSyncMap returns the sync map from the http request context or panics.
//
// This should only be called from functions within the http package.
func mustGetContextSyncMap(req *http.Request) *sync.Map {
return req.Context().Value(txsContextKey).(*sync.Map) //nolint:forcetypeassert
}

// mustGetContextClientDB returns the client DB from the http request context or panics.
//
// This should only be called from functions within the http package.
func mustGetContextClientDB(req *http.Request) client.DB {
return req.Context().Value(dbContextKey).(client.DB) //nolint:forcetypeassert
}

// mustGetContextClientStore returns the client store from the http request context or panics.
//
// This should only be called from functions within the http package.
func mustGetContextClientStore(req *http.Request) client.Store {
return req.Context().Value(dbContextKey).(client.Store) //nolint:forcetypeassert
}

// tryGetContextClientP2P returns the P2P client from the http request context and a boolean
// indicating if p2p was enabled.
//
// This should only be called from functions within the http package.
func tryGetContextClientP2P(req *http.Request) (client.P2P, bool) {
p2p, ok := req.Context().Value(dbContextKey).(client.P2P)
return p2p, ok
}

func requestJSON(req *http.Request, out any) error {
data, err := io.ReadAll(req.Body)
if err != nil {
18 changes: 18 additions & 0 deletions internal/db/definition_validation.go
Original file line number Diff line number Diff line change
@@ -166,6 +166,7 @@ var globalValidators = []definitionValidator{
validateSelfReferences,
validateCollectionMaterialized,
validateMaterializedHasNoPolicy,
validateCollectionFieldDefaultValue,
}

var createValidators = append(
@@ -1018,3 +1019,20 @@ func validateMaterializedHasNoPolicy(

return nil
}

func validateCollectionFieldDefaultValue(
ctx context.Context,
db *db,
newState *definitionState,
oldState *definitionState,
) error {
for name, col := range newState.definitionsByName {
// default values are set when a doc is first created
_, err := client.NewDocFromMap(map[string]any{}, col)
if err != nil {
return NewErrDefaultFieldValueInvalid(name, err)
}
}

return nil
}
9 changes: 9 additions & 0 deletions internal/db/errors.go
Original file line number Diff line number Diff line change
@@ -105,6 +105,7 @@ const (
errSelfReferenceWithoutSelf string = "must specify 'Self' kind for self referencing relations"
errColNotMaterialized string = "non-materialized collections are not supported"
errMaterializedViewAndACPNotSupported string = "materialized views do not support ACP"
errInvalidDefaultFieldValue string = "default field value is invalid"
)

var (
@@ -681,3 +682,11 @@ func NewErrMaterializedViewAndACPNotSupported(collection string) error {
errors.NewKV("Collection", collection),
)
}

func NewErrDefaultFieldValueInvalid(collection string, inner error) error {
return errors.New(
errInvalidDefaultFieldValue,
errors.NewKV("Collection", collection),
errors.NewKV("Inner", inner),
)
}
44 changes: 28 additions & 16 deletions internal/request/graphql/schema/collection.go
Original file line number Diff line number Diff line change
@@ -427,23 +427,35 @@ func defaultFromAST(
if !ok {
return nil, NewErrDefaultValueNotAllowed(field.Name.Value, astNamed.Name.Value)
}
if len(directive.Arguments) != 1 {
return nil, NewErrDefaultValueOneArg(field.Name.Value)
}
arg := directive.Arguments[0]
if propName != arg.Name.Value {
return nil, NewErrDefaultValueType(field.Name.Value, propName, arg.Name.Value)
}
var value any
for _, arg := range directive.Arguments {
if propName != arg.Name.Value {
return nil, NewErrDefaultValueInvalid(field.Name.Value, propName, arg.Name.Value)
}
switch t := arg.Value.(type) {
case *ast.IntValue:
value = gql.Int.ParseLiteral(arg.Value, nil)
case *ast.FloatValue:
value = gql.Float.ParseLiteral(arg.Value, nil)
case *ast.BooleanValue:
value = t.Value
case *ast.StringValue:
value = t.Value
default:
value = arg.Value.GetValue()
}
switch propName {
case types.DefaultDirectivePropInt:
value = gql.Int.ParseLiteral(arg.Value, nil)
case types.DefaultDirectivePropFloat:
value = gql.Float.ParseLiteral(arg.Value, nil)
case types.DefaultDirectivePropBool:
value = gql.Boolean.ParseLiteral(arg.Value, nil)
case types.DefaultDirectivePropString:
value = gql.String.ParseLiteral(arg.Value, nil)
case types.DefaultDirectivePropDateTime:
value = gql.DateTime.ParseLiteral(arg.Value, nil)
case types.DefaultDirectivePropJSON:
value = types.JSONScalarType().ParseLiteral(arg.Value, nil)
case types.DefaultDirectivePropBlob:
value = types.BlobScalarType().ParseLiteral(arg.Value, nil)
}
// If the value is nil, then parsing has failed, or a nil value was provided.
// Since setting a default value to nil is the same as not providing one,
// it is safer to return an error to let the user know something is wrong.
if value == nil {
return nil, NewErrDefaultValueInvalid(field.Name.Value, propName)
}
return value, nil
}
21 changes: 19 additions & 2 deletions internal/request/graphql/schema/errors.go
Original file line number Diff line number Diff line change
@@ -30,8 +30,10 @@ const (
errPolicyUnknownArgument string = "policy with unknown argument"
errPolicyInvalidIDProp string = "policy directive with invalid id property"
errPolicyInvalidResourceProp string = "policy directive with invalid resource property"
errDefaultValueInvalid string = "default value type must match field type"
errDefaultValueType string = "default value type must match field type"
errDefaultValueNotAllowed string = "default value is not allowed for this field type"
errDefaultValueInvalid string = "default value is invalid"
errDefaultValueOneArg string = "default value must specify one argument"
errFieldTypeNotSpecified string = "field type not specified"
)

@@ -141,9 +143,24 @@ func NewErrRelationNotFound(relationName string) error {
)
}

func NewErrDefaultValueInvalid(name string, expected string, actual string) error {
func NewErrDefaultValueOneArg(field string) error {
return errors.New(
errDefaultValueOneArg,
errors.NewKV("Field", field),
)
}

func NewErrDefaultValueInvalid(field string, arg string) error {
return errors.New(
errDefaultValueInvalid,
errors.NewKV("Field", field),
errors.NewKV("Arg", arg),
)
}

func NewErrDefaultValueType(name string, expected string, actual string) error {
return errors.New(
errDefaultValueType,
errors.NewKV("Name", name),
errors.NewKV("Expected", expected),
errors.NewKV("Actual", actual),
Original file line number Diff line number Diff line change
@@ -58,7 +58,7 @@ func TestCollectionDescription_WithDefaultFieldValues(t *testing.T) {
{
ID: 3,
Name: "created",
DefaultValue: "2000-07-23T03:00:00-00:00",
DefaultValue: "2000-07-23T03:00:00Z",
},
{
ID: 4,
@@ -90,6 +90,23 @@ func TestCollectionDescription_WithDefaultFieldValues(t *testing.T) {
testUtils.ExecuteTestCase(t, test)
}

func TestCollectionDescription_WithInvalidDefaultFieldValueType_ReturnsError(t *testing.T) {
test := testUtils.TestCase{
Actions: []any{
testUtils.SchemaUpdate{
Schema: `
type Users {
active: Boolean @default(bool: invalid)
}
`,
ExpectedError: "default value is invalid. Field: active, Arg: bool",
},
},
}

testUtils.ExecuteTestCase(t, test)
}

func TestCollectionDescription_WithIncorrectDefaultFieldValueType_ReturnsError(t *testing.T) {
test := testUtils.TestCase{
Actions: []any{
@@ -116,7 +133,7 @@ func TestCollectionDescription_WithMultipleDefaultFieldValueTypes_ReturnsError(t
name: String @default(string: "Bob", int: 10, bool: true, float: 10)
}
`,
ExpectedError: "default value type must match field type. Name: name, Expected: string, Actual: int",
ExpectedError: "default value must specify one argument. Field: name",
},
},
}
Loading