Skip to content

Commit

Permalink
feat: Default scalar field values (#2997)
Browse files Browse the repository at this point in the history
## Relevant issue(s)

Resolves #2952

## Description

This PR adds support for default field values using a new `@default`
directive.

## 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?

Added integration tests

Specify the platform(s) on which this was tested:
- MacOS
  • Loading branch information
nasdf authored Sep 16, 2024
1 parent f5567f5 commit ea3a74f
Show file tree
Hide file tree
Showing 16 changed files with 715 additions and 26 deletions.
7 changes: 7 additions & 0 deletions client/collection_field_description.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ type CollectionFieldDescription struct {
//
// Otherwise will be [None].
RelationName immutable.Option[string]

// DefaultValue contains the default value for this field.
//
// This value has no effect on views.
DefaultValue any
}

func (f FieldID) String() string {
Expand All @@ -50,6 +55,7 @@ type collectionFieldDescription struct {
Name string
ID FieldID
RelationName immutable.Option[string]
DefaultValue any

// Properties below this line are unmarshalled using custom logic in [UnmarshalJSON]
Kind json.RawMessage
Expand All @@ -64,6 +70,7 @@ func (f *CollectionFieldDescription) UnmarshalJSON(bytes []byte) error {

f.Name = descMap.Name
f.ID = descMap.ID
f.DefaultValue = descMap.DefaultValue
f.RelationName = descMap.RelationName
kind, err := parseFieldKind(descMap.Kind)
if err != nil {
Expand Down
5 changes: 5 additions & 0 deletions client/definitions.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@ type FieldDefinition struct {

// If true, this is the primary half of a relation, otherwise is false.
IsPrimaryRelation bool

// DefaultValue contains the default value for this field.
DefaultValue any
}

// NewFieldDefinition returns a new [FieldDefinition], combining the given local and global elements
Expand All @@ -164,6 +167,7 @@ func NewFieldDefinition(local CollectionFieldDescription, global SchemaFieldDesc
RelationName: local.RelationName.Value(),
Typ: global.Typ,
IsPrimaryRelation: kind.IsObject() && !kind.IsArray(),
DefaultValue: local.DefaultValue,
}
}

Expand All @@ -174,6 +178,7 @@ func NewLocalFieldDefinition(local CollectionFieldDescription) FieldDefinition {
ID: local.ID,
Kind: local.Kind.Value(),
RelationName: local.RelationName.Value(),
DefaultValue: local.DefaultValue,
}
}

Expand Down
48 changes: 38 additions & 10 deletions client/document.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,25 +84,34 @@ type Document struct {
collectionDefinition CollectionDefinition
}

func newEmptyDoc(collectionDefinition CollectionDefinition) *Document {
return &Document{
func newEmptyDoc(collectionDefinition CollectionDefinition) (*Document, error) {
doc := &Document{
fields: make(map[string]Field),
values: make(map[Field]*FieldValue),
collectionDefinition: collectionDefinition,
}
if err := doc.setDefaultValues(); err != nil {
return nil, err
}
return doc, nil
}

// NewDocWithID creates a new Document with a specified key.
func NewDocWithID(docID DocID, collectionDefinition CollectionDefinition) *Document {
doc := newEmptyDoc(collectionDefinition)
func NewDocWithID(docID DocID, collectionDefinition CollectionDefinition) (*Document, error) {
doc, err := newEmptyDoc(collectionDefinition)
if err != nil {
return nil, err
}
doc.id = docID
return doc
return doc, nil
}

// NewDocFromMap creates a new Document from a data map.
func NewDocFromMap(data map[string]any, collectionDefinition CollectionDefinition) (*Document, error) {
var err error
doc := newEmptyDoc(collectionDefinition)
doc, err := newEmptyDoc(collectionDefinition)
if err != nil {
return nil, err
}

// check if document contains special _docID field
k, hasDocID := data[request.DocIDFieldName]
Expand Down Expand Up @@ -142,8 +151,11 @@ func IsJSONArray(obj []byte) bool {

// NewFromJSON creates a new instance of a Document from a raw JSON object byte array.
func NewDocFromJSON(obj []byte, collectionDefinition CollectionDefinition) (*Document, error) {
doc := newEmptyDoc(collectionDefinition)
err := doc.SetWithJSON(obj)
doc, err := newEmptyDoc(collectionDefinition)
if err != nil {
return nil, err
}
err = doc.SetWithJSON(obj)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -172,7 +184,10 @@ func NewDocsFromJSON(obj []byte, collectionDefinition CollectionDefinition) ([]*
if err != nil {
return nil, err
}
doc := newEmptyDoc(collectionDefinition)
doc, err := newEmptyDoc(collectionDefinition)
if err != nil {
return nil, err
}
err = doc.setWithFastJSONObject(o)
if err != nil {
return nil, err
Expand Down Expand Up @@ -653,6 +668,19 @@ func (doc *Document) setAndParseObjectType(value map[string]any) error {
return nil
}

func (doc *Document) setDefaultValues() error {
for _, field := range doc.collectionDefinition.GetFields() {
if field.DefaultValue == nil {
continue // no default value to set
}
err := doc.Set(field.Name, field.DefaultValue)
if err != nil {
return err
}
}
return nil
}

// Fields gets the document fields as a map.
func (doc *Document) Fields() map[string]Field {
doc.mu.RLock()
Expand Down
2 changes: 2 additions & 0 deletions docs/website/references/http/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
"Fields": {
"items": {
"properties": {
"DefaultValue": {},
"ID": {
"maximum": 4294967295,
"minimum": 0,
Expand Down Expand Up @@ -160,6 +161,7 @@
"Fields": {
"items": {
"properties": {
"DefaultValue": {},
"ID": {
"maximum": 4294967295,
"minimum": 0,
Expand Down
5 changes: 4 additions & 1 deletion http/client_collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,10 @@ func (c *Collection) Get(
if err != nil {
return nil, err
}
doc := client.NewDocWithID(docID, c.def)
doc, err := client.NewDocWithID(docID, c.def)
if err != nil {
return nil, err
}
err = doc.SetWithJSON(data)
if err != nil {
return nil, err
Expand Down
5 changes: 4 additions & 1 deletion internal/db/fetcher/encoded_doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,10 @@ func Decode(encdoc EncodedDocument, collectionDefinition client.CollectionDefini
return nil, err
}

doc := client.NewDocWithID(docID, collectionDefinition)
doc, err := client.NewDocWithID(docID, collectionDefinition)
if err != nil {
return nil, err
}
properties, err := encdoc.Properties(false)
if err != nil {
return nil, err
Expand Down
81 changes: 69 additions & 12 deletions internal/request/graphql/schema/collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"sort"
"strings"

gql "github.com/sourcenetwork/graphql-go"
"github.com/sourcenetwork/graphql-go/language/ast"
gqlp "github.com/sourcenetwork/graphql-go/language/parser"
"github.com/sourcenetwork/graphql-go/language/source"
Expand All @@ -26,6 +27,29 @@ import (
"github.com/sourcenetwork/defradb/internal/request/graphql/schema/types"
)

const (
typeID string = "ID"
typeBoolean string = "Boolean"
typeInt string = "Int"
typeFloat string = "Float"
typeDateTime string = "DateTime"
typeString string = "String"
typeBlob string = "Blob"
typeJSON string = "JSON"
)

// this mapping is used to check that the default prop value
// matches the field type
var TypeToDefaultPropName = map[string]string{
typeString: types.DefaultDirectivePropString,
typeBoolean: types.DefaultDirectivePropBool,
typeInt: types.DefaultDirectivePropInt,
typeFloat: types.DefaultDirectivePropFloat,
typeDateTime: types.DefaultDirectivePropDateTime,
typeJSON: types.DefaultDirectivePropJSON,
typeBlob: types.DefaultDirectivePropBlob,
}

// FromString parses a GQL SDL string into a set of collection descriptions.
func FromString(ctx context.Context, schemaString string) (
[]client.CollectionDefinition,
Expand Down Expand Up @@ -369,6 +393,39 @@ func indexFieldFromAST(value ast.Value, defaultDirection *ast.EnumValue) (client
}, nil
}

func defaultFromAST(
field *ast.FieldDefinition,
directive *ast.Directive,
) (any, error) {
astNamed, ok := field.Type.(*ast.Named)
if !ok {
return nil, NewErrDefaultValueNotAllowed(field.Name.Value, field.Type.String())
}
propName, ok := TypeToDefaultPropName[astNamed.Name.Value]
if !ok {
return nil, NewErrDefaultValueNotAllowed(field.Name.Value, astNamed.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)
case *ast.FloatValue:
value = gql.Float.ParseLiteral(arg.Value)
case *ast.BooleanValue:
value = t.Value
case *ast.StringValue:
value = t.Value
default:
value = arg.Value.GetValue()
}
}
return value, nil
}

func fieldsFromAST(
field *ast.FieldDefinition,
hostObjectName string,
Expand All @@ -392,6 +449,16 @@ func fieldsFromAST(
}
hostMap[field.Name.Value] = cType

var defaultValue any
for _, directive := range field.Directives {
if directive.Name.Value == types.DefaultDirectiveLabel {
defaultValue, err = defaultFromAST(field, directive)
if err != nil {
return nil, nil, err
}
}
}

schemaFieldDescriptions := []client.SchemaFieldDescription{}
collectionFieldDescriptions := []client.CollectionFieldDescription{}

Expand Down Expand Up @@ -467,7 +534,8 @@ func fieldsFromAST(
collectionFieldDescriptions = append(
collectionFieldDescriptions,
client.CollectionFieldDescription{
Name: field.Name.Value,
Name: field.Name.Value,
DefaultValue: defaultValue,
},
)
}
Expand Down Expand Up @@ -529,17 +597,6 @@ func setCRDTType(field *ast.FieldDefinition, kind client.FieldKind) (client.CTyp
}

func astTypeToKind(t ast.Type) (client.FieldKind, error) {
const (
typeID string = "ID"
typeBoolean string = "Boolean"
typeInt string = "Int"
typeFloat string = "Float"
typeDateTime string = "DateTime"
typeString string = "String"
typeBlob string = "Blob"
typeJSON string = "JSON"
)

switch astTypeVal := t.(type) {
case *ast.List:
switch innerAstTypeVal := astTypeVal.Type.(type) {
Expand Down
19 changes: 19 additions & 0 deletions internal/request/graphql/schema/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ 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"
errDefaultValueNotAllowed string = "default value is not allowed for this field type"
)

var (
Expand Down Expand Up @@ -136,3 +138,20 @@ func NewErrRelationNotFound(relationName string) error {
errors.NewKV("RelationName", relationName),
)
}

func NewErrDefaultValueInvalid(name string, expected string, actual string) error {
return errors.New(
errDefaultValueInvalid,
errors.NewKV("Name", name),
errors.NewKV("Expected", expected),
errors.NewKV("Actual", actual),
)
}

func NewErrDefaultValueNotAllowed(fieldName, fieldType string) error {
return errors.New(
errDefaultValueNotAllowed,
errors.NewKV("Name", fieldName),
errors.NewKV("Type", fieldType),
)
}
3 changes: 2 additions & 1 deletion internal/request/graphql/schema/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,8 @@ func (g *Generator) buildMutationInputTypes(collections []client.CollectionDefin
}

fields[field.Name] = &gql.InputObjectFieldConfig{
Type: ttype,
Type: ttype,
DefaultValue: field.DefaultValue,
}
}

Expand Down
1 change: 1 addition & 0 deletions internal/request/graphql/schema/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ func defaultDirectivesType(
) []*gql.Directive {
return []*gql.Directive{
schemaTypes.CRDTFieldDirective(crdtEnum),
schemaTypes.DefaultDirective(),
schemaTypes.ExplainDirective(explainEnum),
schemaTypes.PolicyDirective(),
schemaTypes.IndexDirective(orderEnum, indexFieldInput),
Expand Down
Loading

0 comments on commit ea3a74f

Please sign in to comment.