refactored Record.data and Record.expand to be concurrent safe

This commit is contained in:
Gani Georgiev
2023-01-25 22:39:42 +02:00
parent 39df263a03
commit ae371e8481
38 changed files with 313 additions and 88 deletions
+83 -38
View File
@@ -12,6 +12,7 @@ import (
"github.com/pocketbase/pocketbase/models/schema"
"github.com/pocketbase/pocketbase/tools/list"
"github.com/pocketbase/pocketbase/tools/security"
"github.com/pocketbase/pocketbase/tools/store"
"github.com/pocketbase/pocketbase/tools/types"
"github.com/spf13/cast"
"golang.org/x/crypto/bcrypt"
@@ -28,19 +29,19 @@ type Record struct {
collection *Collection
exportUnknown bool // whether to export unknown fields
ignoreEmailVisibility bool // whether to ignore the emailVisibility flag for auth collections
data map[string]any // any custom data in addition to the base model fields
expand map[string]any // expanded relations
exportUnknown bool // whether to export unknown fields
ignoreEmailVisibility bool // whether to ignore the emailVisibility flag for auth collections
loaded bool
originalData map[string]any // the original (aka. first loaded) model data
originalData map[string]any // the original (aka. first loaded) model data
expand *store.Store[any] // expanded relations
data *store.Store[any] // any custom data in addition to the base model fields
}
// NewRecord initializes a new empty Record model.
func NewRecord(collection *Collection) *Record {
return &Record{
collection: collection,
data: map[string]any{},
data: store.New[any](nil),
}
}
@@ -109,7 +110,8 @@ func (m *Record) Collection() *Collection {
}
// OriginalCopy returns a copy of the current record model populated
// with its original (aka. the initially loaded) data state.
// with its ORIGINAL data state (aka. the initially loaded) and
// everything else reset to the defaults.
func (m *Record) OriginalCopy() *Record {
newRecord := NewRecord(m.collection)
newRecord.Load(m.originalData)
@@ -123,15 +125,40 @@ func (m *Record) OriginalCopy() *Record {
return newRecord
}
// Expand returns a shallow copy of the record.expand data
// attached to the current Record model.
func (m *Record) Expand() map[string]any {
return shallowCopy(m.expand)
// CleanCopy returns a copy of the current record model populated only
// with its LATEST data state and everything else reset to the defaults.
func (m *Record) CleanCopy() *Record {
newRecord := NewRecord(m.collection)
newRecord.Load(m.data.GetAll())
newRecord.Id = m.Id
newRecord.Created = m.Created
newRecord.Updated = m.Updated
if m.IsNew() {
newRecord.MarkAsNew()
} else {
newRecord.MarkAsNotNew()
}
return newRecord
}
// SetExpand assigns the provided data to record.expand.
// Expand returns a shallow copy of the current Record model expand data.
func (m *Record) Expand() map[string]any {
if m.expand == nil {
m.expand = store.New[any](nil)
}
return m.expand.GetAll()
}
// SetExpand shallow copies the provided data to the current Record model's expand.
func (m *Record) SetExpand(expand map[string]any) {
m.expand = shallowCopy(expand)
if m.expand == nil {
m.expand = store.New[any](nil)
}
m.expand.Reset(expand)
}
// MergeExpand merges recursively the provided expand data into
@@ -141,14 +168,23 @@ func (m *Record) SetExpand(expand map[string]any) {
// then both old and new records will be merged into a new slice (aka. a :merge: [b,c] => [a,b,c]).
// Otherwise the "old" expanded record will be replace with the "new" one (aka. a :merge: aNew => aNew).
func (m *Record) MergeExpand(expand map[string]any) {
if m.expand == nil && len(expand) > 0 {
m.expand = make(map[string]any, len(expand))
// nothing to merge
if len(expand) == 0 {
return
}
// no old expand
if m.expand == nil {
m.expand = store.New(expand)
return
}
oldExpand := m.expand.GetAll()
for key, new := range expand {
old, ok := m.expand[key]
old, ok := oldExpand[key]
if !ok {
m.expand[key] = new
oldExpand[key] = new
continue
}
@@ -163,7 +199,7 @@ func (m *Record) MergeExpand(expand map[string]any) {
default:
// invalid old expand data -> assign directly the new
// (no matter whether new is valid or not)
m.expand[key] = new
oldExpand[key] = new
continue
}
@@ -197,19 +233,23 @@ func (m *Record) MergeExpand(expand map[string]any) {
}
if wasOldSlice || wasNewSlice || len(oldSlice) == 0 {
m.expand[key] = oldSlice
oldExpand[key] = oldSlice
} else {
m.expand[key] = oldSlice[0]
oldExpand[key] = oldSlice[0]
}
}
m.expand.Reset(oldExpand)
}
// SchemaData returns a shallow copy ONLY of the defined record schema fields data.
func (m *Record) SchemaData() map[string]any {
result := make(map[string]any, len(m.collection.Schema.Fields()))
data := m.data.GetAll()
for _, field := range m.collection.Schema.Fields() {
if v, ok := m.data[field.Name]; ok {
if v, ok := data[field.Name]; ok {
result[field.Name] = v
}
}
@@ -221,7 +261,11 @@ func (m *Record) SchemaData() map[string]any {
// aka. fields that are neither one of the base and special system ones,
// nor defined by the collection schema.
func (m *Record) UnknownData() map[string]any {
return m.extractUnknownData(m.data)
if m.data == nil {
return nil
}
return m.extractUnknownData(m.data.GetAll())
}
// IgnoreEmailVisibility toggles the flag to ignore the auth record email visibility check.
@@ -267,10 +311,10 @@ func (m *Record) Set(key string, value any) {
}
if m.data == nil {
m.data = map[string]any{}
m.data = store.New[any](nil)
}
m.data[key] = v
m.data.Set(key, v)
}
}
@@ -284,11 +328,11 @@ func (m *Record) Get(key string) any {
case schema.FieldNameUpdated:
return m.Updated
default:
if v, ok := m.data[key]; ok {
return v
if m.data == nil {
return nil
}
return nil
return m.data.Get(key)
}
}
@@ -331,10 +375,11 @@ func (m *Record) GetStringSlice(key string) []string {
// Retrieves the "key" json field value and unmarshals it into "result".
//
// Example
// result := struct {
// FirstName string `json:"first_name"`
// }{}
// err := m.UnmarshalJSONField("my_field_name", &result)
//
// result := struct {
// FirstName string `json:"first_name"`
// }{}
// err := m.UnmarshalJSONField("my_field_name", &result)
func (m *Record) UnmarshalJSONField(key string, result any) error {
return json.Unmarshal([]byte(m.GetString(key)), &result)
}
@@ -432,8 +477,8 @@ func (m *Record) PublicExport() map[string]any {
result[schema.FieldNameCollectionName] = m.collection.Name
// add expand (if set)
if m.expand != nil {
result[schema.FieldNameExpand] = m.expand
if m.expand != nil && m.expand.Length() > 0 {
result[schema.FieldNameExpand] = m.expand.GetAll()
}
return result
@@ -469,10 +514,10 @@ func (m *Record) UnmarshalJSON(data []byte) error {
//
// Example usage:
//
// newData := record.ReplaceModifers(data)
// // record: {"field": 10}
// // data: {"field+": 5}
// // newData: {"field": 15}
// newData := record.ReplaceModifers(data)
// // record: {"field": 10}
// // data: {"field+": 5}
// // newData: {"field": 15}
func (m *Record) ReplaceModifers(data map[string]any) map[string]any {
var clone = shallowCopy(data)
if len(clone) == 0 {
@@ -624,7 +669,7 @@ func (m *Record) extractUnknownData(data map[string]any) map[string]any {
result := map[string]any{}
for k, v := range m.data {
for k, v := range data {
if _, ok := knownFields[k]; !ok {
result[k] = v
}
+34 -1
View File
@@ -1,6 +1,7 @@
package models_test
import (
"bytes"
"database/sql"
"encoding/json"
"testing"
@@ -346,7 +347,7 @@ func TestRecordOriginalCopy(t *testing.T) {
t.Fatalf("Expected the initial/original f to be %q, got %q", "123", v)
}
// Loading new data shouldn't affect the original state
// loading new data shouldn't affect the original state
m.Load(map[string]any{"f": "789"})
if v := m.GetString("f"); v != "789" {
@@ -358,6 +359,38 @@ func TestRecordOriginalCopy(t *testing.T) {
}
}
func TestRecordCleanCopy(t *testing.T) {
m := models.NewRecord(&models.Collection{
Name: "cname",
Type: models.CollectionTypeAuth,
})
m.Load(map[string]any{
"id": "id1",
"created": "2023-01-01 00:00:00.000Z",
"updated": "2023-01-02 00:00:00.000Z",
"username": "test",
"verified": true,
"email": "test@example.com",
"unknown": "456",
})
// make a change to ensure that the latest data is targeted
m.Set("id", "id2")
// allow the special flags and options to check whether they will be ignored
m.SetExpand(map[string]any{"test": 123})
m.IgnoreEmailVisibility(true)
m.WithUnkownData(true)
copy := m.CleanCopy()
copyExport, _ := copy.MarshalJSON()
expectedExport := []byte(`{"collectionId":"","collectionName":"cname","created":"2023-01-01 00:00:00.000Z","emailVisibility":false,"id":"id2","updated":"2023-01-02 00:00:00.000Z","username":"test","verified":true}`)
if !bytes.Equal(copyExport, expectedExport) {
t.Fatalf("Expected clean export \n%s, \ngot \n%s", expectedExport, copyExport)
}
}
func TestRecordSetAndGetExpand(t *testing.T) {
collection := &models.Collection{}
m := models.NewRecord(collection)