1
0
mirror of https://github.com/pocketbase/pocketbase.git synced 2025-03-19 22:19:23 +02:00

restored DynamicModel types cache

This commit is contained in:
Gani Georgiev 2025-03-06 21:26:57 +02:00
parent 856cc604a7
commit 58dab5bf70
2 changed files with 75 additions and 37 deletions

View File

@ -13,6 +13,7 @@ import (
"path/filepath"
"reflect"
"slices"
"sort"
"strings"
"time"
@ -1020,9 +1021,19 @@ func structConstructorUnmarshal(vm *goja.Runtime, call goja.ConstructorCall, ins
return instanceValue
}
var cachedDynamicModelStructs = store.New[string, reflect.Type](nil)
// newDynamicModel creates a new dynamic struct with fields based
// on the specified "shape".
//
// The "shape" values are used as defaults and could be of type:
// - int (ex. 0)
// - float (ex. -0)
// - string (ex. "")
// - bool (ex. false)
// - slice (ex. [])
// - map (ex. map[string]any{})
//
// Example:
//
// m := newDynamicModel(map[string]any{
@ -1030,37 +1041,21 @@ func structConstructorUnmarshal(vm *goja.Runtime, call goja.ConstructorCall, ins
// "total": 0,
// })
func newDynamicModel(shape map[string]any) any {
modelType := getDynamicModelStruct(shape)
info := make([]*shapeFieldInfo, 0, len(shape))
rvShapeValues := make([]reflect.Value, len(modelType.shapeValues))
for i, v := range modelType.shapeValues {
rvShapeValues[i] = reflect.ValueOf(v)
var hash strings.Builder
sortedKeys := make([]string, 0, len(shape))
for k := range shape {
sortedKeys = append(sortedKeys, k)
}
sort.Strings(sortedKeys)
elem := reflect.New(modelType.structType).Elem()
for i, v := range rvShapeValues {
elem.Field(i).Set(v)
}
return elem.Addr().Interface()
}
type dynamicModelType struct {
structType reflect.Type
shapeValues []any
}
func getDynamicModelStruct(shape map[string]any) *dynamicModelType {
result := new(dynamicModelType)
result.shapeValues = make([]any, 0, len(shape))
structFields := make([]reflect.StructField, 0, len(shape))
for k, v := range shape {
for _, k := range sortedKeys {
v := shape[k]
vt := reflect.TypeOf(v)
switch kind := vt.Kind(); kind {
switch vt.Kind() {
case reflect.Map:
raw, _ := json.Marshal(v)
newV := types.JSONMap[any]{}
@ -1075,16 +1070,40 @@ func getDynamicModelStruct(shape map[string]any) *dynamicModelType {
vt = reflect.TypeOf(newV)
}
result.shapeValues = append(result.shapeValues, v)
hash.WriteString(k)
hash.WriteString(":")
hash.WriteString(vt.String()) // it doesn't guarantee to be unique across all types but it should be fine with the primitive types DynamicModel is used
hash.WriteString("|")
structFields = append(structFields, reflect.StructField{
Name: inflector.UcFirst(k), // ensures that the field is exportable
Type: vt,
Tag: reflect.StructTag(`db:"` + k + `" json:"` + k + `" form:"` + k + `"`),
info = append(info, &shapeFieldInfo{key: k, value: v, valueType: vt})
}
st := cachedDynamicModelStructs.GetOrSet(hash.String(), func() reflect.Type {
structFields := make([]reflect.StructField, len(info))
for i, item := range info {
structFields[i] = reflect.StructField{
Name: inflector.UcFirst(item.key), // ensures that the field is exportable
Type: item.valueType,
Tag: reflect.StructTag(`db:"` + item.key + `" json:"` + item.key + `" form:"` + item.key + `"`),
}
}
return reflect.StructOf(structFields)
})
elem := reflect.New(st).Elem()
// load default values into the new model
for i, item := range info {
elem.Field(i).Set(reflect.ValueOf(item.value))
}
result.structType = reflect.StructOf(structFields)
return result
return elem.Addr().Interface()
}
type shapeFieldInfo struct {
value any
valueType reflect.Type
key string
}

View File

@ -1137,7 +1137,6 @@ func TestLoadingDynamicModel(t *testing.T) {
}
}
// @todo revert the reflect caching and check other types
func TestDynamicModelMapFieldCaching(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
@ -1149,24 +1148,44 @@ func TestDynamicModelMapFieldCaching(t *testing.T) {
_, err := vm.RunString(`
let m1 = new DynamicModel({
int: 0,
float: -0,
text: "",
bool: false,
obj: {},
arr: [],
})
let m2 = new DynamicModel({
int: 0,
float: -0,
text: "",
bool: false,
obj: {},
arr: [],
})
m1.int = 1
m1.float = 1.5
m1.text = "a"
m1.bool = true
m1.obj.set("a", 1)
m1.arr.push(1)
m2.int = 2
m2.float = 2.5
m2.text = "b"
m2.bool = false
m2.obj.set("b", 1)
m2.arr.push(2)
let m1Expected = '{"obj":{"a":1}}';
let m1Expected = '{"arr":[1],"bool":true,"float":1.5,"int":1,"obj":{"a":1},"text":"a"}';
let m1Serialized = JSON.stringify(m1);
if (m1Serialized != m1Expected) {
throw new Error("Expected m1 \n" + m1Expected + "\ngot\n" + m1Serialized);
}
let m2Expected = '{"obj":{"b":1}}';
let m2Expected = '{"arr":[2],"bool":false,"float":2.5,"int":2,"obj":{"b":1},"text":"b"}';
let m2Serialized = JSON.stringify(m2);
if (m2Serialized != m2Expected) {
throw new Error("Expected m2 \n" + m2Expected + "\ngot\n" + m2Serialized);