updated automigrate templates, added js bindings tests and updated models IsNew behavior

This commit is contained in:
Gani Georgiev
2022-12-05 13:57:09 +02:00
parent 604009bd10
commit b8cd686b32
21 changed files with 546 additions and 213 deletions
+1 -1
View File
@@ -65,7 +65,7 @@ func RegisterMigrations(app core.App, options *MigrationsOptions) error {
registry := new(require.Registry) // this can be shared by multiple runtimes
for file, content := range files {
vm := NewBaseVM(l.app)
vm := NewBaseVM()
registry.Enable(vm)
console.Enable(vm)
+48 -29
View File
@@ -9,24 +9,22 @@ import (
"github.com/dop251/goja"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/apis"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/daos"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/models/schema"
)
func NewBaseVM(app core.App) *goja.Runtime {
func NewBaseVM() *goja.Runtime {
vm := goja.New()
vm.SetFieldNameMapper(fieldMapper{})
vm.Set("$app", app)
vm.SetFieldNameMapper(FieldMapper{})
baseBind(vm)
dbxBind(vm)
baseBinds(vm)
dbxBinds(vm)
return vm
}
func baseBind(vm *goja.Runtime) {
func baseBinds(vm *goja.Runtime) {
vm.Set("unmarshal", func(src map[string]any, dest any) (any, error) {
raw, err := json.Marshal(src)
if err != nil {
@@ -40,39 +38,46 @@ func baseBind(vm *goja.Runtime) {
return dest, nil
})
vm.Set("Collection", func(call goja.ConstructorCall) *goja.Object {
instance := &models.Collection{}
vm.Set("Record", func(call goja.ConstructorCall) *goja.Object {
var instance *models.Record
collection, ok := call.Argument(0).Export().(*models.Collection)
if ok {
instance = models.NewRecord(collection)
data, ok := call.Argument(1).Export().(map[string]any)
if ok {
if raw, err := json.Marshal(data); err == nil {
json.Unmarshal(raw, instance)
}
}
} else {
instance = &models.Record{}
}
instanceValue := vm.ToValue(instance).(*goja.Object)
instanceValue.SetPrototype(call.This.Prototype())
return instanceValue
})
vm.Set("Record", func(call goja.ConstructorCall) *goja.Object {
instance := &models.Record{}
instanceValue := vm.ToValue(instance).(*goja.Object)
instanceValue.SetPrototype(call.This.Prototype())
return instanceValue
vm.Set("Collection", func(call goja.ConstructorCall) *goja.Object {
instance := &models.Collection{}
return defaultConstructor(vm, call, instance)
})
vm.Set("Admin", func(call goja.ConstructorCall) *goja.Object {
instance := &models.Admin{}
instanceValue := vm.ToValue(instance).(*goja.Object)
instanceValue.SetPrototype(call.This.Prototype())
return instanceValue
return defaultConstructor(vm, call, instance)
})
vm.Set("Schema", func(call goja.ConstructorCall) *goja.Object {
instance := &schema.Schema{}
instanceValue := vm.ToValue(instance).(*goja.Object)
instanceValue.SetPrototype(call.This.Prototype())
return instanceValue
return defaultConstructor(vm, call, instance)
})
vm.Set("SchemaField", func(call goja.ConstructorCall) *goja.Object {
instance := &schema.SchemaField{}
instanceValue := vm.ToValue(instance).(*goja.Object)
instanceValue.SetPrototype(call.This.Prototype())
return instanceValue
return defaultConstructor(vm, call, instance)
})
vm.Set("Dao", func(call goja.ConstructorCall) *goja.Object {
@@ -84,11 +89,25 @@ func baseBind(vm *goja.Runtime) {
instance := daos.New(db)
instanceValue := vm.ToValue(instance).(*goja.Object)
instanceValue.SetPrototype(call.This.Prototype())
return instanceValue
})
}
func dbxBind(vm *goja.Runtime) {
func defaultConstructor(vm *goja.Runtime, call goja.ConstructorCall, instance any) *goja.Object {
if data := call.Argument(0).Export(); data != nil {
if raw, err := json.Marshal(data); err == nil {
json.Unmarshal(raw, instance)
}
}
instanceValue := vm.ToValue(instance).(*goja.Object)
instanceValue.SetPrototype(call.This.Prototype())
return instanceValue
}
func dbxBinds(vm *goja.Runtime) {
obj := vm.NewObject()
vm.Set("$dbx", obj)
@@ -141,27 +160,27 @@ func apisBind(vm *goja.Runtime) {
obj.Set("enrichRecords", apis.EnrichRecords)
}
// fieldMapper provides custom mapping between Go and JavaScript property names.
// FieldMapper provides custom mapping between Go and JavaScript property names.
//
// It is similar to the builtin "uncapFieldNameMapper" but also converts
// all uppercase identifiers to their lowercase equivalent (eg. "GET" -> "get").
type fieldMapper struct {
type FieldMapper struct {
}
// FieldName implements the [FieldNameMapper.FieldName] interface method.
func (u fieldMapper) FieldName(_ reflect.Type, f reflect.StructField) string {
func (u FieldMapper) FieldName(_ reflect.Type, f reflect.StructField) string {
return convertGoToJSName(f.Name)
}
// MethodName implements the [FieldNameMapper.MethodName] interface method.
func (u fieldMapper) MethodName(_ reflect.Type, m reflect.Method) string {
func (u FieldMapper) MethodName(_ reflect.Type, m reflect.Method) string {
return convertGoToJSName(m.Name)
}
func convertGoToJSName(name string) string {
allUppercase := true
for _, c := range name {
if !unicode.IsUpper(c) {
if c != '_' && !unicode.IsUpper(c) {
allUppercase = false
break
}
+268
View File
@@ -0,0 +1,268 @@
package jsvm_test
import (
"reflect"
"testing"
"github.com/pocketbase/pocketbase/daos"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/models/schema"
"github.com/pocketbase/pocketbase/plugins/jsvm"
"github.com/pocketbase/pocketbase/tests"
)
func TestBaseVMUnmarshal(t *testing.T) {
vm := jsvm.NewBaseVM()
v, err := vm.RunString(`unmarshal({ name: "test" }, new Collection())`)
if err != nil {
t.Fatal(err)
}
m, ok := v.Export().(*models.Collection)
if !ok {
t.Fatalf("Expected models.Collection, got %v", m)
}
if m.Name != "test" {
t.Fatalf("Expected collection with name %q, got %q", "test", m.Name)
}
}
func TestBaseVMRecordBind(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
collection, err := app.Dao().FindCollectionByNameOrId("users")
if err != nil {
t.Fatal(err)
}
vm := jsvm.NewBaseVM()
vm.Set("collection", collection)
// without record data
// ---
v1, err := vm.RunString(`new Record(collection)`)
if err != nil {
t.Fatal(err)
}
m1, ok := v1.Export().(*models.Record)
if !ok {
t.Fatalf("Expected m1 to be models.Record, got \n%v", m1)
}
// with record data
// ---
v2, err := vm.RunString(`new Record(collection, { email: "test@example.com" })`)
if err != nil {
t.Fatal(err)
}
m2, ok := v2.Export().(*models.Record)
if !ok {
t.Fatalf("Expected m2 to be models.Record, got \n%v", m2)
}
if m2.Collection().Name != "users" {
t.Fatalf("Expected record with collection %q, got \n%v", "users", m2.Collection())
}
if m2.Email() != "test@example.com" {
t.Fatalf("Expected record with email field set to %q, got \n%v", "test@example.com", m2)
}
}
// @todo enable after https://github.com/dop251/goja/issues/426
// func TestBaseVMRecordGetAndSetBind(t *testing.T) {
// app, _ := tests.NewTestApp()
// defer app.Cleanup()
// collection, err := app.Dao().FindCollectionByNameOrId("users")
// if err != nil {
// t.Fatal(err)
// }
// vm := jsvm.NewBaseVM()
// vm.Set("collection", collection)
// vm.Set("getRecord", func() *models.Record {
// return models.NewRecord(collection)
// })
// _, runErr := vm.RunString(`
// const jsRecord = new Record(collection);
// jsRecord.email = "test@example.com"; // test js record setter
// const email = jsRecord.email; // test js record getter
// const goRecord = getRecord()
// goRecord.name = "test" // test go record setter
// const name = goRecord.name; // test go record getter
// `)
// if runErr != nil {
// t.Fatal(runErr)
// }
// expectedEmail := "test@example.com"
// expectedName := "test"
// jsRecord, ok := vm.Get("jsRecord").Export().(*models.Record)
// if !ok {
// t.Fatalf("Failed to export jsRecord")
// }
// if v := jsRecord.Email(); v != expectedEmail {
// t.Fatalf("Expected the js created record to have email %q, got %q", expectedEmail, v)
// }
// email := vm.Get("email").Export().(string)
// if email != expectedEmail {
// t.Fatalf("Expected exported email %q, got %q", expectedEmail, email)
// }
// goRecord, ok := vm.Get("goRecord").Export().(*models.Record)
// if !ok {
// t.Fatalf("Failed to export goRecord")
// }
// if v := goRecord.GetString("name"); v != expectedName {
// t.Fatalf("Expected the go created record to have name %q, got %q", expectedName, v)
// }
// name := vm.Get("name").Export().(string)
// if name != expectedName {
// t.Fatalf("Expected exported name %q, got %q", expectedName, name)
// }
// // ensure that the two record instances are not mixed
// if v := goRecord.Email(); v != "" {
// t.Fatalf("Expected the go created record to not have an email, got %q", v)
// }
// if v := jsRecord.GetString("name"); v != "" {
// t.Fatalf("Expected the js created record to not have a name, got %q", v)
// }
// }
func TestBaseVMCollectionBind(t *testing.T) {
vm := jsvm.NewBaseVM()
v, err := vm.RunString(`new Collection({ name: "test", schema: [{name: "title", "type": "text"}] })`)
if err != nil {
t.Fatal(err)
}
m, ok := v.Export().(*models.Collection)
if !ok {
t.Fatalf("Expected models.Collection, got %v", m)
}
if m.Name != "test" {
t.Fatalf("Expected collection with name %q, got %q", "test", m.Name)
}
if f := m.Schema.GetFieldByName("title"); f == nil {
t.Fatalf("Expected schema to be set, got %v", m.Schema)
}
}
func TestBaseVMAdminBind(t *testing.T) {
vm := jsvm.NewBaseVM()
v, err := vm.RunString(`new Admin({ email: "test@example.com" })`)
if err != nil {
t.Fatal(err)
}
m, ok := v.Export().(*models.Admin)
if !ok {
t.Fatalf("Expected models.Admin, got %v", m)
}
}
func TestBaseVMSchemaBind(t *testing.T) {
vm := jsvm.NewBaseVM()
v, err := vm.RunString(`new Schema([{name: "title", "type": "text"}])`)
if err != nil {
t.Fatal(err)
}
m, ok := v.Export().(*schema.Schema)
if !ok {
t.Fatalf("Expected schema.Schema, got %v", m)
}
if f := m.GetFieldByName("title"); f == nil {
t.Fatalf("Expected schema fields to be loaded, got %v", m.Fields())
}
}
func TestBaseVMSchemaFieldBind(t *testing.T) {
vm := jsvm.NewBaseVM()
v, err := vm.RunString(`new SchemaField({name: "title", "type": "text"})`)
if err != nil {
t.Fatal(err)
}
f, ok := v.Export().(*schema.SchemaField)
if !ok {
t.Fatalf("Expected schema.SchemaField, got %v", f)
}
if f.Name != "title" {
t.Fatalf("Expected field %q, got %v", "title", f)
}
}
func TestBaseVMDaoBind(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
vm := jsvm.NewBaseVM()
vm.Set("db", app.DB())
v, err := vm.RunString(`new Dao(db)`)
if err != nil {
t.Fatal(err)
}
d, ok := v.Export().(*daos.Dao)
if !ok {
t.Fatalf("Expected daos.Dao, got %v", d)
}
if d.DB() != app.DB() {
t.Fatalf("The db instances doesn't match")
}
}
func TestFieldMapper(t *testing.T) {
mapper := jsvm.FieldMapper{}
scenarios := []struct {
name string
expected string
}{
{"", ""},
{"test", "test"},
{"Test", "test"},
{"miXeD", "miXeD"},
{"MiXeD", "miXeD"},
{"ResolveRequestAsJSON", "resolveRequestAsJSON"},
{"Variable_with_underscore", "variable_with_underscore"},
{"ALLCAPS", "allcaps"},
{"NOTALLCAPs", "nOTALLCAPs"},
{"ALL_CAPS_WITH_UNDERSCORE", "all_caps_with_underscore"},
}
for i, s := range scenarios {
field := reflect.StructField{Name: s.name}
if v := mapper.FieldName(nil, field); v != s.expected {
t.Fatalf("[%d] Expected FieldName %q, got %q", i, s.expected, v)
}
method := reflect.Method{Name: s.name}
if v := mapper.MethodName(nil, method); v != s.expected {
t.Fatalf("[%d] Expected MethodName %q, got %q", i, s.expected, v)
}
}
}