diff --git a/docs/data_format_changes/i3137-default-value-fix.md b/docs/data_format_changes/i3137-default-value-fix.md new file mode 100644 index 0000000000..038caf8425 --- /dev/null +++ b/docs/data_format_changes/i3137-default-value-fix.md @@ -0,0 +1,3 @@ +# Default Value Fix + +Default value parsing has changed slightly and causes the change detector to fail. diff --git a/internal/db/definition_validation.go b/internal/db/definition_validation.go index 2d178f1e07..7afef5dd13 100644 --- a/internal/db/definition_validation.go +++ b/internal/db/definition_validation.go @@ -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 +} diff --git a/internal/db/errors.go b/internal/db/errors.go index d210860501..bd38cf052e 100644 --- a/internal/db/errors.go +++ b/internal/db/errors.go @@ -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), + ) +} diff --git a/internal/request/graphql/schema/collection.go b/internal/request/graphql/schema/collection.go index 81d5182366..df450fb986 100644 --- a/internal/request/graphql/schema/collection.go +++ b/internal/request/graphql/schema/collection.go @@ -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 } diff --git a/internal/request/graphql/schema/errors.go b/internal/request/graphql/schema/errors.go index 41f17bf373..a19a940ebf 100644 --- a/internal/request/graphql/schema/errors.go +++ b/internal/request/graphql/schema/errors.go @@ -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), diff --git a/tests/integration/collection_description/with_default_fields_test.go b/tests/integration/collection_description/with_default_fields_test.go index 4a0f86af77..dca776532d 100644 --- a/tests/integration/collection_description/with_default_fields_test.go +++ b/tests/integration/collection_description/with_default_fields_test.go @@ -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", }, }, }