filter enhancements

This commit is contained in:
Gani Georgiev
2023-01-07 22:25:56 +02:00
parent d5775ff657
commit 9b880f5ab4
102 changed files with 3693 additions and 986 deletions
+73 -19
View File
@@ -15,22 +15,36 @@ import (
var schemaFieldNameRegex = regexp.MustCompile(`^\w+$`)
// field value modifiers
const (
FieldValueModifierAdd string = "+"
FieldValueModifierSubtract string = "-"
)
// FieldValueModifiers returns a list with all available field modifier tokens.
func FieldValueModifiers() []string {
return []string{
FieldValueModifierAdd,
FieldValueModifierSubtract,
}
}
// commonly used field names
const (
FieldNameId = "id"
FieldNameCreated = "created"
FieldNameUpdated = "updated"
FieldNameCollectionId = "collectionId"
FieldNameCollectionName = "collectionName"
FieldNameExpand = "expand"
FieldNameUsername = "username"
FieldNameEmail = "email"
FieldNameEmailVisibility = "emailVisibility"
FieldNameVerified = "verified"
FieldNameTokenKey = "tokenKey"
FieldNamePasswordHash = "passwordHash"
FieldNameLastResetSentAt = "lastResetSentAt"
FieldNameLastVerificationSentAt = "lastVerificationSentAt"
FieldNameId string = "id"
FieldNameCreated string = "created"
FieldNameUpdated string = "updated"
FieldNameCollectionId string = "collectionId"
FieldNameCollectionName string = "collectionName"
FieldNameExpand string = "expand"
FieldNameUsername string = "username"
FieldNameEmail string = "email"
FieldNameEmailVisibility string = "emailVisibility"
FieldNameVerified string = "verified"
FieldNameTokenKey string = "tokenKey"
FieldNamePasswordHash string = "passwordHash"
FieldNameLastResetSentAt string = "lastResetSentAt"
FieldNameLastVerificationSentAt string = "lastVerificationSentAt"
)
// BaseModelFieldNames returns the field names that all models have (id, created, updated).
@@ -168,8 +182,8 @@ func (f SchemaField) Validate() error {
f.InitOptions()
excludeNames := BaseModelFieldNames()
// exclude filter literals
excludeNames = append(excludeNames, "null", "true", "false")
// exclude special filter literals
excludeNames = append(excludeNames, "null", "true", "false", "isset")
// exclude system literals
excludeNames = append(excludeNames, SystemFieldNames()...)
@@ -276,7 +290,7 @@ func (f *SchemaField) PrepareValue(value any) any {
options, _ := f.Options.(*SelectOptions)
if options.MaxSelect <= 1 {
if len(val) > 0 {
return val[0]
return val[len(val)-1] // the last selected
}
return ""
}
@@ -288,7 +302,7 @@ func (f *SchemaField) PrepareValue(value any) any {
options, _ := f.Options.(*FileOptions)
if options.MaxSelect <= 1 {
if len(val) > 0 {
return val[0]
return val[len(val)-1] // the last selected
}
return ""
}
@@ -300,7 +314,7 @@ func (f *SchemaField) PrepareValue(value any) any {
options, _ := f.Options.(*RelationOptions)
if options.MaxSelect != nil && *options.MaxSelect <= 1 {
if len(ids) > 0 {
return ids[0]
return ids[len(ids)-1] // the last selected
}
return ""
}
@@ -311,6 +325,46 @@ func (f *SchemaField) PrepareValue(value any) any {
}
}
// PrepareValueWithModifier returns normalized and properly formatted field value
// by "merging" baseValue with the modifierValue based on the specified modifier (+ or -).
func (f *SchemaField) PrepareValueWithModifier(baseValue any, modifier string, modifierValue any) any {
resolvedValue := baseValue
switch f.Type {
case FieldTypeNumber:
switch modifier {
case FieldValueModifierAdd:
resolvedValue = cast.ToFloat64(baseValue) + cast.ToFloat64(modifierValue)
case FieldValueModifierSubtract:
resolvedValue = cast.ToFloat64(baseValue) - cast.ToFloat64(modifierValue)
}
case FieldTypeSelect, FieldTypeRelation:
switch modifier {
case FieldValueModifierAdd:
resolvedValue = append(
list.ToUniqueStringSlice(baseValue),
list.ToUniqueStringSlice(modifierValue)...,
)
case FieldValueModifierSubtract:
resolvedValue = list.SubtractSlice(
list.ToUniqueStringSlice(baseValue),
list.ToUniqueStringSlice(modifierValue),
)
}
case FieldTypeFile:
// note: file for now supports only the subtract modifier
switch modifier {
case FieldValueModifierSubtract:
resolvedValue = list.SubtractSlice(
list.ToUniqueStringSlice(baseValue),
list.ToUniqueStringSlice(modifierValue),
)
}
}
return f.PrepareValue(resolvedValue)
}
// -------------------------------------------------------------------
// FieldOptions interfaces that defines common methods that every field options struct has.
+694 -4
View File
@@ -603,7 +603,7 @@ func TestSchemaFieldPrepareValue(t *testing.T) {
{schema.SchemaField{Type: schema.FieldTypeSelect}, "", `""`},
{schema.SchemaField{Type: schema.FieldTypeSelect}, 123, `"123"`},
{schema.SchemaField{Type: schema.FieldTypeSelect}, "test", `"test"`},
{schema.SchemaField{Type: schema.FieldTypeSelect}, []string{"test1", "test2"}, `"test1"`},
{schema.SchemaField{Type: schema.FieldTypeSelect}, []string{"test1", "test2"}, `"test2"`},
{
// no values validation/filtering
schema.SchemaField{
@@ -680,7 +680,7 @@ func TestSchemaFieldPrepareValue(t *testing.T) {
{schema.SchemaField{Type: schema.FieldTypeFile}, "", `""`},
{schema.SchemaField{Type: schema.FieldTypeFile}, 123, `"123"`},
{schema.SchemaField{Type: schema.FieldTypeFile}, "test", `"test"`},
{schema.SchemaField{Type: schema.FieldTypeFile}, []string{"test1", "test2"}, `"test1"`},
{schema.SchemaField{Type: schema.FieldTypeFile}, []string{"test1", "test2"}, `"test2"`},
// file (multiple)
{
schema.SchemaField{
@@ -785,7 +785,7 @@ func TestSchemaFieldPrepareValue(t *testing.T) {
{
schema.SchemaField{Type: schema.FieldTypeRelation, Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}},
[]string{"1ba88b4f-e9da-42f0-9764-9a55c953e724", "2ba88b4f-e9da-42f0-9764-9a55c953e724"},
`"1ba88b4f-e9da-42f0-9764-9a55c953e724"`,
`"2ba88b4f-e9da-42f0-9764-9a55c953e724"`,
},
// relation (multiple)
{
@@ -863,6 +863,696 @@ func TestSchemaFieldPrepareValue(t *testing.T) {
}
}
func TestSchemaFieldPrepareValueWithModifier(t *testing.T) {
scenarios := []struct {
name string
field schema.SchemaField
baseValue any
modifier string
modifierValue any
expectJson string
}{
// text
{
"text with '+' modifier",
schema.SchemaField{Type: schema.FieldTypeText},
"base",
"+",
"new",
`"base"`,
},
{
"text with '-' modifier",
schema.SchemaField{Type: schema.FieldTypeText},
"base",
"-",
"new",
`"base"`,
},
{
"text with unknown modifier",
schema.SchemaField{Type: schema.FieldTypeText},
"base",
"?",
"new",
`"base"`,
},
{
"text cast check",
schema.SchemaField{Type: schema.FieldTypeText},
123,
"?",
"new",
`"123"`,
},
// number
{
"number with '+' modifier",
schema.SchemaField{Type: schema.FieldTypeNumber},
1,
"+",
4,
`5`,
},
{
"number with '-' modifier",
schema.SchemaField{Type: schema.FieldTypeNumber},
1,
"-",
4,
`-3`,
},
{
"number with unknown modifier",
schema.SchemaField{Type: schema.FieldTypeNumber},
"1",
"?",
4,
`1`,
},
{
"number cast check",
schema.SchemaField{Type: schema.FieldTypeNumber},
"test",
"+",
"4",
`4`,
},
// bool
{
"bool with '+' modifier",
schema.SchemaField{Type: schema.FieldTypeBool},
true,
"+",
false,
`true`,
},
{
"bool with '-' modifier",
schema.SchemaField{Type: schema.FieldTypeBool},
true,
"-",
false,
`true`,
},
{
"bool with unknown modifier",
schema.SchemaField{Type: schema.FieldTypeBool},
true,
"?",
false,
`true`,
},
{
"bool cast check",
schema.SchemaField{Type: schema.FieldTypeBool},
"true",
"?",
false,
`true`,
},
// email
{
"email with '+' modifier",
schema.SchemaField{Type: schema.FieldTypeEmail},
"base",
"+",
"new",
`"base"`,
},
{
"email with '-' modifier",
schema.SchemaField{Type: schema.FieldTypeEmail},
"base",
"-",
"new",
`"base"`,
},
{
"email with unknown modifier",
schema.SchemaField{Type: schema.FieldTypeEmail},
"base",
"?",
"new",
`"base"`,
},
{
"email cast check",
schema.SchemaField{Type: schema.FieldTypeEmail},
123,
"?",
"new",
`"123"`,
},
// url
{
"url with '+' modifier",
schema.SchemaField{Type: schema.FieldTypeUrl},
"base",
"+",
"new",
`"base"`,
},
{
"url with '-' modifier",
schema.SchemaField{Type: schema.FieldTypeUrl},
"base",
"-",
"new",
`"base"`,
},
{
"url with unknown modifier",
schema.SchemaField{Type: schema.FieldTypeUrl},
"base",
"?",
"new",
`"base"`,
},
{
"url cast check",
schema.SchemaField{Type: schema.FieldTypeUrl},
123,
"-",
"new",
`"123"`,
},
// date
{
"date with '+' modifier",
schema.SchemaField{Type: schema.FieldTypeDate},
"2023-01-01 00:00:00.123",
"+",
"2023-02-01 00:00:00.456",
`"2023-01-01 00:00:00.123Z"`,
},
{
"date with '-' modifier",
schema.SchemaField{Type: schema.FieldTypeDate},
"2023-01-01 00:00:00.123Z",
"-",
"2023-02-01 00:00:00.456Z",
`"2023-01-01 00:00:00.123Z"`,
},
{
"date with unknown modifier",
schema.SchemaField{Type: schema.FieldTypeDate},
"2023-01-01 00:00:00.123",
"?",
"2023-01-01 00:00:00.456",
`"2023-01-01 00:00:00.123Z"`,
},
{
"date cast check",
schema.SchemaField{Type: schema.FieldTypeDate},
1672524000, // 2022-12-31 22:00:00.000Z
"+",
100,
`"2022-12-31 22:00:00.000Z"`,
},
// json
{
"json with '+' modifier",
schema.SchemaField{Type: schema.FieldTypeJson},
10,
"+",
5,
`10`,
},
{
"json with '+' modifier (slice)",
schema.SchemaField{Type: schema.FieldTypeJson},
[]string{"a", "b"},
"+",
"c",
`["a","b"]`,
},
{
"json with '-' modifier",
schema.SchemaField{Type: schema.FieldTypeJson},
10,
"-",
5,
`10`,
},
{
"json with '-' modifier (slice)",
schema.SchemaField{Type: schema.FieldTypeJson},
`["a","b"]`,
"-",
"c",
`["a","b"]`,
},
{
"json with unknown modifier",
schema.SchemaField{Type: schema.FieldTypeJson},
`"base"`,
"?",
`"new"`,
`"base"`,
},
// single select
{
"single select with '+' modifier (empty base)",
schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 1}},
"",
"+",
"b",
`"b"`,
},
{
"single select with '+' modifier (nonempty base)",
schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 1}},
"a",
"+",
"b",
`"b"`,
},
{
"single select with '-' modifier (empty base)",
schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 1}},
"",
"-",
"a",
`""`,
},
{
"single select with '-' modifier (nonempty base and empty modifier value)",
schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 1}},
"a",
"-",
"",
`"a"`,
},
{
"single select with '-' modifier (nonempty base and different value)",
schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 1}},
"a",
"-",
"b",
`"a"`,
},
{
"single select with '-' modifier (nonempty base and matching value)",
schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 1}},
"a",
"-",
"a",
`""`,
},
{
"single select with '-' modifier (nonempty base and matching value in a slice)",
schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 1}},
"a",
"-",
[]string{"b", "a", "c", "123"},
`""`,
},
{
"single select with unknown modifier (nonempty)",
schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 1}},
"",
"?",
"a",
`""`,
},
// multi select
{
"multi select with '+' modifier (empty base)",
schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 10}},
nil,
"+",
"b",
`["b"]`,
},
{
"multi select with '+' modifier (nonempty base)",
schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 10}},
[]string{"a"},
"+",
[]string{"b", "c"},
`["a","b","c"]`,
},
{
"multi select with '+' modifier (nonempty base; already existing value)",
schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 10}},
[]string{"a", "b"},
"+",
"b",
`["a","b"]`,
},
{
"multi select with '-' modifier (empty base)",
schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 10}},
nil,
"-",
[]string{"a"},
`[]`,
},
{
"multi select with '-' modifier (nonempty base and empty modifier value)",
schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 10}},
"a",
"-",
"",
`["a"]`,
},
{
"multi select with '-' modifier (nonempty base and different value)",
schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 10}},
"a",
"-",
"b",
`["a"]`,
},
{
"multi select with '-' modifier (nonempty base and matching value)",
schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 10}},
[]string{"a", "b", "c", "d"},
"-",
"c",
`["a","b","d"]`,
},
{
"multi select with '-' modifier (nonempty base and matching value in a slice)",
schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 10}},
[]string{"a", "b", "c", "d"},
"-",
[]string{"b", "a", "123"},
`["c","d"]`,
},
{
"multi select with unknown modifier (nonempty)",
schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 10}},
[]string{"a", "b"},
"?",
"a",
`["a","b"]`,
},
// single relation
{
"single relation with '+' modifier (empty base)",
schema.SchemaField{Type: schema.FieldTypeRelation, Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}},
"",
"+",
"b",
`"b"`,
},
{
"single relation with '+' modifier (nonempty base)",
schema.SchemaField{Type: schema.FieldTypeRelation, Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}},
"a",
"+",
"b",
`"b"`,
},
{
"single relation with '-' modifier (empty base)",
schema.SchemaField{Type: schema.FieldTypeRelation, Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}},
"",
"-",
"a",
`""`,
},
{
"single relation with '-' modifier (nonempty base and empty modifier value)",
schema.SchemaField{Type: schema.FieldTypeRelation, Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}},
"a",
"-",
"",
`"a"`,
},
{
"single relation with '-' modifier (nonempty base and different value)",
schema.SchemaField{Type: schema.FieldTypeRelation, Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}},
"a",
"-",
"b",
`"a"`,
},
{
"single relation with '-' modifier (nonempty base and matching value)",
schema.SchemaField{Type: schema.FieldTypeRelation, Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}},
"a",
"-",
"a",
`""`,
},
{
"single relation with '-' modifier (nonempty base and matching value in a slice)",
schema.SchemaField{Type: schema.FieldTypeRelation, Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}},
"a",
"-",
[]string{"b", "a", "c", "123"},
`""`,
},
{
"single relation with unknown modifier (nonempty)",
schema.SchemaField{Type: schema.FieldTypeRelation, Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}},
"",
"?",
"a",
`""`,
},
// multi relation
{
"multi relation with '+' modifier (empty base)",
schema.SchemaField{Type: schema.FieldTypeRelation},
nil,
"+",
"b",
`["b"]`,
},
{
"multi relation with '+' modifier (nonempty base)",
schema.SchemaField{Type: schema.FieldTypeRelation},
[]string{"a"},
"+",
[]string{"b", "c"},
`["a","b","c"]`,
},
{
"multi relation with '+' modifier (nonempty base; already existing value)",
schema.SchemaField{Type: schema.FieldTypeRelation},
[]string{"a", "b"},
"+",
"b",
`["a","b"]`,
},
{
"multi relation with '-' modifier (empty base)",
schema.SchemaField{Type: schema.FieldTypeRelation},
nil,
"-",
[]string{"a"},
`[]`,
},
{
"multi relation with '-' modifier (nonempty base and empty modifier value)",
schema.SchemaField{Type: schema.FieldTypeRelation},
"a",
"-",
"",
`["a"]`,
},
{
"multi relation with '-' modifier (nonempty base and different value)",
schema.SchemaField{Type: schema.FieldTypeRelation},
"a",
"-",
"b",
`["a"]`,
},
{
"multi relation with '-' modifier (nonempty base and matching value)",
schema.SchemaField{Type: schema.FieldTypeRelation},
[]string{"a", "b", "c", "d"},
"-",
"c",
`["a","b","d"]`,
},
{
"multi relation with '-' modifier (nonempty base and matching value in a slice)",
schema.SchemaField{Type: schema.FieldTypeRelation},
[]string{"a", "b", "c", "d"},
"-",
[]string{"b", "a", "123"},
`["c","d"]`,
},
{
"multi relation with unknown modifier (nonempty)",
schema.SchemaField{Type: schema.FieldTypeRelation},
[]string{"a", "b"},
"?",
"a",
`["a","b"]`,
},
// single file
{
"single file with '+' modifier (empty base)",
schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 1}},
"",
"+",
"b",
`""`,
},
{
"single file with '+' modifier (nonempty base)",
schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 1}},
"a",
"+",
"b",
`"a"`,
},
{
"single file with '-' modifier (empty base)",
schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 1}},
"",
"-",
"a",
`""`,
},
{
"single file with '-' modifier (nonempty base and empty modifier value)",
schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 1}},
"a",
"-",
"",
`"a"`,
},
{
"single file with '-' modifier (nonempty base and different value)",
schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 1}},
"a",
"-",
"b",
`"a"`,
},
{
"single file with '-' modifier (nonempty base and matching value)",
schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 1}},
"a",
"-",
"a",
`""`,
},
{
"single file with '-' modifier (nonempty base and matching value in a slice)",
schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 1}},
"a",
"-",
[]string{"b", "a", "c", "123"},
`""`,
},
{
"single file with unknown modifier (nonempty)",
schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 1}},
"",
"?",
"a",
`""`,
},
// multi file
{
"multi file with '+' modifier (empty base)",
schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 10}},
nil,
"+",
"b",
`[]`,
},
{
"multi file with '+' modifier (nonempty base)",
schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 10}},
[]string{"a"},
"+",
[]string{"b", "c"},
`["a"]`,
},
{
"multi file with '+' modifier (nonempty base; already existing value)",
schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 10}},
[]string{"a", "b"},
"+",
"b",
`["a","b"]`,
},
{
"multi file with '-' modifier (empty base)",
schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 10}},
nil,
"-",
[]string{"a"},
`[]`,
},
{
"multi file with '-' modifier (nonempty base and empty modifier value)",
schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 10}},
"a",
"-",
"",
`["a"]`,
},
{
"multi file with '-' modifier (nonempty base and different value)",
schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 10}},
"a",
"-",
"b",
`["a"]`,
},
{
"multi file with '-' modifier (nonempty base and matching value)",
schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 10}},
[]string{"a", "b", "c", "d"},
"-",
"c",
`["a","b","d"]`,
},
{
"multi file with '-' modifier (nonempty base and matching value in a slice)",
schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 10}},
[]string{"a", "b", "c", "d"},
"-",
[]string{"b", "a", "123"},
`["c","d"]`,
},
{
"multi file with unknown modifier (nonempty)",
schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 10}},
[]string{"a", "b"},
"?",
"a",
`["a","b"]`,
},
}
for _, s := range scenarios {
result := s.field.PrepareValueWithModifier(s.baseValue, s.modifier, s.modifierValue)
encoded, err := json.Marshal(result)
if err != nil {
t.Fatalf("[%s] %v", s.name, err)
}
if string(encoded) != s.expectJson {
t.Fatalf("[%s], Expected %v, got %v", s.name, s.expectJson, string(encoded))
}
}
}
// -------------------------------------------------------------------
type fieldOptionsScenario struct {
@@ -1306,7 +1996,7 @@ func TestRelationOptionsValidate(t *testing.T) {
[]string{"maxSelect"},
},
{
"MaxSelect > 0 && non-empty CollectionId",
"MaxSelect > 0 && nonempty CollectionId",
schema.RelationOptions{
CollectionId: "abc",
MaxSelect: types.Pointer(1),