[add] studio api server
This commit is contained in:
parent
3f0869c11e
commit
d74f74b5d9
7 changed files with 511 additions and 20 deletions
|
|
@ -2,20 +2,26 @@ package config
|
||||||
|
|
||||||
// Config 象传应用引擎配置
|
// Config 象传应用引擎配置
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Mode string `json:"mode,omitempty" env:"YAO_ENV" envDefault:"production"` // 象传引擎启动模式 production/development
|
Mode string `json:"mode,omitempty" env:"YAO_ENV" envDefault:"production"` // 象传引擎启动模式 production/development
|
||||||
Root string `json:"root,omitempty" env:"YAO_ROOT" envDefault:"."` // 应用根目录
|
Root string `json:"root,omitempty" env:"YAO_ROOT" envDefault:"."` // 应用根目录
|
||||||
DataRoot string `json:"data_root,omitempty" env:"YAO_DATA_ROOT" envDefault:""` // DATA PATH
|
DataRoot string `json:"data_root,omitempty" env:"YAO_DATA_ROOT" envDefault:""` // DATA PATH
|
||||||
Host string `json:"host,omitempty" env:"YAO_HOST" envDefault:"0.0.0.0"` // 服务监听地址
|
Host string `json:"host,omitempty" env:"YAO_HOST" envDefault:"0.0.0.0"` // 服务监听地址
|
||||||
Port int `json:"port,omitempty" env:"YAO_PORT" envDefault:"5099"` // 服务监听端口
|
Port int `json:"port,omitempty" env:"YAO_PORT" envDefault:"5099"` // 服务监听端口
|
||||||
Cert string `json:"cert,omitempty" env:"YAO_CERT"` // HTTPS 证书文件地址
|
Cert string `json:"cert,omitempty" env:"YAO_CERT"` // HTTPS 证书文件地址
|
||||||
Key string `json:"key,omitempty" env:"YAO_KEY"` // HTTPS 证书密钥地址
|
Key string `json:"key,omitempty" env:"YAO_KEY"` // HTTPS 证书密钥地址
|
||||||
Log string `json:"log,omitempty" env:"YAO_LOG"` // 服务日志地址
|
Log string `json:"log,omitempty" env:"YAO_LOG"` // 服务日志地址
|
||||||
LogMode string `json:"log_mode,omitempty" env:"YAO_LOG_MODE" envDefault:"TEXT"` // 服务日志模式 JSON|TEXT
|
LogMode string `json:"log_mode,omitempty" env:"YAO_LOG_MODE" envDefault:"TEXT"` // 服务日志模式 JSON|TEXT
|
||||||
// Session string `json:"session,omitempty" env:"YAO_SESSION" envDefault:"memory"` // 用户会话模式 memory|redis|database
|
JWTSecret string `json:"jwt_secret,omitempty" env:"YAO_JWT_SECRET"` // JWT 密钥
|
||||||
JWTSecret string `json:"jwt_secret,omitempty" env:"YAO_JWT_SECRET"` // JWT 密钥
|
DB DBConfig `json:"db,omitempty"` // 数据库配置
|
||||||
DB DBConfig `json:"db,omitempty"` // 数据库配置
|
AllowFrom []string `json:"allowfrom,omitempty" envSeparator:"|" env:"YAO_ALLOW_FROM"` // Domain list the separator is |
|
||||||
AllowFrom []string `json:"allowfrom,omitempty" envSeparator:"|" env:"YAO_ALLOW_FROM" ` // Domain list the separator is |
|
Session SessionConfig `json:"session,omitempty"` // Session Config
|
||||||
Session SessionConfig `json:"session,omitempty"`
|
Studio StudioConfig `json:"studio,omitempty"` // Studio config
|
||||||
|
}
|
||||||
|
|
||||||
|
// StudioConfig the studio config
|
||||||
|
type StudioConfig struct {
|
||||||
|
Port int `json:"studio_port,omitempty" env:"YAO_STUDIO_PORT" envDefault:"5077"` // Studio port
|
||||||
|
Secret int `json:"studio_secret,omitempty" env:"YAO_STUDIO_SECRET"` // Studio Secret, if does not set, auto-generate a secret
|
||||||
}
|
}
|
||||||
|
|
||||||
// DBConfig 数据库配置
|
// DBConfig 数据库配置
|
||||||
|
|
|
||||||
41
studio/middleware.go
Normal file
41
studio/middleware.go
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
package studio
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/yaoapp/kun/exception"
|
||||||
|
"github.com/yaoapp/xun"
|
||||||
|
)
|
||||||
|
|
||||||
|
func hdRecovered(c *gin.Context, recovered interface{}) {
|
||||||
|
|
||||||
|
var code = http.StatusInternalServerError
|
||||||
|
|
||||||
|
if err, ok := recovered.(string); ok {
|
||||||
|
c.JSON(code, xun.R{
|
||||||
|
"code": code,
|
||||||
|
"message": fmt.Sprintf("%s", err),
|
||||||
|
})
|
||||||
|
} else if err, ok := recovered.(exception.Exception); ok {
|
||||||
|
code = err.Code
|
||||||
|
c.JSON(code, xun.R{
|
||||||
|
"code": code,
|
||||||
|
"message": err.Message,
|
||||||
|
})
|
||||||
|
} else if err, ok := recovered.(*exception.Exception); ok {
|
||||||
|
code = err.Code
|
||||||
|
c.JSON(code, xun.R{
|
||||||
|
"code": code,
|
||||||
|
"message": err.Message,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
c.JSON(code, xun.R{
|
||||||
|
"code": code,
|
||||||
|
"message": fmt.Sprintf("%v", recovered),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
c.AbortWithStatus(code)
|
||||||
|
}
|
||||||
183
studio/router.go
Normal file
183
studio/router.go
Normal file
|
|
@ -0,0 +1,183 @@
|
||||||
|
package studio
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
jsoniter "github.com/json-iterator/go"
|
||||||
|
"github.com/yaoapp/gou"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Serve start the api server
|
||||||
|
func setRouter(router *gin.Engine) {
|
||||||
|
|
||||||
|
router.Use(gin.CustomRecovery(hdRecovered))
|
||||||
|
|
||||||
|
// DSL ReadDir, ReadFile
|
||||||
|
router.GET("/dsl/:method", func(c *gin.Context) {
|
||||||
|
method := strings.ToLower(c.Param("method"))
|
||||||
|
switch method {
|
||||||
|
|
||||||
|
case "readfile":
|
||||||
|
name := c.Query("name")
|
||||||
|
if name == "" {
|
||||||
|
throw(c, 400, "file name is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := dfs.ReadFile(name)
|
||||||
|
if err != nil {
|
||||||
|
throw(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res := map[string]interface{}{}
|
||||||
|
err = jsoniter.Unmarshal(data, &res)
|
||||||
|
if err != nil {
|
||||||
|
throw(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(200, res)
|
||||||
|
c.Done()
|
||||||
|
break
|
||||||
|
|
||||||
|
case "readdir":
|
||||||
|
name := c.Query("name")
|
||||||
|
if name == "" {
|
||||||
|
throw(c, 400, "dir name is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
recursive := false
|
||||||
|
if c.Query("recursive") == "1" || strings.ToLower(c.Query("recursive")) == "true" {
|
||||||
|
recursive = true
|
||||||
|
}
|
||||||
|
data, err := dfs.ReadDir(name, recursive)
|
||||||
|
if err != nil {
|
||||||
|
throw(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(200, data)
|
||||||
|
c.Done()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// DSL WriteFile, Mkdir, MkdirAll ...
|
||||||
|
router.POST("/dsl/:method", func(c *gin.Context) {
|
||||||
|
|
||||||
|
method := strings.ToLower(c.Param("method"))
|
||||||
|
switch method {
|
||||||
|
case "writefile":
|
||||||
|
name := c.Query("name")
|
||||||
|
if name == "" {
|
||||||
|
throw(c, 400, "dir name is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := io.ReadAll(c.Request.Body)
|
||||||
|
if err != nil {
|
||||||
|
throw(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload == nil || len(payload) == 0 {
|
||||||
|
throw(c, 500, "file content is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
length, err := dfs.WriteFile(name, payload, 0644)
|
||||||
|
if err != nil {
|
||||||
|
throw(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, length)
|
||||||
|
c.Done()
|
||||||
|
break
|
||||||
|
|
||||||
|
case "mkdir":
|
||||||
|
name := c.Query("name")
|
||||||
|
if name == "" {
|
||||||
|
throw(c, 400, "dir name is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := dfs.Mkdir(name, int(os.ModePerm))
|
||||||
|
if err != nil {
|
||||||
|
throw(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Status(200)
|
||||||
|
c.Done()
|
||||||
|
break
|
||||||
|
|
||||||
|
case "mkdirall":
|
||||||
|
name := c.Query("name")
|
||||||
|
if name == "" {
|
||||||
|
throw(c, 400, "dir name is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := dfs.MkdirAll(name, int(os.ModePerm))
|
||||||
|
if err != nil {
|
||||||
|
throw(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Status(200)
|
||||||
|
c.Done()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
// Cloud Functions
|
||||||
|
router.POST("/service/:name", func(c *gin.Context) {
|
||||||
|
|
||||||
|
name := c.Param("name")
|
||||||
|
if name == "" {
|
||||||
|
throw(c, 400, "service name is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
service := fmt.Sprintf("__yao.studio.%s", c.Param("name"))
|
||||||
|
|
||||||
|
payload, err := io.ReadAll(c.Request.Body)
|
||||||
|
if err != nil {
|
||||||
|
throw(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload == nil || len(payload) > 0 {
|
||||||
|
throw(c, 400, "file content is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var call cloudCall
|
||||||
|
err = jsoniter.Unmarshal(payload, &call)
|
||||||
|
if err != nil {
|
||||||
|
throw(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := gou.Yao.Engine.Call(map[string]interface{}{}, service, call.Method, call.Args...)
|
||||||
|
if err != nil {
|
||||||
|
throw(c, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, res)
|
||||||
|
c.Done()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func throw(c *gin.Context, code int, message string) {
|
||||||
|
c.JSON(code, map[string]interface{}{
|
||||||
|
"message": message,
|
||||||
|
"code": code,
|
||||||
|
})
|
||||||
|
c.Done()
|
||||||
|
}
|
||||||
147
studio/studio.go
147
studio/studio.go
|
|
@ -1,3 +1,148 @@
|
||||||
package studio
|
package studio
|
||||||
|
|
||||||
// Yao Studio
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/yaoapp/gou"
|
||||||
|
"github.com/yaoapp/gou/fs"
|
||||||
|
"github.com/yaoapp/gou/fs/dsl"
|
||||||
|
"github.com/yaoapp/kun/log"
|
||||||
|
"github.com/yaoapp/yao/config"
|
||||||
|
"github.com/yaoapp/yao/share"
|
||||||
|
)
|
||||||
|
|
||||||
|
var shutdownSignal = make(chan bool, 1)
|
||||||
|
var dfs fs.FileSystem
|
||||||
|
var scripts = map[string][]byte{}
|
||||||
|
|
||||||
|
type cloudCall struct {
|
||||||
|
Method string `json:"method"`
|
||||||
|
Args []interface{} `json:"args,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start start the studio api server
|
||||||
|
func Start(cfg config.Config) (err error) {
|
||||||
|
|
||||||
|
// recive interrupt signal
|
||||||
|
interrupt := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT)
|
||||||
|
|
||||||
|
errCh := make(chan error, 1)
|
||||||
|
|
||||||
|
// Set router
|
||||||
|
router := gin.Default()
|
||||||
|
setRouter(router)
|
||||||
|
|
||||||
|
// Server setting
|
||||||
|
addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Studio.Port)
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: addr,
|
||||||
|
Handler: router,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen
|
||||||
|
l, err := net.Listen("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
log.Info("[Studio] %s Close Serve", addr)
|
||||||
|
err = srv.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("[Studio] Close Serve Error (%v)", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// start serve
|
||||||
|
go func() {
|
||||||
|
log.Info("[Studio] Starting: %s", addr)
|
||||||
|
if err := srv.Serve(l); err != nil && err != http.ErrServerClosed {
|
||||||
|
errCh <- err
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
|
||||||
|
case <-shutdownSignal:
|
||||||
|
log.Info("[Studio] %s Exit (Manual)", addr)
|
||||||
|
return err
|
||||||
|
|
||||||
|
case <-interrupt:
|
||||||
|
log.Info("[Studio] %s Exit (Interrupt) ", addr)
|
||||||
|
return err
|
||||||
|
|
||||||
|
case err := <-errCh:
|
||||||
|
log.Error("[Studio] %s Error (%v)", addr, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop stop the studio api server
|
||||||
|
func Stop() {
|
||||||
|
shutdownSignal <- true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load studio config
|
||||||
|
func Load(cfg config.Config) error {
|
||||||
|
err := loadDSL(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return loadScripts(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadDSL(cfg config.Config) error {
|
||||||
|
|
||||||
|
root, err := filepath.Abs(cfg.Root)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
scriptRoot := filepath.Join(root, "scripts")
|
||||||
|
dataRoot := filepath.Join(root, "data")
|
||||||
|
dslDenyList := []string{scriptRoot, dataRoot}
|
||||||
|
dfs = dsl.New(root).DenyAbs(dslDenyList...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadScripts(cfg config.Config) error {
|
||||||
|
root, err := filepath.Abs(cfg.Root)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
studioRoot := filepath.Join(root, "studio")
|
||||||
|
return loadScriptFrom(studioRoot)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load script From dir
|
||||||
|
func loadScriptFrom(dir string) error {
|
||||||
|
|
||||||
|
if share.DirNotExists(dir) {
|
||||||
|
log.Warn("[Studio] Load %s does not exists", dir)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
messages := []string{}
|
||||||
|
err := share.Walk(dir, ".js", func(root, filename string) {
|
||||||
|
name := fmt.Sprintf("__yao.studio.%s", share.SpecName(root, filename))
|
||||||
|
err := gou.Yao.Load(filename, name)
|
||||||
|
if err != nil {
|
||||||
|
messages = append(messages, err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(messages) > 0 {
|
||||||
|
return fmt.Errorf("[Studio] Load %s", strings.Join(messages, ";"))
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
|
||||||
78
studio/studio_test.go
Normal file
78
studio/studio_test.go
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
package studio
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/yaoapp/gou"
|
||||||
|
"github.com/yaoapp/yao/config"
|
||||||
|
"github.com/yaoapp/yao/network"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLoad(t *testing.T) {
|
||||||
|
err := Load(config.Conf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
assert.NotNil(t, dfs)
|
||||||
|
|
||||||
|
res, err := gou.Yao.Engine.Call(map[string]interface{}{}, "__yao.studio.table", "Ping")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, "PONG", res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStartStop(t *testing.T) {
|
||||||
|
var err error
|
||||||
|
go func() { err = Start(config.Conf) }()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
Stop()
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStartStopError(t *testing.T) {
|
||||||
|
var err error
|
||||||
|
go func() { err = Start(config.Conf) }()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
go func() { err = Start(config.Conf) }()
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
|
||||||
|
Stop()
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetAPI(t *testing.T) {
|
||||||
|
|
||||||
|
Load(config.Conf)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
go func() { err = Start(config.Conf) }()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer Stop()
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
|
||||||
|
url := fmt.Sprintf("http://127.0.0.1:%d/dsl/readfile?name=/models/user.json", config.Conf.Studio.Port)
|
||||||
|
res := network.RequestGet(url, nil, nil)
|
||||||
|
assert.Equal(t, "用户", res.Data.(map[string]interface{})["name"])
|
||||||
|
|
||||||
|
url = fmt.Sprintf("http://127.0.0.1:%d/dsl/readdir?name=/models", config.Conf.Studio.Port)
|
||||||
|
res = network.RequestGet(url, nil, nil)
|
||||||
|
assert.Equal(t, 11, len(res.Data.([]interface{})))
|
||||||
|
|
||||||
|
url = fmt.Sprintf("http://127.0.0.1:%d/dsl/readdir?name=/models&recursive=1", config.Conf.Studio.Port)
|
||||||
|
res = network.RequestGet(url, nil, nil)
|
||||||
|
assert.Equal(t, 12, len(res.Data.([]interface{})))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -385,17 +385,22 @@ func ProcessExport(process *gou.Process) interface{} {
|
||||||
WithSID(process.Sid).
|
WithSID(process.Sid).
|
||||||
Run()
|
Run()
|
||||||
|
|
||||||
// After Hook
|
|
||||||
response = table.After(table.Hooks.AfterSearch, response, []interface{}{param, page, pagesize}, process.Sid)
|
|
||||||
|
|
||||||
if debug {
|
if debug {
|
||||||
bytes, _ := jsoniter.Marshal(response)
|
bytes, _ := jsoniter.Marshal(response)
|
||||||
log.Info("[Export] %s %d %d Prepare: %s", filename, page, pagesize, string(bytes))
|
log.Info("[Export] %s %d %d Prepare: %s", filename, page, pagesize, string(bytes))
|
||||||
}
|
}
|
||||||
|
|
||||||
res, ok := response.(map[string]interface{})
|
// After Hook
|
||||||
|
respAfterHook := table.After(table.Hooks.AfterSearch, response, []interface{}{param, page, pagesize}, process.Sid)
|
||||||
|
|
||||||
|
if debug {
|
||||||
|
bytes, _ := jsoniter.Marshal(respAfterHook)
|
||||||
|
log.Info("[Export] %s %d %d Prepare After: %s", filename, page, pagesize, string(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
res, ok := respAfterHook.(map[string]interface{})
|
||||||
if !ok {
|
if !ok {
|
||||||
res, ok = response.(maps.MapStrAny)
|
res, ok = respAfterHook.(maps.MapStrAny)
|
||||||
if !ok {
|
if !ok {
|
||||||
page = -1
|
page = -1
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
33
tests/studio/table.js
Normal file
33
tests/studio/table.js
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
/**
|
||||||
|
* Create table
|
||||||
|
* @param {*} name
|
||||||
|
*/
|
||||||
|
function Make(name) {
|
||||||
|
var fs = new FS("dsl");
|
||||||
|
var model = GetModel(name);
|
||||||
|
var table = JSON.stringify({
|
||||||
|
name: `${model.name} Admin`,
|
||||||
|
actions: { bind: { model: name } },
|
||||||
|
});
|
||||||
|
fs.WriteFile(`/tables/auto/${name}.tab.json`, table);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Model
|
||||||
|
* @param {*} name
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function Model(name) {
|
||||||
|
var fs = new FS("dsl");
|
||||||
|
var file = `/models/${name}.mod.json`;
|
||||||
|
var data = fs.ReadFile(file);
|
||||||
|
return JSON.parse(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* for unit tests
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function Ping() {
|
||||||
|
return "PONG";
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue