changed types.JsonArray to support generics

This commit is contained in:
Gani Georgiev 2023-03-22 17:12:44 +02:00
parent a79f3a7c56
commit 923fc26a31
13 changed files with 69 additions and 65 deletions

View File

@ -4,6 +4,9 @@
- (@todo docs) Added support for advanced unique constraints and indexes management ([#345](https://github.com/pocketbase/pocketbase/issues/345), [#544](https://github.com/pocketbase/pocketbase/issues/544))
- Deprecated `SchemaField.Unique`. Unique constraints are now managed via indexes.
The `Unique` field is a no-op and will be removed in future version.
- Optimized single relation lookups.
- Normalized record values on `maxSelect` field option change (`select`, `file`, `relation`).
@ -17,6 +20,9 @@
- Added option to explicitly set the record id from the Admin UI ([#2118](https://github.com/pocketbase/pocketbase/issues/2118)).
- **!** Changed `types.JsonArray` to support specifying a generic type, aka. `types.JsonArray[T]`.
If you have previously used `types.JsonArray`, you'll have to update it to `types.JsonArray[any]`.
- **!** Registered the `RemoveTrailingSlash` middleware only for the `/api/*` routes since it is causing issues with subpath file serving endpoints ([#2072](https://github.com/pocketbase/pocketbase/issues/2072)).
- **!** Changed the request logs `method` value to UPPERCASE, eg. "get" => "GET" ([#1956](https://github.com/pocketbase/pocketbase/discussions/1956)).

View File

@ -247,9 +247,9 @@ func (dao *Dao) IsRecordValueUnique(
var normalizedVal any
switch val := value.(type) {
case []string:
normalizedVal = append(types.JsonArray{}, list.ToInterfaceSlice(val)...)
normalizedVal = append(types.JsonArray[string]{}, val...)
case []any:
normalizedVal = append(types.JsonArray{}, val...)
normalizedVal = append(types.JsonArray[any]{}, val...)
default:
normalizedVal = val
}

View File

@ -273,7 +273,7 @@ func normalizeExpands(paths []string) []string {
func isRelFieldUnique(collection *models.Collection, fieldName string) bool {
for _, idx := range collection.Indexes {
parsed := dbutils.ParseIndex(idx.(string))
parsed := dbutils.ParseIndex(idx)
if parsed.Unique && len(parsed.Columns) == 1 && strings.EqualFold(parsed.Columns[0].Name, fieldName) {
return true
}

View File

@ -12,7 +12,6 @@ import (
"github.com/pocketbase/pocketbase/tools/dbutils"
"github.com/pocketbase/pocketbase/tools/list"
"github.com/pocketbase/pocketbase/tools/security"
"github.com/spf13/cast"
)
// SyncRecordTableSchema compares the two provided collections
@ -309,7 +308,7 @@ func (dao *Dao) dropCollectionIndex(collection *models.Collection) error {
return dao.RunInTransaction(func(txDao *Dao) error {
for _, raw := range collection.Indexes {
parsed := dbutils.ParseIndex(cast.ToString(raw))
parsed := dbutils.ParseIndex(raw)
if !parsed.IsValid() {
continue
@ -342,8 +341,7 @@ func (dao *Dao) createCollectionIndexes(collection *models.Collection) error {
// record table changes
errs := validation.Errors{}
for i, idx := range collection.Indexes {
idxString := cast.ToString(idx)
parsed := dbutils.ParseIndex(idxString)
parsed := dbutils.ParseIndex(idx)
// ensure that the index is always for the current collection
parsed.TableName = collection.Name

View File

@ -40,7 +40,7 @@ func TestSyncRecordTableSchema(t *testing.T) {
Type: schema.FieldTypeEmail,
},
)
updatedCollection.Indexes = types.JsonArray{"create index idx_title_renamed on anything (title_renamed)"}
updatedCollection.Indexes = types.JsonArray[string]{"create index idx_title_renamed on anything (title_renamed)"}
scenarios := []struct {
name string
@ -75,7 +75,7 @@ func TestSyncRecordTableSchema(t *testing.T) {
Type: schema.FieldTypeText,
},
),
Indexes: types.JsonArray{"create index idx_auth_test on anything (email, username)"},
Indexes: types.JsonArray[string]{"create index idx_auth_test on anything (email, username)"},
},
nil,
[]string{

View File

@ -27,18 +27,18 @@ type CollectionUpsert struct {
dao *daos.Dao
collection *models.Collection
Id string `form:"id" json:"id"`
Type string `form:"type" json:"type"`
Name string `form:"name" json:"name"`
System bool `form:"system" json:"system"`
Schema schema.Schema `form:"schema" json:"schema"`
Indexes []string `form:"indexes" json:"indexes"`
ListRule *string `form:"listRule" json:"listRule"`
ViewRule *string `form:"viewRule" json:"viewRule"`
CreateRule *string `form:"createRule" json:"createRule"`
UpdateRule *string `form:"updateRule" json:"updateRule"`
DeleteRule *string `form:"deleteRule" json:"deleteRule"`
Options types.JsonMap `form:"options" json:"options"`
Id string `form:"id" json:"id"`
Type string `form:"type" json:"type"`
Name string `form:"name" json:"name"`
System bool `form:"system" json:"system"`
Schema schema.Schema `form:"schema" json:"schema"`
Indexes types.JsonArray[string] `form:"indexes" json:"indexes"`
ListRule *string `form:"listRule" json:"listRule"`
ViewRule *string `form:"viewRule" json:"viewRule"`
CreateRule *string `form:"createRule" json:"createRule"`
UpdateRule *string `form:"updateRule" json:"updateRule"`
DeleteRule *string `form:"deleteRule" json:"deleteRule"`
Options types.JsonMap `form:"options" json:"options"`
}
// NewCollectionUpsert creates a new [CollectionUpsert] form with initializer
@ -59,7 +59,7 @@ func NewCollectionUpsert(app core.App, collection *models.Collection) *Collectio
form.Type = form.collection.Type
form.Name = form.collection.Name
form.System = form.collection.System
form.Indexes = list.ToUniqueStringSlice(form.collection.Indexes)
form.Indexes = form.collection.Indexes
form.ListRule = form.collection.ListRule
form.ViewRule = form.collection.ViewRule
form.CreateRule = form.collection.CreateRule
@ -388,7 +388,7 @@ func (form *CollectionUpsert) checkRule(value any) error {
}
func (form *CollectionUpsert) checkIndexes(value any) error {
v, _ := value.([]string)
v, _ := value.(types.JsonArray[string])
for i, rawIndex := range v {
parsed := dbutils.ParseIndex(rawIndex)
@ -510,12 +510,9 @@ func (form *CollectionUpsert) Submit(interceptors ...InterceptorFunc[*models.Col
form.collection.Schema = form.Schema
// normalize indexes format
form.collection.Indexes = types.JsonArray{}
for _, rawIdx := range form.Indexes {
form.collection.Indexes = append(
form.collection.Indexes,
dbutils.ParseIndex(rawIdx).Build(),
)
form.collection.Indexes = make(types.JsonArray[string], len(form.Indexes))
for i, rawIdx := range form.Indexes {
form.collection.Indexes[i] = dbutils.ParseIndex(rawIdx).Build()
}
}

View File

@ -23,11 +23,11 @@ const (
type Collection struct {
BaseModel
Name string `db:"name" json:"name"`
Type string `db:"type" json:"type"`
System bool `db:"system" json:"system"`
Schema schema.Schema `db:"schema" json:"schema"`
Indexes types.JsonArray `db:"indexes" json:"indexes"`
Name string `db:"name" json:"name"`
Type string `db:"type" json:"type"`
System bool `db:"system" json:"system"`
Schema schema.Schema `db:"schema" json:"schema"`
Indexes types.JsonArray[string] `db:"indexes" json:"indexes"`
// rules
ListRule *string `db:"listRule" json:"listRule"`

View File

@ -79,7 +79,7 @@ func TestCollectionMarshalJSON(t *testing.T) {
},
{
"unknown type + non empty options",
models.Collection{Name: "test", Type: "unknown", ListRule: types.Pointer("test_list"), Options: types.JsonMap{"test": 123}, Indexes: types.JsonArray{"idx_test"}},
models.Collection{Name: "test", Type: "unknown", ListRule: types.Pointer("test_list"), Options: types.JsonMap{"test": 123}, Indexes: types.JsonArray[string]{"idx_test"}},
`{"id":"","created":"","updated":"","name":"test","type":"unknown","system":false,"schema":[],"indexes":["idx_test"],"listRule":"test_list","viewRule":null,"createRule":null,"updateRule":null,"deleteRule":null,"options":{}}`,
},
{

View File

@ -630,16 +630,16 @@ func (m *Record) getNormalizeDataValueForDB(key string) any {
switch ids := val.(type) {
case []string:
// encode string slice
return append(types.JsonArray{}, list.ToInterfaceSlice(ids)...)
return append(types.JsonArray[string]{}, ids...)
case []int:
// encode int slice
return append(types.JsonArray{}, list.ToInterfaceSlice(ids)...)
return append(types.JsonArray[int]{}, ids...)
case []float64:
// encode float64 slice
return append(types.JsonArray{}, list.ToInterfaceSlice(ids)...)
return append(types.JsonArray[float64]{}, ids...)
case []any:
// encode interface slice
return append(types.JsonArray{}, ids...)
return append(types.JsonArray[any]{}, ids...)
default:
// no changes
return val

View File

@ -149,7 +149,7 @@ func init() {
collection.Updated = collection.Created
collection.ListRule = types.Pointer("@request.auth.id != '' && created > 0 || 'backtick`test' = 0")
collection.ViewRule = types.Pointer(`id = "1"`)
collection.Indexes = types.JsonArray{"create index test on new_name (id)"}
collection.Indexes = types.JsonArray[string]{"create index test on new_name (id)"}
collection.SetOptions(models.CollectionAuthOptions{
ManageRule: types.Pointer("created > 0"),
MinPasswordLength: 20,
@ -318,7 +318,7 @@ func init() {
collection.Updated = collection.Created
collection.ListRule = types.Pointer("@request.auth.id != '' && created > 0 || 'backtick`test' = 0")
collection.ViewRule = types.Pointer(`id = "1"`)
collection.Indexes = types.JsonArray{"create index test on test456 (id)"}
collection.Indexes = types.JsonArray[string]{"create index test on test456 (id)"}
collection.SetOptions(models.CollectionAuthOptions{
ManageRule: types.Pointer("created > 0"),
MinPasswordLength: 20,
@ -642,7 +642,7 @@ func init() {
collection.Updated = collection.Created
collection.ListRule = types.Pointer("@request.auth.id != '' && created > 0")
collection.ViewRule = types.Pointer(`id = "1"`)
collection.Indexes = types.JsonArray{"create index test1 on test456 (f1_name)"}
collection.Indexes = types.JsonArray[string]{"create index test1 on test456 (f1_name)"}
collection.SetOptions(models.CollectionAuthOptions{
ManageRule: types.Pointer("created > 0"),
MinPasswordLength: 20,
@ -681,7 +681,7 @@ func init() {
collection.Type = models.CollectionTypeBase
collection.DeleteRule = types.Pointer(`updated > 0 && @request.auth.id != ''`)
collection.ListRule = nil
collection.Indexes = types.JsonArray{
collection.Indexes = types.JsonArray[string]{
"create index test1 on test456_update (f1_name)",
}
collection.NormalizeOptions()

View File

@ -254,7 +254,7 @@ func TestToUniqueStringSlice(t *testing.T) {
{[]any{0, 1, "test", ""}, []string{"0", "1", "test"}},
{[]string{"test1", "test2", "test1"}, []string{"test1", "test2"}},
{`["test1", "test2", "test2"]`, []string{"test1", "test2"}},
{types.JsonArray{"test1", "test2", "test1"}, []string{"test1", "test2"}},
{types.JsonArray[string]{"test1", "test2", "test1"}, []string{"test1", "test2"}},
}
for i, scenario := range scenarios {

View File

@ -7,30 +7,32 @@ import (
)
// JsonArray defines a slice that is safe for json and db read/write.
type JsonArray []any
type JsonArray[T any] []T
// internal alias to prevent recursion during marshalization.
type jsonArrayAlias[T any] JsonArray[T]
// MarshalJSON implements the [json.Marshaler] interface.
func (m JsonArray) MarshalJSON() ([]byte, error) {
type alias JsonArray // prevent recursion
func (m JsonArray[T]) MarshalJSON() ([]byte, error) {
// initialize an empty map to ensure that `[]` is returned as json
if m == nil {
m = JsonArray{}
m = JsonArray[T]{}
}
return json.Marshal(alias(m))
return json.Marshal(jsonArrayAlias[T](m))
}
// Value implements the [driver.Valuer] interface.
func (m JsonArray) Value() (driver.Value, error) {
func (m JsonArray[T]) Value() (driver.Value, error) {
data, err := json.Marshal(m)
return string(data), err
}
// Scan implements [sql.Scanner] interface to scan the provided value
// into the current `JsonArray` instance.
func (m *JsonArray) Scan(value any) error {
// into the current JsonArray[T] instance.
func (m *JsonArray[T]) Scan(value any) error {
var data []byte
switch v := value.(type) {
case nil:

View File

@ -2,6 +2,7 @@ package types_test
import (
"database/sql/driver"
"encoding/json"
"testing"
"github.com/pocketbase/pocketbase/tools/types"
@ -9,14 +10,14 @@ import (
func TestJsonArrayMarshalJSON(t *testing.T) {
scenarios := []struct {
json types.JsonArray
json json.Marshaler
expected string
}{
{nil, "[]"},
{types.JsonArray{}, `[]`},
{types.JsonArray{1, 2, 3}, `[1,2,3]`},
{types.JsonArray{"test1", "test2", "test3"}, `["test1","test2","test3"]`},
{types.JsonArray{1, "test"}, `[1,"test"]`},
{new(types.JsonArray[any]), "[]"},
{types.JsonArray[any]{}, `[]`},
{types.JsonArray[int]{1, 2, 3}, `[1,2,3]`},
{types.JsonArray[string]{"test1", "test2", "test3"}, `["test1","test2","test3"]`},
{types.JsonArray[any]{1, "test"}, `[1,"test"]`},
}
for i, s := range scenarios {
@ -33,14 +34,14 @@ func TestJsonArrayMarshalJSON(t *testing.T) {
func TestJsonArrayValue(t *testing.T) {
scenarios := []struct {
json types.JsonArray
json driver.Valuer
expected driver.Value
}{
{nil, `[]`},
{types.JsonArray{}, `[]`},
{types.JsonArray{1, 2, 3}, `[1,2,3]`},
{types.JsonArray{"test1", "test2", "test3"}, `["test1","test2","test3"]`},
{types.JsonArray{1, "test"}, `[1,"test"]`},
{new(types.JsonArray[any]), `[]`},
{types.JsonArray[any]{}, `[]`},
{types.JsonArray[int]{1, 2, 3}, `[1,2,3]`},
{types.JsonArray[string]{"test1", "test2", "test3"}, `["test1","test2","test3"]`},
{types.JsonArray[any]{1, "test"}, `[1,"test"]`},
}
for i, s := range scenarios {
@ -77,7 +78,7 @@ func TestJsonArrayScan(t *testing.T) {
}
for i, s := range scenarios {
arr := types.JsonArray{}
arr := types.JsonArray[any]{}
scanErr := arr.Scan(s.value)
hasErr := scanErr != nil