Skip to content

Commit

Permalink
changed types.JsonArray to support generics
Browse files Browse the repository at this point in the history
  • Loading branch information
ganigeorgiev committed Mar 22, 2023
1 parent a79f3a7 commit 923fc26
Show file tree
Hide file tree
Showing 13 changed files with 69 additions and 65 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
Expand All @@ -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)).
Expand Down
4 changes: 2 additions & 2 deletions daos/record.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion daos/record_expand.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
6 changes: 2 additions & 4 deletions daos/record_table_sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions daos/record_table_sync_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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{
Expand Down
37 changes: 17 additions & 20 deletions forms/collection_upsert.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()
}
}

Expand Down
10 changes: 5 additions & 5 deletions models/collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
2 changes: 1 addition & 1 deletion models/collection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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":{}}`,
},
{
Expand Down
8 changes: 4 additions & 4 deletions models/record.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions plugins/migratecmd/migratecmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion tools/list/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
18 changes: 10 additions & 8 deletions tools/types/json_array.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
27 changes: 14 additions & 13 deletions tools/types/json_array_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,22 @@ package types_test

import (
"database/sql/driver"
"encoding/json"
"testing"

"github.com/pocketbase/pocketbase/tools/types"
)

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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 923fc26

Please sign in to comment.