changed types.JsonArray to support generics
This commit is contained in:
parent
a79f3a7c56
commit
923fc26a31
|
@ -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)).
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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":{}}`,
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue