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)
|
||||
```
|
||||
|
||||
- (@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
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue