added datetime macros

This commit is contained in:
Gani Georgiev
2023-08-18 08:43:32 +03:00
parent 75f58a28ac
commit 8a916cd636
37 changed files with 303 additions and 117 deletions
+7 -7
View File
@@ -10,7 +10,6 @@ import (
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/tools/security"
"github.com/pocketbase/pocketbase/tools/store"
"github.com/pocketbase/pocketbase/tools/types"
"github.com/spf13/cast"
)
@@ -206,21 +205,22 @@ func buildResolversExpr(
return expr, nil
}
var identifierMacros = map[string]func() string{
"@now": func() string { return types.NowDateTime().String() },
}
func resolveToken(token fexpr.Token, fieldResolver FieldResolver) (*ResolverResult, error) {
switch token.Type {
case fexpr.TokenIdentifier:
// check for macros
// ---
if f, ok := identifierMacros[token.Literal]; ok {
if macroFunc, ok := identifierMacros[token.Literal]; ok {
placeholder := "t" + security.PseudorandomString(5)
macroValue, err := macroFunc()
if err != nil {
return nil, err
}
return &ResolverResult{
Identifier: "{:" + placeholder + "}",
Params: dbx.Params{placeholder: f()},
Params: dbx.Params{placeholder: macroValue},
}, nil
}
+39 -72
View File
@@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"regexp"
"strings"
"testing"
"time"
@@ -12,7 +13,7 @@ import (
)
func TestFilterDataBuildExpr(t *testing.T) {
resolver := search.NewSimpleFieldResolver("test1", "test2", "test3", "test4.sub")
resolver := search.NewSimpleFieldResolver("test1", "test2", "test3", `^test4.\w+$`)
scenarios := []struct {
name string
@@ -48,11 +49,7 @@ func TestFilterDataBuildExpr(t *testing.T) {
"simple expression",
"test1 > 1",
false,
"^" +
regexp.QuoteMeta("[[test1]] > {:") +
".+" +
regexp.QuoteMeta("}") +
"$",
"[[test1]] > {:TEST}",
},
{
"empty string vs null",
@@ -64,113 +61,76 @@ func TestFilterDataBuildExpr(t *testing.T) {
"like with 2 columns",
"test1 ~ test2",
false,
"^" +
regexp.QuoteMeta("[[test1]] LIKE ('%' || [[test2]] || '%') ESCAPE '\\'") +
"$",
"[[test1]] LIKE ('%' || [[test2]] || '%') ESCAPE '\\'",
},
{
"like with right column operand",
"'lorem' ~ test1",
false,
"^" +
regexp.QuoteMeta("{:") +
".+" +
regexp.QuoteMeta("} LIKE ('%' || [[test1]] || '%') ESCAPE '\\'") +
"$",
"{:TEST} LIKE ('%' || [[test1]] || '%') ESCAPE '\\'",
},
{
"like with left column operand and text as right operand",
"test1 ~ 'lorem'",
false,
"^" +
regexp.QuoteMeta("[[test1]] LIKE {:") +
".+" +
regexp.QuoteMeta("} ESCAPE '\\'") +
"$",
"[[test1]] LIKE {:TEST} ESCAPE '\\'",
},
{
"not like with 2 columns",
"test1 !~ test2",
false,
"^" +
regexp.QuoteMeta("[[test1]] NOT LIKE ('%' || [[test2]] || '%') ESCAPE '\\'") +
"$",
"[[test1]] NOT LIKE ('%' || [[test2]] || '%') ESCAPE '\\'",
},
{
"not like with right column operand",
"'lorem' !~ test1",
false,
"^" +
regexp.QuoteMeta("{:") +
".+" +
regexp.QuoteMeta("} NOT LIKE ('%' || [[test1]] || '%') ESCAPE '\\'") +
"$",
"{:TEST} NOT LIKE ('%' || [[test1]] || '%') ESCAPE '\\'",
},
{
"like with left column operand and text as right operand",
"test1 !~ 'lorem'",
false,
"^" +
regexp.QuoteMeta("[[test1]] NOT LIKE {:") +
".+" +
regexp.QuoteMeta("} ESCAPE '\\'") +
"$",
"[[test1]] NOT LIKE {:TEST} ESCAPE '\\'",
},
{
"current datetime constant",
"test1 > @now",
"macros",
`
test4.1 > @now &&
test4.2 > @second &&
test4.3 > @minute &&
test4.4 > @hour &&
test4.5 > @day &&
test4.6 > @year &&
test4.7 > @month &&
test4.9 > @weekday &&
test4.9 > @todayStart &&
test4.10 > @todayEnd &&
test4.11 > @monthStart &&
test4.12 > @monthEnd &&
test4.13 > @yearStart &&
test4.14 > @yearEnd
`,
false,
"^" +
regexp.QuoteMeta("[[test1]] > {:") +
".+" +
regexp.QuoteMeta("}") +
"$",
"([[test4.1]] > {:TEST} AND [[test4.2]] > {:TEST} AND [[test4.3]] > {:TEST} AND [[test4.4]] > {:TEST} AND [[test4.5]] > {:TEST} AND [[test4.6]] > {:TEST} AND [[test4.7]] > {:TEST} AND [[test4.9]] > {:TEST} AND [[test4.9]] > {:TEST} AND [[test4.10]] > {:TEST} AND [[test4.11]] > {:TEST} AND [[test4.12]] > {:TEST} AND [[test4.13]] > {:TEST} AND [[test4.14]] > {:TEST})",
},
{
"complex expression",
"((test1 > 1) || (test2 != 2)) && test3 ~ '%%example' && test4.sub = null",
false,
"^" +
regexp.QuoteMeta("(([[test1]] > {:") +
".+" +
regexp.QuoteMeta("} OR [[test2]] != {:") +
".+" +
regexp.QuoteMeta("}) AND [[test3]] LIKE {:") +
".+" +
regexp.QuoteMeta("} ESCAPE '\\' AND ([[test4.sub]] = '' OR [[test4.sub]] IS NULL))") +
"$",
"(([[test1]] > {:TEST} OR [[test2]] != {:TEST}) AND [[test3]] LIKE {:TEST} ESCAPE '\\' AND ([[test4.sub]] = '' OR [[test4.sub]] IS NULL))",
},
{
"combination of special literals (null, true, false)",
"test1=true && test2 != false && null = test3 || null != test4.sub",
false,
"^" + regexp.QuoteMeta("([[test1]] = 1 AND [[test2]] != 0 AND ('' = [[test3]] OR [[test3]] IS NULL) OR ('' != [[test4.sub]] AND [[test4.sub]] IS NOT NULL))") + "$",
"([[test1]] = 1 AND [[test2]] != 0 AND ('' = [[test3]] OR [[test3]] IS NULL) OR ('' != [[test4.sub]] AND [[test4.sub]] IS NOT NULL))",
},
{
"all operators",
"(test1 = test2 || test2 != test3) && (test2 ~ 'example' || test2 !~ '%%abc') && 'switch1%%' ~ test1 && 'switch2' !~ test2 && test3 > 1 && test3 >= 0 && test3 <= 4 && 2 < 5",
false,
"^" +
regexp.QuoteMeta("((COALESCE([[test1]], '') = COALESCE([[test2]], '') OR COALESCE([[test2]], '') != COALESCE([[test3]], '')) AND ([[test2]] LIKE {:") +
".+" +
regexp.QuoteMeta("} ESCAPE '\\' OR [[test2]] NOT LIKE {:") +
".+" +
regexp.QuoteMeta("} ESCAPE '\\') AND {:") +
".+" +
regexp.QuoteMeta("} LIKE ('%' || [[test1]] || '%') ESCAPE '\\' AND {:") +
".+" +
regexp.QuoteMeta("} NOT LIKE ('%' || [[test2]] || '%') ESCAPE '\\' AND [[test3]] > {:") +
".+" +
regexp.QuoteMeta("} AND [[test3]] >= {:") +
".+" +
regexp.QuoteMeta("} AND [[test3]] <= {:") +
".+" +
regexp.QuoteMeta("} AND {:") +
".+" +
regexp.QuoteMeta("} < {:") +
".+" +
regexp.QuoteMeta("})") +
"$",
"((COALESCE([[test1]], '') = COALESCE([[test2]], '') OR COALESCE([[test2]], '') != COALESCE([[test3]], '')) AND ([[test2]] LIKE {:TEST} ESCAPE '\\' OR [[test2]] NOT LIKE {:TEST} ESCAPE '\\') AND {:TEST} LIKE ('%' || [[test1]] || '%') ESCAPE '\\' AND {:TEST} NOT LIKE ('%' || [[test2]] || '%') ESCAPE '\\' AND [[test3]] > {:TEST} AND [[test3]] >= {:TEST} AND [[test3]] <= {:TEST} AND {:TEST} < {:TEST})",
},
}
@@ -191,9 +151,16 @@ func TestFilterDataBuildExpr(t *testing.T) {
rawSql := expr.Build(dummyDB, dbx.Params{})
pattern := regexp.MustCompile(s.expectPattern)
// replace TEST placeholder with .+ regex pattern
expectPattern := strings.ReplaceAll(
"^"+regexp.QuoteMeta(s.expectPattern)+"$",
"TEST",
`\w+`,
)
pattern := regexp.MustCompile(expectPattern)
if !pattern.MatchString(rawSql) {
t.Fatalf("[%s] Pattern %v don't match with expression: \n%v", s.name, s.expectPattern, rawSql)
t.Fatalf("[%s] Pattern %v don't match with expression: \n%v", s.name, expectPattern, rawSql)
}
})
}
+115
View File
@@ -0,0 +1,115 @@
package search
import (
"fmt"
"time"
"github.com/pocketbase/pocketbase/tools/types"
)
// note: used primarily for the tests
var timeNow = func() time.Time {
return time.Now()
}
var identifierMacros = map[string]func() (any, error){
"@now": func() (any, error) {
today := timeNow().UTC()
d, err := types.ParseDateTime(today)
if err != nil {
return "", fmt.Errorf("@now: %w", err)
}
return d.String(), nil
},
"@second": func() (any, error) {
return int(timeNow().UTC().Second()), nil
},
"@minute": func() (any, error) {
return int(timeNow().UTC().Minute()), nil
},
"@hour": func() (any, error) {
return int(timeNow().UTC().Hour()), nil
},
"@day": func() (any, error) {
return int(timeNow().UTC().Day()), nil
},
"@month": func() (any, error) {
return int(timeNow().UTC().Month()), nil
},
"@weekday": func() (any, error) {
return int(timeNow().UTC().Weekday()), nil
},
"@year": func() (any, error) {
return int(timeNow().UTC().Year()), nil
},
"@todayStart": func() (any, error) {
today := timeNow().UTC()
start := time.Date(today.Year(), today.Month(), today.Day(), 0, 0, 0, 0, time.UTC)
d, err := types.ParseDateTime(start)
if err != nil {
return "", fmt.Errorf("@todayStart: %w", err)
}
return d.String(), nil
},
"@todayEnd": func() (any, error) {
today := timeNow().UTC()
start := time.Date(today.Year(), today.Month(), today.Day(), 23, 59, 59, 999999999, time.UTC)
d, err := types.ParseDateTime(start)
if err != nil {
return "", fmt.Errorf("@todayEnd: %w", err)
}
return d.String(), nil
},
"@monthStart": func() (any, error) {
today := timeNow().UTC()
start := time.Date(today.Year(), today.Month(), 1, 0, 0, 0, 0, time.UTC)
d, err := types.ParseDateTime(start)
if err != nil {
return "", fmt.Errorf("@monthStart: %w", err)
}
return d.String(), nil
},
"@monthEnd": func() (any, error) {
today := timeNow().UTC()
start := time.Date(today.Year(), today.Month(), 1, 23, 59, 59, 999999999, time.UTC)
end := start.AddDate(0, 1, -1)
d, err := types.ParseDateTime(end)
if err != nil {
return "", fmt.Errorf("@monthEnd: %w", err)
}
return d.String(), nil
},
"@yearStart": func() (any, error) {
today := timeNow().UTC()
start := time.Date(today.Year(), 1, 1, 0, 0, 0, 0, time.UTC)
d, err := types.ParseDateTime(start)
if err != nil {
return "", fmt.Errorf("@yearStart: %w", err)
}
return d.String(), nil
},
"@yearEnd": func() (any, error) {
today := timeNow().UTC()
end := time.Date(today.Year(), 12, 31, 23, 59, 59, 999999999, time.UTC)
d, err := types.ParseDateTime(end)
if err != nil {
return "", fmt.Errorf("@yearEnd: %w", err)
}
return d.String(), nil
},
}
+56
View File
@@ -0,0 +1,56 @@
package search
import (
"testing"
"time"
)
func TestIdentifierMacros(t *testing.T) {
originalTimeNow := timeNow
timeNow = func() time.Time {
return time.Date(2023, 2, 3, 4, 5, 6, 7, time.UTC)
}
testMacros := map[string]any{
"@now": "2023-02-03 04:05:06.000Z",
"@second": 6,
"@minute": 5,
"@hour": 4,
"@day": 3,
"@month": 2,
"@weekday": 5,
"@year": 2023,
"@todayStart": "2023-02-03 00:00:00.000Z",
"@todayEnd": "2023-02-03 23:59:59.999Z",
"@monthStart": "2023-02-01 00:00:00.000Z",
"@monthEnd": "2023-02-28 23:59:59.999Z",
"@yearStart": "2023-01-01 00:00:00.000Z",
"@yearEnd": "2023-12-31 23:59:59.999Z",
}
if len(testMacros) != len(identifierMacros) {
t.Fatalf("Expected %d macros, got %d", len(testMacros), len(identifierMacros))
}
for key, expected := range testMacros {
t.Run(key, func(t *testing.T) {
macro, ok := identifierMacros[key]
if !ok {
t.Fatalf("Missing macro %s", key)
}
result, err := macro()
if err != nil {
t.Fatal(err)
}
if result != expected {
t.Fatalf("Expected %q, got %q", expected, result)
}
})
}
// restore
timeNow = originalTimeNow
}