added DynamicModel and DynamicList goja bindings
This commit is contained in:
parent
1adcfcc03b
commit
93606c6647
|
@ -55,8 +55,8 @@
|
||||||
new: migratecmd.MustRegister(app core.App, rootCmd *cobra.Command, config migratecmd.Config)
|
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).
|
- (@todo docs) Added new experimental 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.
|
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:
|
To enable them as part of a custom Go build:
|
||||||
```go
|
```go
|
||||||
jsvm.MustRegisterHooks(app core.App, config jsvm.HooksConfig{})
|
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`.
|
- 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
|
## v0.16.6-WIP
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||||
|
@ -36,6 +37,7 @@ import (
|
||||||
"github.com/pocketbase/pocketbase/tools/filesystem"
|
"github.com/pocketbase/pocketbase/tools/filesystem"
|
||||||
"github.com/pocketbase/pocketbase/tools/mailer"
|
"github.com/pocketbase/pocketbase/tools/mailer"
|
||||||
"github.com/pocketbase/pocketbase/tools/security"
|
"github.com/pocketbase/pocketbase/tools/security"
|
||||||
|
"github.com/pocketbase/pocketbase/tools/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
func baseBinds(vm *goja.Runtime) {
|
func baseBinds(vm *goja.Runtime) {
|
||||||
|
@ -43,24 +45,24 @@ func baseBinds(vm *goja.Runtime) {
|
||||||
|
|
||||||
// override primitive class constructors to return pointers
|
// override primitive class constructors to return pointers
|
||||||
// (this is useful when unmarshaling or scaning a db result)
|
// (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
|
return &arg
|
||||||
})
|
})
|
||||||
vm.Set("_stringPointer", func(arg string) *string {
|
vm.Set("__stringPointer", func(arg string) *string {
|
||||||
return &arg
|
return &arg
|
||||||
})
|
})
|
||||||
vm.Set("_boolPointer", func(arg bool) *bool {
|
vm.Set("__boolPointer", func(arg bool) *bool {
|
||||||
return &arg
|
return &arg
|
||||||
})
|
})
|
||||||
vm.RunString(`
|
vm.RunString(`
|
||||||
this.Number = function(arg) {
|
this.Number = function(arg) {
|
||||||
return _numberPointer(arg)
|
return __numberPointer(arg)
|
||||||
}
|
}
|
||||||
this.String = function(arg) {
|
this.String = function(arg) {
|
||||||
return _stringPointer(arg)
|
return __stringPointer(arg)
|
||||||
}
|
}
|
||||||
this.Boolean = function(arg) {
|
this.Boolean = function(arg) {
|
||||||
return _boolPointer(arg)
|
return __boolPointer(arg)
|
||||||
}
|
}
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
@ -77,6 +79,50 @@ func baseBinds(vm *goja.Runtime) {
|
||||||
return dest, nil
|
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 {
|
vm.Set("Record", func(call goja.ConstructorCall) *goja.Object {
|
||||||
var instance *models.Record
|
var instance *models.Record
|
||||||
|
|
||||||
|
@ -364,3 +410,71 @@ func filesContent(dirPath string, pattern string) (map[string][]byte, error) {
|
||||||
|
|
||||||
return result, nil
|
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()
|
||||||
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ func TestBaseBindsCount(t *testing.T) {
|
||||||
vm := goja.New()
|
vm := goja.New()
|
||||||
baseBinds(vm)
|
baseBinds(vm)
|
||||||
|
|
||||||
testBindsCount(vm, "this", 12, t)
|
testBindsCount(vm, "this", 15, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBaseBindsUnmarshal(t *testing.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)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue