added DynamicModel and DynamicList goja bindings

This commit is contained in:
Gani Georgiev 2023-06-21 11:21:40 +03:00
parent 1adcfcc03b
commit 93606c6647
3 changed files with 227 additions and 9 deletions

View File

@ -55,8 +55,8 @@
new: migratecmd.MustRegister(app core.App, rootCmd *cobra.Command, config migratecmd.Config)
```
- (@todo docs) Added new optional JavaScript app hooks binding via [goja](https://github.com/dop251/goja).
There are available by default with the prebuilt executable if you add a `*.pb.js` file in `pb_hooks` directory.
- (@todo docs) Added new experimental JavaScript app hooks binding via [goja](https://github.com/dop251/goja).
They are available by default with the prebuilt executable if you add a `*.pb.js` file in `pb_hooks` directory.
To enable them as part of a custom Go build:
```go
jsvm.MustRegisterHooks(app core.App, config jsvm.HooksConfig{})
@ -64,6 +64,8 @@
- Refactored `apis.ApiError` validation errors serialization to allow `map[string]error` and `map[string]any` when generating the public safe formatted `ApiError.Data`.
- Added `types.JsonMap.Get(k)` and `types.JsonMap.Set(k, v)` helpers for the cases where the type aliased direct map access is not allowed (eg. in [goja](https://pkg.go.dev/github.com/dop251/goja#hdr-Maps_with_methods)).
## v0.16.6-WIP

View File

@ -22,6 +22,7 @@ import (
"path/filepath"
"reflect"
"regexp"
"strings"
"github.com/dop251/goja"
validation "github.com/go-ozzo/ozzo-validation/v4"
@ -36,6 +37,7 @@ import (
"github.com/pocketbase/pocketbase/tools/filesystem"
"github.com/pocketbase/pocketbase/tools/mailer"
"github.com/pocketbase/pocketbase/tools/security"
"github.com/pocketbase/pocketbase/tools/types"
)
func baseBinds(vm *goja.Runtime) {
@ -43,24 +45,24 @@ func baseBinds(vm *goja.Runtime) {
// override primitive class constructors to return pointers
// (this is useful when unmarshaling or scaning a db result)
vm.Set("_numberPointer", func(arg float64) *float64 {
vm.Set("__numberPointer", func(arg float64) *float64 {
return &arg
})
vm.Set("_stringPointer", func(arg string) *string {
vm.Set("__stringPointer", func(arg string) *string {
return &arg
})
vm.Set("_boolPointer", func(arg bool) *bool {
vm.Set("__boolPointer", func(arg bool) *bool {
return &arg
})
vm.RunString(`
this.Number = function(arg) {
return _numberPointer(arg)
return __numberPointer(arg)
}
this.String = function(arg) {
return _stringPointer(arg)
return __stringPointer(arg)
}
this.Boolean = function(arg) {
return _boolPointer(arg)
return __boolPointer(arg)
}
`)
@ -77,6 +79,50 @@ func baseBinds(vm *goja.Runtime) {
return dest, nil
})
// temporary helper to properly return the length of an array
// see https://github.com/dop251/goja/issues/521
vm.Set("len", func(val any) int {
rv := reflect.ValueOf(val)
rk := rv.Kind()
if rk == reflect.Ptr {
rv = rv.Elem()
rk = rv.Kind()
}
if rk == reflect.Slice || rk == reflect.Array {
return rv.Len()
}
return 0
})
vm.Set("DynamicModel", func(call goja.ConstructorCall) *goja.Object {
shape, ok := call.Argument(0).Export().(map[string]any)
if !ok || len(shape) == 0 {
panic("missing shape data")
}
instance := newDynamicModel(shape)
instanceValue := vm.ToValue(instance).(*goja.Object)
instanceValue.SetPrototype(call.This.Prototype())
return instanceValue
})
vm.Set("DynamicList", func(call goja.ConstructorCall) *goja.Object {
shape, ok := call.Argument(0).Export().(map[string]any)
if !ok || len(shape) == 0 {
panic("missing shape data")
}
instance := newDynamicList(shape)
instanceValue := vm.ToValue(instance).(*goja.Object)
instanceValue.SetPrototype(call.This.Prototype())
return instanceValue
})
vm.Set("Record", func(call goja.ConstructorCall) *goja.Object {
var instance *models.Record
@ -364,3 +410,71 @@ func filesContent(dirPath string, pattern string) (map[string][]byte, error) {
return result, nil
}
// newDynamicList creates a new dynamic slice of structs with fields based
// on the specified "shape".
//
// Example:
//
// m := newDynamicList(map[string]any{
// "title": "",
// "total": 0,
// })
func newDynamicList(shape map[string]any) any {
m := newDynamicModel(shape)
mt := reflect.TypeOf(m)
st := reflect.SliceOf(mt)
elem := reflect.New(st).Elem()
return elem.Addr().Interface()
}
// newDynamicModel creates a new dynamic struct with fields based
// on the specified "shape".
//
// Example:
//
// m := newDynamicModel(map[string]any{
// "title": "",
// "total": 0,
// })
func newDynamicModel(shape map[string]any) any {
shapeValues := make([]reflect.Value, 0, len(shape))
structFields := make([]reflect.StructField, 0, len(shape))
for k, v := range shape {
vt := reflect.TypeOf(v)
switch kind := vt.Kind(); kind {
case reflect.Map:
raw, _ := json.Marshal(v)
newV := types.JsonMap{}
newV.Scan(raw)
v = newV
vt = reflect.TypeOf(v)
case reflect.Slice, reflect.Array:
raw, _ := json.Marshal(v)
newV := types.JsonArray[any]{}
newV.Scan(raw)
v = newV
vt = reflect.TypeOf(newV)
}
shapeValues = append(shapeValues, reflect.ValueOf(v))
structFields = append(structFields, reflect.StructField{
Name: strings.ToUpper(k), // ensures that the field is exportable
Type: vt,
Tag: reflect.StructTag(`db:"` + k + `" json:"` + k + `"`),
})
}
st := reflect.StructOf(structFields)
elem := reflect.New(st).Elem()
for i, v := range shapeValues {
elem.Field(i).Set(v)
}
return elem.Addr().Interface()
}

View File

@ -24,7 +24,7 @@ func TestBaseBindsCount(t *testing.T) {
vm := goja.New()
baseBinds(vm)
testBindsCount(vm, "this", 12, t)
testBindsCount(vm, "this", 15, t)
}
func TestBaseBindsUnmarshal(t *testing.T) {
@ -626,3 +626,105 @@ func testBindsCount(vm *goja.Runtime, namespace string, count int, t *testing.T)
t.Fatalf("Expected %d %s binds, got %d", count, namespace, total)
}
}
func TestLoadingDynamicModel(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
vm := goja.New()
baseBinds(vm)
dbxBinds(vm)
vm.Set("$app", app)
_, err := vm.RunString(`
let result = new DynamicModel({
text: "",
bool: false,
number: 0,
select_many: [],
json: [],
// custom map-like field
obj: {},
})
$app.dao().db()
.select("text", "bool", "number", "select_many", "json", "('{\"test\": 1}') as obj")
.from("demo1")
.where($dbx.hashExp({"id": "84nmscqy84lsi1t"}))
.limit(1)
.one(result)
if (result.text != "test") {
throw new Error('Expected text "test", got ' + result.text);
}
if (result.bool != true) {
throw new Error('Expected bool true, got ' + result.bool);
}
if (result.number != 123456) {
throw new Error('Expected number 123456, got ' + result.number);
}
if (result.select_many.length != 2 || result.select_many[0] != "optionB" || result.select_many[1] != "optionC") {
throw new Error('Expected select_many ["optionB", "optionC"], got ' + result.select_many);
}
if (result.json.length != 3 || result.json[0] != 1 || result.json[1] != 2 || result.json[2] != 3) {
throw new Error('Expected json [1, 2, 3], got ' + result.json);
}
if (result.obj.get("test") != 1) {
throw new Error('Expected obj.get("test") 1, got ' + JSON.stringify(result.obj));
}
`)
if err != nil {
t.Fatal(err)
}
}
func TestLoadingDynamicList(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
vm := goja.New()
baseBinds(vm)
dbxBinds(vm)
vm.Set("$app", app)
_, err := vm.RunString(`
let result = new DynamicList({
id: "",
text: "",
})
$app.dao().db()
.select("id", "text")
.from("demo1")
.where($dbx.exp("id='84nmscqy84lsi1t' OR id='al1h9ijdeojtsjy'"))
.limit(2)
.orderBy("text ASC")
.all(result)
if (len(result) != 2) {
throw new Error('Expected 2 list items, got ' + result.length);
}
if (result[0].id != "84nmscqy84lsi1t") {
throw new Error('Expected 0.id "84nmscqy84lsi1t", got ' + result[0].id);
}
if (result[0].text != "test") {
throw new Error('Expected 0.text "test", got ' + result[0].text);
}
if (result[1].id != "al1h9ijdeojtsjy") {
throw new Error('Expected 1.id "al1h9ijdeojtsjy", got ' + result[1].id);
}
if (result[1].text != "test2") {
throw new Error('Expected 1.text "test2", got ' + result[1].text);
}
`)
if err != nil {
t.Fatal(err)
}
}