feat: initial commit
This commit is contained in:
1
pkg/module/.gitignore
vendored
Normal file
1
pkg/module/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.sqlite
|
109
pkg/module/authorization.go
Normal file
109
pkg/module/authorization.go
Normal file
@ -0,0 +1,109 @@
|
||||
package module
|
||||
|
||||
// import (
|
||||
// "context"
|
||||
// "sync"
|
||||
|
||||
// "forge.cadoles.com/arcad/edge/pkg/app"
|
||||
// "forge.cadoles.com/arcad/edge/pkg/bus"
|
||||
// "forge.cadoles.com/arcad/edge/pkg/repository"
|
||||
// "github.com/dop251/goja"
|
||||
// "github.com/pkg/errors"
|
||||
// "gitlab.com/wpetit/goweb/logger"
|
||||
// )
|
||||
|
||||
// type AuthorizationModule struct {
|
||||
// appID app.ID
|
||||
// bus bus.Bus
|
||||
// backend *app.Backend
|
||||
// admins sync.Map
|
||||
// }
|
||||
|
||||
// func (m *AuthorizationModule) Name() string {
|
||||
// return "authorization"
|
||||
// }
|
||||
|
||||
// func (m *AuthorizationModule) Export(export *goja.Object) {
|
||||
// if err := export.Set("isAdmin", m.isAdmin); err != nil {
|
||||
// panic(errors.Wrap(err, "could not set 'register' function"))
|
||||
// }
|
||||
// }
|
||||
|
||||
// func (m *AuthorizationModule) isAdmin(call goja.FunctionCall) goja.Value {
|
||||
// userID := call.Argument(0).String()
|
||||
// if userID == "" {
|
||||
// panic(errors.New("first argument must be a user id"))
|
||||
// }
|
||||
|
||||
// rawValue, exists := m.admins.Load(repository.UserID(userID))
|
||||
// if !exists {
|
||||
// return m.backend.ToValue(false)
|
||||
// }
|
||||
|
||||
// isAdmin, ok := rawValue.(bool)
|
||||
// if !ok {
|
||||
// return m.backend.ToValue(false)
|
||||
// }
|
||||
|
||||
// return m.backend.ToValue(isAdmin)
|
||||
// }
|
||||
|
||||
// func (m *AuthorizationModule) handleEvents() {
|
||||
// ctx := logger.With(context.Background(), logger.F("moduleAppID", m.appID))
|
||||
|
||||
// ns := AppMessageNamespace(m.appID)
|
||||
|
||||
// userConnectedMessages, err := m.bus.Subscribe(ctx, ns, MessageTypeUserConnected)
|
||||
// if err != nil {
|
||||
// panic(errors.WithStack(err))
|
||||
// }
|
||||
|
||||
// userDisconnectedMessages, err := m.bus.Subscribe(ctx, ns, MessageTypeUserDisconnected)
|
||||
// if err != nil {
|
||||
// panic(errors.WithStack(err))
|
||||
// }
|
||||
|
||||
// defer func() {
|
||||
// m.bus.Unsubscribe(ctx, ns, MessageTypeUserConnected, userConnectedMessages)
|
||||
// m.bus.Unsubscribe(ctx, ns, MessageTypeUserDisconnected, userDisconnectedMessages)
|
||||
// }()
|
||||
|
||||
// for {
|
||||
// select {
|
||||
// case msg := <-userConnectedMessages:
|
||||
// userConnectedMsg, ok := msg.(*MessageUserConnected)
|
||||
// if !ok {
|
||||
// continue
|
||||
// }
|
||||
|
||||
// logger.Debug(ctx, "user connected", logger.F("msg", userConnectedMsg))
|
||||
|
||||
// m.admins.Store(userConnectedMsg.UserID, userConnectedMsg.IsAdmin)
|
||||
|
||||
// case msg := <-userDisconnectedMessages:
|
||||
// userDisconnectedMsg, ok := msg.(*MessageUserDisconnected)
|
||||
// if !ok {
|
||||
// continue
|
||||
// }
|
||||
|
||||
// logger.Debug(ctx, "user disconnected", logger.F("msg", userDisconnectedMsg))
|
||||
|
||||
// m.admins.Delete(userDisconnectedMsg.UserID)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// func AuthorizationModuleFactory(b bus.Bus) app.BackendModuleFactory {
|
||||
// return func(appID app.ID, backend *app.Backend) app.BackendModule {
|
||||
// mod := &AuthorizationModule{
|
||||
// appID: appID,
|
||||
// bus: b,
|
||||
// backend: backend,
|
||||
// admins: sync.Map{},
|
||||
// }
|
||||
|
||||
// go mod.handleEvents()
|
||||
|
||||
// return mod
|
||||
// }
|
||||
// }
|
103
pkg/module/authorization_test.go
Normal file
103
pkg/module/authorization_test.go
Normal file
@ -0,0 +1,103 @@
|
||||
package module
|
||||
|
||||
// import (
|
||||
// "context"
|
||||
// "io/ioutil"
|
||||
// "testing"
|
||||
// "time"
|
||||
|
||||
// "forge.cadoles.com/arcad/edge/pkg/app"
|
||||
// "forge.cadoles.com/arcad/edge/pkg/bus/memory"
|
||||
// )
|
||||
|
||||
// func TestAuthorizationModule(t *testing.T) {
|
||||
// t.Parallel()
|
||||
|
||||
// testAppID := app.ID("test-app")
|
||||
|
||||
// b := memory.NewBus()
|
||||
|
||||
// backend := app.NewBackend(testAppID,
|
||||
// ConsoleModuleFactory(),
|
||||
// AuthorizationModuleFactory(b),
|
||||
// )
|
||||
|
||||
// data, err := ioutil.ReadFile("testdata/authorization.js")
|
||||
// if err != nil {
|
||||
// t.Fatal(err)
|
||||
// }
|
||||
|
||||
// if err := backend.Load(string(data)); err != nil {
|
||||
// t.Fatal(err)
|
||||
// }
|
||||
|
||||
// backend.Start()
|
||||
// defer backend.Stop()
|
||||
|
||||
// if err := backend.OnInit(); err != nil {
|
||||
// t.Error(err)
|
||||
// }
|
||||
|
||||
// // Test non connected user
|
||||
|
||||
// retValue, err := backend.ExecFuncByName("isAdmin", testUserID)
|
||||
// if err != nil {
|
||||
// t.Error(err)
|
||||
// }
|
||||
|
||||
// isAdmin := retValue.ToBoolean()
|
||||
|
||||
// if e, g := false, isAdmin; e != g {
|
||||
// t.Errorf("isAdmin: expected '%v', got '%v'", e, g)
|
||||
// }
|
||||
|
||||
// // Test user connection as normal user
|
||||
|
||||
// ctx := context.Background()
|
||||
|
||||
// b.Publish(ctx, NewMessageUserConnected(testAppID, testUserID, false))
|
||||
// time.Sleep(2 * time.Second)
|
||||
|
||||
// retValue, err = backend.ExecFuncByName("isAdmin", testUserID)
|
||||
// if err != nil {
|
||||
// t.Error(err)
|
||||
// }
|
||||
|
||||
// isAdmin = retValue.ToBoolean()
|
||||
|
||||
// if e, g := false, isAdmin; e != g {
|
||||
// t.Errorf("isAdmin: expected '%v', got '%v'", e, g)
|
||||
// }
|
||||
|
||||
// // Test user connection as admin
|
||||
|
||||
// b.Publish(ctx, NewMessageUserConnected(testAppID, testUserID, true))
|
||||
// time.Sleep(2 * time.Second)
|
||||
|
||||
// retValue, err = backend.ExecFuncByName("isAdmin", testUserID)
|
||||
// if err != nil {
|
||||
// t.Error(err)
|
||||
// }
|
||||
|
||||
// isAdmin = retValue.ToBoolean()
|
||||
|
||||
// if e, g := true, isAdmin; e != g {
|
||||
// t.Errorf("isAdmin: expected '%v', got '%v'", e, g)
|
||||
// }
|
||||
|
||||
// // Test user disconnection
|
||||
|
||||
// b.Publish(ctx, NewMessageUserDisconnected(testAppID, testUserID))
|
||||
// time.Sleep(2 * time.Second)
|
||||
|
||||
// retValue, err = backend.ExecFuncByName("isAdmin", testUserID)
|
||||
// if err != nil {
|
||||
// t.Error(err)
|
||||
// }
|
||||
|
||||
// isAdmin = retValue.ToBoolean()
|
||||
|
||||
// if e, g := false, isAdmin; e != g {
|
||||
// t.Errorf("isAdmin: expected '%v', got '%v'", e, g)
|
||||
// }
|
||||
// }
|
44
pkg/module/console.go
Normal file
44
pkg/module/console.go
Normal file
@ -0,0 +1,44 @@
|
||||
package module
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"github.com/dop251/goja"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type ConsoleModule struct{}
|
||||
|
||||
func (m *ConsoleModule) Name() string {
|
||||
return "console"
|
||||
}
|
||||
|
||||
func (m *ConsoleModule) log(call goja.FunctionCall) goja.Value {
|
||||
var sb strings.Builder
|
||||
|
||||
for _, arg := range call.Arguments {
|
||||
sb.WriteString(fmt.Sprintf("%+v", arg.Export()))
|
||||
sb.WriteString(" ")
|
||||
}
|
||||
|
||||
logger.Debug(context.Background(), sb.String())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ConsoleModule) Export(export *goja.Object) {
|
||||
if err := export.Set("log", m.log); err != nil {
|
||||
panic(errors.Wrap(err, "could not set 'log' function"))
|
||||
}
|
||||
}
|
||||
|
||||
func ConsoleModuleFactory() app.BackendModuleFactory {
|
||||
return func(backend *app.Backend) app.BackendModule {
|
||||
return &ConsoleModule{}
|
||||
}
|
||||
}
|
55
pkg/module/context.go
Normal file
55
pkg/module/context.go
Normal file
@ -0,0 +1,55 @@
|
||||
package module
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"github.com/dop251/goja"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func assertContext(v goja.Value, r *goja.Runtime) context.Context {
|
||||
if c, ok := v.Export().(context.Context); ok {
|
||||
return c
|
||||
}
|
||||
|
||||
panic(r.NewTypeError("value should be a context"))
|
||||
}
|
||||
|
||||
type ContextModule struct {
|
||||
ctx BackendContext
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
func (m *ContextModule) Name() string {
|
||||
return "context"
|
||||
}
|
||||
|
||||
func (m *ContextModule) get(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
return rt.ToValue(m.ctx)
|
||||
}
|
||||
|
||||
func (m *ContextModule) Export(export *goja.Object) {
|
||||
if err := export.Set("get", m.get); err != nil {
|
||||
panic(errors.Wrap(err, "could not set 'get' function"))
|
||||
}
|
||||
}
|
||||
|
||||
func ContextModuleFactory() app.BackendModuleFactory {
|
||||
return func(backend *app.Backend) app.BackendModule {
|
||||
return &ContextModule{
|
||||
ctx: BackendContext{
|
||||
Context: context.Background(),
|
||||
},
|
||||
mutex: sync.RWMutex{},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type BackendContext struct {
|
||||
context.Context
|
||||
}
|
5
pkg/module/error.go
Normal file
5
pkg/module/error.go
Normal file
@ -0,0 +1,5 @@
|
||||
package module
|
||||
|
||||
import "github.com/pkg/errors"
|
||||
|
||||
var ErrUnexpectedArgumentsNumber = errors.New("unexpected number of arguments")
|
293
pkg/module/file.go
Normal file
293
pkg/module/file.go
Normal file
@ -0,0 +1,293 @@
|
||||
package module
|
||||
|
||||
// import (
|
||||
// "context"
|
||||
// "fmt"
|
||||
// "io"
|
||||
// "mime/multipart"
|
||||
// "os"
|
||||
// "path/filepath"
|
||||
// "time"
|
||||
|
||||
// "forge.cadoles.com/arcad/edge/pkg/app"
|
||||
// "forge.cadoles.com/arcad/edge/pkg/bus"
|
||||
// "github.com/dop251/goja"
|
||||
// "github.com/genjidb/genji"
|
||||
// "github.com/genjidb/genji/database"
|
||||
// "github.com/genjidb/genji/document"
|
||||
// "github.com/google/uuid"
|
||||
// "github.com/pkg/errors"
|
||||
// "github.com/spf13/afero"
|
||||
// "gitlab.com/wpetit/goweb/logger"
|
||||
// )
|
||||
|
||||
// const (
|
||||
// fileKeyID = "id"
|
||||
// collectionFiles = "files"
|
||||
// )
|
||||
|
||||
// type FileEntry struct {
|
||||
// ID string
|
||||
// Filename string
|
||||
// Size int64
|
||||
// ContentType string
|
||||
// CreatedAt string
|
||||
// Metadata map[string]interface{}
|
||||
// }
|
||||
|
||||
// type FileModule struct {
|
||||
// appID app.ID
|
||||
// backend *app.Backend
|
||||
// bus bus.Bus
|
||||
// db *AppDatabaseMixin
|
||||
// dataDir string
|
||||
// fs afero.Fs
|
||||
// }
|
||||
|
||||
// func (m *FileModule) Name() string {
|
||||
// return "file"
|
||||
// }
|
||||
|
||||
// func (m *FileModule) Export(export *goja.Object) {
|
||||
// // if err := export.Set("ls", m.ls); err != nil {
|
||||
// // panic(errors.Wrap(err, "could not set 'save' function"))
|
||||
// // }
|
||||
// }
|
||||
|
||||
// func (m *FileModule) logAndPanic(msg string, err error) {
|
||||
// err = errors.Wrap(err, msg)
|
||||
// logger.Error(context.Background(), msg, logger.E(err))
|
||||
// panic(errors.WithStack(err))
|
||||
// }
|
||||
|
||||
// func (m *FileModule) handleMessages() {
|
||||
// ctx := context.Background()
|
||||
|
||||
// ns := createAppMessageNamespace(m.appID)
|
||||
|
||||
// go func() {
|
||||
// err := m.bus.Reply(ctx, ns, MessageTypeUploadRequest, func(msg bus.Message) (bus.Message, error) {
|
||||
// uploadRequest, ok := msg.(*MessageUploadRequest)
|
||||
// if !ok {
|
||||
// return nil, errors.Wrapf(bus.ErrUnexpectedMessage, "expected message upload request, got '%s'", msg.MessageType())
|
||||
// }
|
||||
|
||||
// res, err := m.handleUploadRequest(uploadRequest)
|
||||
// if err != nil {
|
||||
// logger.Error(ctx, "could not handle upload request", logger.E(errors.WithStack(err)))
|
||||
|
||||
// return nil, errors.WithStack(err)
|
||||
// }
|
||||
|
||||
// logger.Debug(ctx, "upload request response", logger.F("response", res))
|
||||
|
||||
// return res, nil
|
||||
// })
|
||||
// if err != nil {
|
||||
// panic(errors.WithStack(err))
|
||||
// }
|
||||
// }()
|
||||
|
||||
// err := m.bus.Reply(ctx, ns, MessageTypeDownloadRequest, func(msg bus.Message) (bus.Message, error) {
|
||||
// downloadRequest, ok := msg.(*MessageDownloadRequest)
|
||||
// if !ok {
|
||||
// return nil, errors.Wrapf(bus.ErrUnexpectedMessage, "expected message download request, got '%s'", msg.MessageType())
|
||||
// }
|
||||
|
||||
// res, err := m.handleDownloadRequest(downloadRequest)
|
||||
// if err != nil {
|
||||
// logger.Error(ctx, "could not handle download request", logger.E(errors.WithStack(err)))
|
||||
|
||||
// return nil, errors.WithStack(err)
|
||||
// }
|
||||
|
||||
// return res, nil
|
||||
// })
|
||||
// if err != nil {
|
||||
// panic(errors.WithStack(err))
|
||||
// }
|
||||
// }
|
||||
|
||||
// func (m *FileModule) handleUploadRequest(req *MessageUploadRequest) (*MessageUploadResponse, error) {
|
||||
// fileInfo := map[string]interface{}{
|
||||
// "filename": req.Header.Filename,
|
||||
// "contentType": req.Header.Header.Get("Content-Type"),
|
||||
// "size": req.Header.Size,
|
||||
// "metadata": req.Metadata,
|
||||
// }
|
||||
|
||||
// res := NewMessageUploadResponse(req.AppID, req.UserID, req.RequestID)
|
||||
// fileID := uuid.New().String()
|
||||
|
||||
// result, err := m.backend.ExecFuncByName("onFileUpload", req.UserID, fileID, fileInfo)
|
||||
// if err != nil {
|
||||
// if errors.Is(err, app.ErrFuncDoesNotExist) {
|
||||
// res.Allow = false
|
||||
|
||||
// return res, nil
|
||||
// }
|
||||
|
||||
// return nil, errors.WithStack(err)
|
||||
// }
|
||||
|
||||
// res.Allow = result.ToBoolean()
|
||||
|
||||
// if res.Allow {
|
||||
// if err := m.saveFile(fileID, req.UserID, req.Header, req.File, req.Metadata); err != nil {
|
||||
// return nil, errors.WithStack(err)
|
||||
// }
|
||||
|
||||
// res.FileID = fileID
|
||||
// }
|
||||
|
||||
// return res, nil
|
||||
// }
|
||||
|
||||
// func (m *FileModule) saveFile(fileID string, header *multipart.FileHeader, file multipart.File, metadata map[string]interface{}) error {
|
||||
// err := m.db.WithCollectionTx(collectionFiles, func(tx *genji.Tx) error {
|
||||
// entry := &FileEntry{
|
||||
// ID: fileID,
|
||||
// Filename: header.Filename,
|
||||
// Size: header.Size,
|
||||
// ContentType: header.Header.Get("Content-Type"),
|
||||
// CreatedAt: time.Now().UTC().String(),
|
||||
// Metadata: metadata,
|
||||
// }
|
||||
|
||||
// insertQuery := fmt.Sprintf("INSERT INTO `%s` VALUES ?", collectionFiles)
|
||||
// if err := tx.Exec(insertQuery, &entry); err != nil {
|
||||
// return errors.WithStack(err)
|
||||
// }
|
||||
|
||||
// fileDir := m.getFileDir(fileID)
|
||||
|
||||
// if err := m.fs.MkdirAll(fileDir, 0o755); err != nil {
|
||||
// return errors.WithStack(err)
|
||||
// }
|
||||
|
||||
// filePath := filepath.Join(fileDir, fileID)
|
||||
|
||||
// newFile, err := m.fs.Create(filePath)
|
||||
// if err != nil {
|
||||
// return errors.WithStack(err)
|
||||
// }
|
||||
|
||||
// defer newFile.Close()
|
||||
|
||||
// if _, err := io.Copy(newFile, file); err != nil {
|
||||
// return errors.WithStack(err)
|
||||
// }
|
||||
|
||||
// return nil
|
||||
// })
|
||||
// if err != nil {
|
||||
// return errors.WithStack(err)
|
||||
// }
|
||||
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// func (m *FileModule) handleDownloadRequest(req *MessageDownloadRequest) (*MessageDownloadResponse, error) {
|
||||
// res := NewMessageDownloadResponse(req.AppID, req.UserID, req.RequestID)
|
||||
|
||||
// result, err := m.backend.ExecFuncByName("onFileDownload", req.UserID, req.FileID)
|
||||
// if err != nil {
|
||||
// if errors.Is(err, app.ErrFuncDoesNotExist) {
|
||||
// res.Allow = false
|
||||
|
||||
// return res, nil
|
||||
// }
|
||||
|
||||
// return nil, errors.WithStack(err)
|
||||
// }
|
||||
|
||||
// res.Allow = result.ToBoolean()
|
||||
|
||||
// file, fileEntry, err := m.openFile(req.FileID)
|
||||
// if err != nil && !os.IsNotExist(errors.Cause(err)) {
|
||||
// return nil, errors.WithStack(err)
|
||||
// }
|
||||
|
||||
// if file != nil {
|
||||
// res.File = file
|
||||
// }
|
||||
|
||||
// if fileEntry != nil {
|
||||
// res.Filename = fileEntry.Filename
|
||||
// res.ContentType = fileEntry.ContentType
|
||||
// res.Size = fileEntry.Size
|
||||
// }
|
||||
|
||||
// return res, nil
|
||||
// }
|
||||
|
||||
// func (m *FileModule) openFile(fileID string) (afero.File, *FileEntry, error) {
|
||||
// var (
|
||||
// fileEntry *FileEntry
|
||||
// file afero.File
|
||||
// )
|
||||
|
||||
// err := m.db.WithCollectionTx(collectionFiles, func(tx *genji.Tx) error {
|
||||
// selectQuery := fmt.Sprintf("SELECT * FROM `%s` WHERE id = ?", collectionFiles)
|
||||
|
||||
// doc, err := tx.QueryDocument(selectQuery, fileID)
|
||||
// if err != nil {
|
||||
// if errors.Is(err, database.ErrDocumentNotFound) {
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// return errors.WithStack(err)
|
||||
// }
|
||||
|
||||
// fileEntry = &FileEntry{}
|
||||
|
||||
// if err := document.StructScan(doc, fileEntry); err != nil {
|
||||
// return errors.WithStack(err)
|
||||
// }
|
||||
|
||||
// fileDir := m.getFileDir(fileID)
|
||||
// filePath := filepath.Join(fileDir, fileID)
|
||||
|
||||
// file, err = m.fs.Open(filePath)
|
||||
// if err != nil {
|
||||
// file = nil
|
||||
|
||||
// return errors.WithStack(err)
|
||||
// }
|
||||
|
||||
// return nil
|
||||
// })
|
||||
// if err != nil {
|
||||
// return nil, nil, errors.WithStack(err)
|
||||
// }
|
||||
|
||||
// return file, fileEntry, nil
|
||||
// }
|
||||
|
||||
// func (m *FileModule) getFileDir(fileID string) string {
|
||||
// return filepath.Join(m.dataDir, string(m.appID), "files", fileID[0:2], fileID[2:4], fileID[4:6])
|
||||
// }
|
||||
|
||||
// func FileModuleFactory(dataDir string, bus bus.Bus) app.BackendModuleFactory {
|
||||
// return func(appID app.ID, backend *app.Backend) app.BackendModule {
|
||||
// var fs afero.Fs
|
||||
// if dataDir == inMemory {
|
||||
// fs = afero.NewMemMapFs()
|
||||
// } else {
|
||||
// fs = afero.NewOsFs()
|
||||
// }
|
||||
|
||||
// mod := &FileModule{
|
||||
// dataDir: dataDir,
|
||||
// appID: appID,
|
||||
// bus: bus,
|
||||
// backend: backend,
|
||||
// db: NewAppDatabaseMixin(dataDir, "file.db", appID),
|
||||
// fs: fs,
|
||||
// }
|
||||
|
||||
// go mod.handleMessages()
|
||||
|
||||
// return mod
|
||||
// }
|
||||
// }
|
127
pkg/module/file_message.go
Normal file
127
pkg/module/file_message.go
Normal file
@ -0,0 +1,127 @@
|
||||
package module
|
||||
|
||||
// import (
|
||||
// "io"
|
||||
// "mime/multipart"
|
||||
|
||||
// "forge.cadoles.com/arcad/edge/pkg/app"
|
||||
// "forge.cadoles.com/arcad/edge/pkg/bus"
|
||||
// "github.com/google/uuid"
|
||||
// )
|
||||
|
||||
// const (
|
||||
// MessageTypeUploadRequest bus.MessageType = "uploadRequest"
|
||||
// MessageTypeUploadResponse bus.MessageType = "uploadResponse"
|
||||
// MessageTypeDownloadRequest bus.MessageType = "downloadRequest"
|
||||
// MessageTypeDownloadResponse bus.MessageType = "downloadResponse"
|
||||
// )
|
||||
|
||||
// type MessageUploadRequest struct {
|
||||
// AppID app.ID
|
||||
// RequestID string
|
||||
// Header *multipart.FileHeader
|
||||
// File multipart.File
|
||||
// Metadata map[string]interface{}
|
||||
|
||||
// ns bus.MessageNamespace
|
||||
// }
|
||||
|
||||
// func (m *MessageUploadRequest) MessageNamespace() bus.MessageNamespace {
|
||||
// return m.ns
|
||||
// }
|
||||
|
||||
// func (m *MessageUploadRequest) MessageType() bus.MessageType {
|
||||
// return MessageTypeUploadRequest
|
||||
// }
|
||||
|
||||
// func NewMessageUploadRequest(appID app.ID, header *multipart.FileHeader, file multipart.File, metadata map[string]interface{}) *MessageUploadRequest {
|
||||
// return &MessageUploadRequest{
|
||||
// AppID: appID,
|
||||
// RequestID: uuid.New().String(),
|
||||
// Header: header,
|
||||
// File: file,
|
||||
// Metadata: metadata,
|
||||
|
||||
// ns: AppMessageNamespace(appID),
|
||||
// }
|
||||
// }
|
||||
|
||||
// type MessageUploadResponse struct {
|
||||
// AppID app.ID
|
||||
// RequestID string
|
||||
// FileID string
|
||||
// Allow bool
|
||||
|
||||
// ns bus.MessageNamespace
|
||||
// }
|
||||
|
||||
// func (m *MessageUploadResponse) MessageNamespace() bus.MessageNamespace {
|
||||
// return m.ns
|
||||
// }
|
||||
|
||||
// func (m *MessageUploadResponse) MessageType() bus.MessageType {
|
||||
// return MessageTypeUploadResponse
|
||||
// }
|
||||
|
||||
// func NewMessageUploadResponse(appID app.ID, requestID string) *MessageUploadResponse {
|
||||
// return &MessageUploadResponse{
|
||||
// AppID: appID,
|
||||
// RequestID: requestID,
|
||||
|
||||
// ns: AppMessageNamespace(appID),
|
||||
// }
|
||||
// }
|
||||
|
||||
// type MessageDownloadRequest struct {
|
||||
// AppID app.ID
|
||||
// RequestID string
|
||||
// FileID string
|
||||
|
||||
// ns bus.MessageNamespace
|
||||
// }
|
||||
|
||||
// func (m *MessageDownloadRequest) MessageNamespace() bus.MessageNamespace {
|
||||
// return m.ns
|
||||
// }
|
||||
|
||||
// func (m *MessageDownloadRequest) MessageType() bus.MessageType {
|
||||
// return MessageTypeDownloadRequest
|
||||
// }
|
||||
|
||||
// func NewMessageDownloadRequest(appID app.ID, fileID string) *MessageDownloadRequest {
|
||||
// return &MessageDownloadRequest{
|
||||
// AppID: appID,
|
||||
// RequestID: uuid.New().String(),
|
||||
// FileID: fileID,
|
||||
// ns: AppMessageNamespace(appID),
|
||||
// }
|
||||
// }
|
||||
|
||||
// type MessageDownloadResponse struct {
|
||||
// AppID app.ID
|
||||
// RequestID string
|
||||
// Allow bool
|
||||
// File io.ReadCloser
|
||||
// ContentType string
|
||||
// Filename string
|
||||
// Size int64
|
||||
|
||||
// ns bus.MessageNamespace
|
||||
// }
|
||||
|
||||
// func (m *MessageDownloadResponse) MessageNamespace() bus.MessageNamespace {
|
||||
// return m.ns
|
||||
// }
|
||||
|
||||
// func (e *MessageDownloadResponse) MessageType() bus.MessageType {
|
||||
// return MessageTypeDownloadResponse
|
||||
// }
|
||||
|
||||
// func NewMessageDownloadResponse(appID app.ID, requestID string) *MessageDownloadResponse {
|
||||
// return &MessageDownloadResponse{
|
||||
// AppID: appID,
|
||||
// RequestID: requestID,
|
||||
|
||||
// ns: AppMessageNamespace(appID),
|
||||
// }
|
||||
// }
|
191
pkg/module/lifecycle.go
Normal file
191
pkg/module/lifecycle.go
Normal file
@ -0,0 +1,191 @@
|
||||
package module
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||
"github.com/dop251/goja"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type LifecycleModule struct {
|
||||
backend *app.Backend
|
||||
bus bus.Bus
|
||||
}
|
||||
|
||||
func (m *LifecycleModule) Name() string {
|
||||
return "lifecycle"
|
||||
}
|
||||
|
||||
func (m *LifecycleModule) Export(export *goja.Object) {
|
||||
}
|
||||
|
||||
func (m *LifecycleModule) OnInit() error {
|
||||
if _, err := m.backend.ExecFuncByName("onInit"); err != nil {
|
||||
if errors.Is(err, app.ErrFuncDoesNotExist) {
|
||||
logger.Warn(context.Background(), "could not find onInit() function", logger.E(errors.WithStack(err)))
|
||||
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *LifecycleModule) handleMessages() {
|
||||
ctx := context.Background()
|
||||
|
||||
logger.Debug(
|
||||
ctx,
|
||||
"subscribing to bus messages",
|
||||
)
|
||||
|
||||
userConnectedMessages, err := m.bus.Subscribe(ctx, MessageNamespaceUserConnected)
|
||||
if err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
|
||||
userDisconnectedMessages, err := m.bus.Subscribe(ctx, MessageNamespaceUserDisconnected)
|
||||
if err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
|
||||
frontendMessageMessages, err := m.bus.Subscribe(ctx, MessageNamespaceFrontend)
|
||||
if err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
|
||||
defer func() {
|
||||
logger.Debug(
|
||||
ctx,
|
||||
"unsubscribing from bus messages",
|
||||
)
|
||||
|
||||
m.bus.Unsubscribe(ctx, MessageNamespaceFrontend, frontendMessageMessages)
|
||||
m.bus.Unsubscribe(ctx, MessageNamespaceUserDisconnected, userDisconnectedMessages)
|
||||
m.bus.Unsubscribe(ctx, MessageNamespaceUserConnected, userConnectedMessages)
|
||||
}()
|
||||
|
||||
for {
|
||||
logger.Debug(
|
||||
ctx,
|
||||
"waiting for next message",
|
||||
)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
logger.Debug(
|
||||
ctx,
|
||||
"context done",
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
case msg := <-userConnectedMessages:
|
||||
userConnected, ok := msg.(*UserConnectedMessage)
|
||||
if !ok {
|
||||
logger.Error(
|
||||
ctx,
|
||||
"unexpected message type",
|
||||
logger.F("message", msg),
|
||||
)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Debug(
|
||||
ctx,
|
||||
"received user connected message",
|
||||
logger.F("message", userConnected),
|
||||
)
|
||||
|
||||
if _, err := m.backend.ExecFuncByName("onUserConnect"); err != nil {
|
||||
if errors.Is(err, app.ErrFuncDoesNotExist) {
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Error(
|
||||
ctx,
|
||||
"on user connected error",
|
||||
logger.E(err),
|
||||
)
|
||||
}
|
||||
|
||||
case msg := <-userDisconnectedMessages:
|
||||
userDisconnected, ok := msg.(*UserDisconnectedMessage)
|
||||
if !ok {
|
||||
logger.Error(
|
||||
ctx,
|
||||
"unexpected message type",
|
||||
logger.F("message", msg),
|
||||
)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Debug(
|
||||
ctx,
|
||||
"received user disconnected message",
|
||||
logger.F("message", userDisconnected),
|
||||
)
|
||||
|
||||
if _, err := m.backend.ExecFuncByName("onUserDisconnect"); err != nil {
|
||||
if errors.Is(err, app.ErrFuncDoesNotExist) {
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Error(
|
||||
ctx,
|
||||
"on user disconnected error",
|
||||
logger.E(err),
|
||||
)
|
||||
}
|
||||
|
||||
case msg := <-frontendMessageMessages:
|
||||
frontendMessage, ok := msg.(*FrontendMessage)
|
||||
if !ok {
|
||||
logger.Error(
|
||||
ctx,
|
||||
"unexpected message type",
|
||||
logger.F("message", msg),
|
||||
)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Debug(
|
||||
ctx,
|
||||
"received frontend message",
|
||||
logger.F("message", frontendMessage),
|
||||
)
|
||||
|
||||
if _, err := m.backend.ExecFuncByName("onUserMessage", frontendMessage.Data); err != nil {
|
||||
if errors.Is(err, app.ErrFuncDoesNotExist) {
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Error(
|
||||
ctx,
|
||||
"on user message error",
|
||||
logger.E(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func LifecycleModuleFactory(bus bus.Bus) app.BackendModuleFactory {
|
||||
return func(backend *app.Backend) app.BackendModule {
|
||||
module := &LifecycleModule{
|
||||
backend: backend,
|
||||
bus: bus,
|
||||
}
|
||||
|
||||
go module.handleMessages()
|
||||
|
||||
return module
|
||||
}
|
||||
}
|
||||
|
||||
var _ app.InitializableModule = &LifecycleModule{}
|
56
pkg/module/lifecycle_message.go
Normal file
56
pkg/module/lifecycle_message.go
Normal file
@ -0,0 +1,56 @@
|
||||
package module
|
||||
|
||||
import (
|
||||
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||
)
|
||||
|
||||
const (
|
||||
MessageNamespaceFrontend bus.MessageNamespace = "frontend"
|
||||
MessageNamespaceBackend bus.MessageNamespace = "backend"
|
||||
MessageNamespaceUserConnected bus.MessageNamespace = "userConnected"
|
||||
MessageNamespaceUserDisconnected bus.MessageNamespace = "userDisconnected"
|
||||
)
|
||||
|
||||
type UserConnectedMessage struct{}
|
||||
|
||||
func (m *UserConnectedMessage) MessageNamespace() bus.MessageNamespace {
|
||||
return MessageNamespaceUserConnected
|
||||
}
|
||||
|
||||
func NewMessageUserConnected() *UserConnectedMessage {
|
||||
return &UserConnectedMessage{}
|
||||
}
|
||||
|
||||
type UserDisconnectedMessage struct{}
|
||||
|
||||
func (m *UserDisconnectedMessage) MessageNamespace() bus.MessageNamespace {
|
||||
return MessageNamespaceUserDisconnected
|
||||
}
|
||||
|
||||
func NewMessageUserDisconnected() *UserDisconnectedMessage {
|
||||
return &UserDisconnectedMessage{}
|
||||
}
|
||||
|
||||
type BackendMessage struct {
|
||||
Data interface{}
|
||||
}
|
||||
|
||||
func (m *BackendMessage) MessageNamespace() bus.MessageNamespace {
|
||||
return MessageNamespaceBackend
|
||||
}
|
||||
|
||||
func NewBackendMessage(data interface{}) *BackendMessage {
|
||||
return &BackendMessage{data}
|
||||
}
|
||||
|
||||
type FrontendMessage struct {
|
||||
Data map[string]interface{}
|
||||
}
|
||||
|
||||
func (m *FrontendMessage) MessageNamespace() bus.MessageNamespace {
|
||||
return MessageNamespaceFrontend
|
||||
}
|
||||
|
||||
func NewFrontendMessage(data map[string]interface{}) *FrontendMessage {
|
||||
return &FrontendMessage{data}
|
||||
}
|
68
pkg/module/net.go
Normal file
68
pkg/module/net.go
Normal file
@ -0,0 +1,68 @@
|
||||
package module
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||
"github.com/dop251/goja"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type NetModule struct {
|
||||
backend *app.Backend
|
||||
bus bus.Bus
|
||||
}
|
||||
|
||||
func (m *NetModule) Name() string {
|
||||
return "net"
|
||||
}
|
||||
|
||||
func (m *NetModule) Export(export *goja.Object) {
|
||||
if err := export.Set("broadcast", m.broadcast); err != nil {
|
||||
panic(errors.Wrap(err, "could not set 'broadcast' function"))
|
||||
}
|
||||
|
||||
if err := export.Set("send", m.send); err != nil {
|
||||
panic(errors.Wrap(err, "could not set 'send' function"))
|
||||
}
|
||||
}
|
||||
|
||||
func (m *NetModule) broadcast(call goja.FunctionCall) goja.Value {
|
||||
if len(call.Arguments) < 1 {
|
||||
panic(m.backend.ToValue("invalid number of argument"))
|
||||
}
|
||||
|
||||
data := call.Arguments[0].Export()
|
||||
|
||||
msg := NewBackendMessage(data)
|
||||
if err := m.bus.Publish(context.Background(), msg); err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *NetModule) send(call goja.FunctionCall) goja.Value {
|
||||
if len(call.Arguments) < 1 {
|
||||
panic(m.backend.ToValue("invalid number of argument"))
|
||||
}
|
||||
|
||||
data := call.Arguments[0].Export()
|
||||
|
||||
msg := NewBackendMessage(data)
|
||||
if err := m.bus.Publish(context.Background(), msg); err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NetModuleFactory(bus bus.Bus) app.BackendModuleFactory {
|
||||
return func(backend *app.Backend) app.BackendModule {
|
||||
return &NetModule{
|
||||
backend: backend,
|
||||
bus: bus,
|
||||
}
|
||||
}
|
||||
}
|
238
pkg/module/rpc.go
Normal file
238
pkg/module/rpc.go
Normal file
@ -0,0 +1,238 @@
|
||||
package module
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||
"github.com/dop251/goja"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type RPCRequest struct {
|
||||
Method string
|
||||
Params interface{}
|
||||
ID interface{}
|
||||
}
|
||||
|
||||
type RPCError struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
type RPCResponse struct {
|
||||
Result interface{}
|
||||
Error *RPCError
|
||||
ID interface{}
|
||||
}
|
||||
|
||||
type RPCModule struct {
|
||||
backend *app.Backend
|
||||
bus bus.Bus
|
||||
callbacks sync.Map
|
||||
}
|
||||
|
||||
func (m *RPCModule) Name() string {
|
||||
return "rpc"
|
||||
}
|
||||
|
||||
func (m *RPCModule) Export(export *goja.Object) {
|
||||
if err := export.Set("register", m.register); err != nil {
|
||||
panic(errors.Wrap(err, "could not set 'register' function"))
|
||||
}
|
||||
|
||||
if err := export.Set("unregister", m.unregister); err != nil {
|
||||
panic(errors.Wrap(err, "could not set 'unregister' function"))
|
||||
}
|
||||
}
|
||||
|
||||
func (m *RPCModule) register(call goja.FunctionCall) goja.Value {
|
||||
fnName := call.Argument(0).String()
|
||||
if fnName == "" {
|
||||
panic(errors.New("First argument must be a function name"))
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
logger.Debug(ctx, "registering method", logger.F("method", fnName))
|
||||
|
||||
m.callbacks.Store(fnName, nil)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *RPCModule) unregister(call goja.FunctionCall) goja.Value {
|
||||
fnName := call.Argument(0).String()
|
||||
if fnName == "" {
|
||||
panic(errors.New("First argument must be a function name"))
|
||||
}
|
||||
|
||||
m.callbacks.Delete(fnName)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *RPCModule) handleMessages() {
|
||||
ctx := context.Background()
|
||||
|
||||
frontendMessages, err := m.bus.Subscribe(ctx, MessageNamespaceFrontend)
|
||||
if err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
|
||||
defer func() {
|
||||
m.bus.Unsubscribe(ctx, MessageNamespaceFrontend, frontendMessages)
|
||||
}()
|
||||
|
||||
sendRes := func(ctx context.Context, req *RPCRequest, result goja.Value) {
|
||||
res := &RPCResponse{
|
||||
ID: req.ID,
|
||||
Result: result.Export(),
|
||||
}
|
||||
|
||||
logger.Debug(ctx, "sending rpc response", logger.F("response", res))
|
||||
|
||||
if err := m.sendResponse(ctx, res); err != nil {
|
||||
logger.Error(
|
||||
ctx, "could not send response",
|
||||
logger.E(errors.WithStack(err)),
|
||||
logger.F("response", res),
|
||||
logger.F("request", req),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for msg := range frontendMessages {
|
||||
frontendMessage, ok := msg.(*FrontendMessage)
|
||||
if !ok {
|
||||
logger.Warn(ctx, "unexpected bus message", logger.F("message", msg))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
ok, req := m.isRPCRequest(frontendMessage)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Debug(ctx, "received rpc request", logger.F("request", req))
|
||||
|
||||
if _, exists := m.callbacks.Load(req.Method); !exists {
|
||||
logger.Debug(ctx, "method not found", logger.F("req", req))
|
||||
|
||||
if err := m.sendMethodNotFoundResponse(ctx, req); err != nil {
|
||||
logger.Error(
|
||||
ctx, "could not send method not found response",
|
||||
logger.E(errors.WithStack(err)),
|
||||
logger.F("request", req),
|
||||
)
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
result, err := m.backend.ExecFuncByName(req.Method, req.Params)
|
||||
if err != nil {
|
||||
if err := m.sendErrorResponse(ctx, req, err); err != nil {
|
||||
logger.Error(
|
||||
ctx, "could not send error response",
|
||||
logger.E(errors.WithStack(err)),
|
||||
logger.F("originalError", err),
|
||||
logger.F("request", req),
|
||||
)
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
promise, ok := m.backend.IsPromise(result)
|
||||
if ok {
|
||||
go func(ctx context.Context, req *RPCRequest, promise *goja.Promise) {
|
||||
result := m.backend.WaitForPromise(promise)
|
||||
sendRes(ctx, req, result)
|
||||
}(ctx, req, promise)
|
||||
} else {
|
||||
sendRes(ctx, req, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *RPCModule) sendErrorResponse(ctx context.Context, req *RPCRequest, err error) error {
|
||||
return m.sendResponse(ctx, &RPCResponse{
|
||||
ID: req.ID,
|
||||
Result: nil,
|
||||
Error: &RPCError{
|
||||
Code: -32603,
|
||||
Message: err.Error(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (m *RPCModule) sendMethodNotFoundResponse(ctx context.Context, req *RPCRequest) error {
|
||||
return m.sendResponse(ctx, &RPCResponse{
|
||||
ID: req.ID,
|
||||
Result: nil,
|
||||
Error: &RPCError{
|
||||
Code: -32601,
|
||||
Message: fmt.Sprintf("method not found"),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (m *RPCModule) sendResponse(ctx context.Context, res *RPCResponse) error {
|
||||
msg := NewBackendMessage(map[string]interface{}{
|
||||
"jsonrpc": "2.0",
|
||||
"id": res.ID,
|
||||
"error": res.Error,
|
||||
"result": res.Result,
|
||||
})
|
||||
|
||||
if err := m.bus.Publish(ctx, msg); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *RPCModule) isRPCRequest(msg *FrontendMessage) (bool, *RPCRequest) {
|
||||
jsonRPC, exists := msg.Data["jsonrpc"]
|
||||
if !exists || jsonRPC != "2.0" {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
rawMethod, exists := msg.Data["method"]
|
||||
if !exists {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
method, ok := rawMethod.(string)
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
id := msg.Data["id"]
|
||||
params := msg.Data["params"]
|
||||
|
||||
return true, &RPCRequest{
|
||||
ID: id,
|
||||
Method: method,
|
||||
Params: params,
|
||||
}
|
||||
}
|
||||
|
||||
func RPCModuleFactory(bus bus.Bus) app.BackendModuleFactory {
|
||||
return func(backend *app.Backend) app.BackendModule {
|
||||
mod := &RPCModule{
|
||||
backend: backend,
|
||||
bus: bus,
|
||||
}
|
||||
|
||||
go mod.handleMessages()
|
||||
|
||||
return mod
|
||||
}
|
||||
}
|
200
pkg/module/store.go
Normal file
200
pkg/module/store.go
Normal file
@ -0,0 +1,200 @@
|
||||
package module
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/filter"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/dop251/goja"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type StoreModule struct {
|
||||
backend *app.Backend
|
||||
store storage.DocumentStore
|
||||
}
|
||||
|
||||
func (m *StoreModule) Name() string {
|
||||
return "store"
|
||||
}
|
||||
|
||||
func (m *StoreModule) Export(export *goja.Object) {
|
||||
if err := export.Set("upsert", m.upsert); err != nil {
|
||||
panic(errors.Wrap(err, "could not set 'upsert' function"))
|
||||
}
|
||||
|
||||
if err := export.Set("get", m.get); err != nil {
|
||||
panic(errors.Wrap(err, "could not set 'get' function"))
|
||||
}
|
||||
|
||||
if err := export.Set("query", m.query); err != nil {
|
||||
panic(errors.Wrap(err, "could not set 'query' function"))
|
||||
}
|
||||
|
||||
if err := export.Set("delete", m.delete); err != nil {
|
||||
panic(errors.Wrap(err, "could not set 'delete' function"))
|
||||
}
|
||||
}
|
||||
|
||||
func (m *StoreModule) upsert(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||
ctx := assertContext(call.Argument(0), rt)
|
||||
collection := m.assertCollection(call.Argument(1), rt)
|
||||
document := m.assertDocument(call.Argument(2), rt)
|
||||
|
||||
document, err := m.store.Upsert(ctx, collection, document)
|
||||
if err != nil {
|
||||
panic(errors.Wrapf(err, "error while upserting document in collection '%s'", collection))
|
||||
}
|
||||
|
||||
spew.Dump(document)
|
||||
|
||||
return rt.ToValue(map[string]interface{}(document))
|
||||
}
|
||||
|
||||
func (m *StoreModule) get(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||
ctx := assertContext(call.Argument(0), rt)
|
||||
collection := m.assertCollection(call.Argument(1), rt)
|
||||
documentID := m.assertDocumentID(call.Argument(2), rt)
|
||||
|
||||
document, err := m.store.Get(ctx, collection, documentID)
|
||||
if err != nil {
|
||||
if errors.Is(err, storage.ErrDocumentNotFound) {
|
||||
return nil
|
||||
}
|
||||
|
||||
panic(errors.Wrapf(err, "error while getting document '%s' in collection '%s'", documentID, collection))
|
||||
}
|
||||
|
||||
return rt.ToValue(map[string]interface{}(document))
|
||||
}
|
||||
|
||||
type queryOptions struct {
|
||||
Limit *int `mapstructure:"limit"`
|
||||
Offset *int `mapstructure:"offset"`
|
||||
OrderBy *string `mapstructure:"orderBy"`
|
||||
OrderDirection *string `mapstructure:"orderDirection"`
|
||||
}
|
||||
|
||||
func (m *StoreModule) query(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||
ctx := assertContext(call.Argument(0), rt)
|
||||
collection := m.assertCollection(call.Argument(1), rt)
|
||||
filter := m.assertFilter(call.Argument(2), rt)
|
||||
queryOptions := m.assertQueryOptions(call.Argument(3), rt)
|
||||
|
||||
queryOptionsFuncs := make([]storage.QueryOptionFunc, 0)
|
||||
|
||||
if queryOptions.Limit != nil {
|
||||
queryOptionsFuncs = append(queryOptionsFuncs, storage.WithLimit(*queryOptions.Limit))
|
||||
}
|
||||
|
||||
if queryOptions.OrderBy != nil {
|
||||
queryOptionsFuncs = append(queryOptionsFuncs, storage.WithOrderBy(*queryOptions.OrderBy))
|
||||
}
|
||||
|
||||
if queryOptions.Offset != nil {
|
||||
queryOptionsFuncs = append(queryOptionsFuncs, storage.WithOffset(*queryOptions.Limit))
|
||||
}
|
||||
|
||||
if queryOptions.OrderDirection != nil {
|
||||
queryOptionsFuncs = append(queryOptionsFuncs, storage.WithOrderDirection(
|
||||
storage.OrderDirection(*queryOptions.OrderDirection),
|
||||
))
|
||||
}
|
||||
|
||||
documents, err := m.store.Query(ctx, collection, filter, queryOptionsFuncs...)
|
||||
if err != nil {
|
||||
panic(errors.Wrapf(err, "error while querying documents in collection '%s'", collection))
|
||||
}
|
||||
|
||||
rawDocuments := make([]map[string]interface{}, len(documents))
|
||||
for idx, doc := range documents {
|
||||
rawDocuments[idx] = map[string]interface{}(doc)
|
||||
}
|
||||
|
||||
return rt.ToValue(rawDocuments)
|
||||
}
|
||||
|
||||
func (m *StoreModule) delete(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||
ctx := assertContext(call.Argument(0), rt)
|
||||
collection := m.assertCollection(call.Argument(1), rt)
|
||||
documentID := m.assertDocumentID(call.Argument(2), rt)
|
||||
|
||||
if err := m.store.Delete(ctx, collection, documentID); err != nil {
|
||||
panic(errors.Wrapf(err, "error while deleting document '%s' in collection '%s'", documentID, collection))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *StoreModule) assertCollection(value goja.Value, rt *goja.Runtime) string {
|
||||
collection, ok := value.Export().(string)
|
||||
if !ok {
|
||||
panic(rt.NewTypeError(fmt.Sprintf("collection must be a string, got '%T'", value.Export())))
|
||||
}
|
||||
|
||||
return collection
|
||||
}
|
||||
|
||||
func (m *StoreModule) assertFilter(value goja.Value, rt *goja.Runtime) *filter.Filter {
|
||||
rawFilter, ok := value.Export().(map[string]interface{})
|
||||
if !ok {
|
||||
panic(rt.NewTypeError(fmt.Sprintf("filter must be an object, got '%T'", value.Export())))
|
||||
}
|
||||
|
||||
filter, err := filter.NewFrom(rawFilter)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "could not convert object to filter"))
|
||||
}
|
||||
|
||||
return filter
|
||||
}
|
||||
|
||||
func (m *StoreModule) assertDocumentID(value goja.Value, rt *goja.Runtime) storage.DocumentID {
|
||||
documentID, ok := value.Export().(storage.DocumentID)
|
||||
if !ok {
|
||||
rawDocumentID, ok := value.Export().(string)
|
||||
if !ok {
|
||||
panic(rt.NewTypeError(fmt.Sprintf("document id must be a documentid or a string, got '%T'", value.Export())))
|
||||
}
|
||||
|
||||
documentID = storage.DocumentID(rawDocumentID)
|
||||
}
|
||||
|
||||
return documentID
|
||||
}
|
||||
|
||||
func (m *StoreModule) assertQueryOptions(value goja.Value, rt *goja.Runtime) *queryOptions {
|
||||
rawQueryOptions, ok := value.Export().(map[string]interface{})
|
||||
if !ok {
|
||||
panic(rt.NewTypeError(fmt.Sprintf("query options must be an object, got '%T'", value.Export())))
|
||||
}
|
||||
|
||||
queryOptions := &queryOptions{}
|
||||
|
||||
if err := mapstructure.Decode(rawQueryOptions, queryOptions); err != nil {
|
||||
panic(errors.Wrap(err, "could not convert object to query options"))
|
||||
}
|
||||
|
||||
return queryOptions
|
||||
}
|
||||
|
||||
func (m *StoreModule) assertDocument(value goja.Value, rt *goja.Runtime) storage.Document {
|
||||
document, ok := value.Export().(map[string]interface{})
|
||||
if !ok {
|
||||
panic(rt.NewTypeError("document must be an object"))
|
||||
}
|
||||
|
||||
return document
|
||||
}
|
||||
|
||||
func StoreModuleFactory(store storage.DocumentStore) app.BackendModuleFactory {
|
||||
return func(backend *app.Backend) app.BackendModule {
|
||||
return &StoreModule{
|
||||
backend: backend,
|
||||
store: store,
|
||||
}
|
||||
}
|
||||
}
|
41
pkg/module/store_test.go
Normal file
41
pkg/module/store_test.go
Normal file
@ -0,0 +1,41 @@
|
||||
package module
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/sqlite"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
func TestStoreModule(t *testing.T) {
|
||||
logger.SetLevel(logger.LevelDebug)
|
||||
|
||||
store := sqlite.NewDocumentStore(":memory:")
|
||||
backend := app.NewBackend(
|
||||
ContextModuleFactory(),
|
||||
ConsoleModuleFactory(),
|
||||
StoreModuleFactory(store),
|
||||
)
|
||||
|
||||
data, err := ioutil.ReadFile("testdata/store.js")
|
||||
if err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
if err := backend.Load("testdata/store.js", string(data)); err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
if err := backend.Start(); err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
if _, err := backend.ExecFuncByName("testStore"); err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
backend.Stop()
|
||||
}
|
32
pkg/module/testdata/store.js
vendored
Normal file
32
pkg/module/testdata/store.js
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
var ctx = context.get();
|
||||
|
||||
function testStore() {
|
||||
var obj = store.upsert(ctx, "test", {"foo": "bar"});
|
||||
var obj1 = store.get(ctx, "test", obj._id);
|
||||
|
||||
console.log(obj, obj1);
|
||||
|
||||
for (var key in obj) {
|
||||
if (!obj.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (obj[key].toString() !== obj1[key].toString()) {
|
||||
throw new Error("obj['"+key+"'] !== obj1['"+key+"']");
|
||||
}
|
||||
}
|
||||
|
||||
var results = store.query(ctx, "test", { "eq": {"foo": "bar"} }, {"orderBy": "foo", "limit": 10, "skip": 0});
|
||||
|
||||
if (!results || results.length !== 1) {
|
||||
throw new Error("results should contains 1 item");
|
||||
}
|
||||
|
||||
store.delete(ctx, "test", obj._id);
|
||||
|
||||
var obj2 = store.get(ctx, "test", obj._id);
|
||||
|
||||
if (obj2 != null) {
|
||||
throw new Error("obj2 should be null");
|
||||
}
|
||||
}
|
59
pkg/module/user.go
Normal file
59
pkg/module/user.go
Normal file
@ -0,0 +1,59 @@
|
||||
package module
|
||||
|
||||
// import (
|
||||
// "context"
|
||||
|
||||
// "github.com/dop251/goja"
|
||||
// "github.com/pkg/errors"
|
||||
// "forge.cadoles.com/arcad/edge/pkg/app"
|
||||
// "forge.cadoles.com/arcad/edge/pkg/repository"
|
||||
// "gitlab.com/wpetit/goweb/logger"
|
||||
// )
|
||||
|
||||
// type UserModule struct {
|
||||
// appID app.ID
|
||||
// repo repository.UserRepository
|
||||
// backend *app.Backend
|
||||
// ctx context.Context
|
||||
// }
|
||||
|
||||
// func (m *UserModule) Name() string {
|
||||
// return "user"
|
||||
// }
|
||||
|
||||
// func (m *UserModule) Export(export *goja.Object) {
|
||||
// if err := export.Set("getUserById", m.getUserByID); err != nil {
|
||||
// panic(errors.Wrap(err, "could not set 'getUserById' function"))
|
||||
// }
|
||||
// }
|
||||
|
||||
// func (m *UserModule) getUserByID(call goja.FunctionCall) goja.Value {
|
||||
// if len(call.Arguments) != 1 {
|
||||
// panic(m.backend.ToValue("invalid number of arguments"))
|
||||
// }
|
||||
|
||||
// userID := repository.UserID(call.Arguments[0].String())
|
||||
|
||||
// user, err := m.repo.Get(userID)
|
||||
// if err != nil {
|
||||
// err = errors.Wrapf(err, "could not find user '%s'", userID)
|
||||
// logger.Error(m.ctx, "could not find user", logger.E(err), logger.F("userID", userID))
|
||||
// panic(m.backend.ToValue(err))
|
||||
// }
|
||||
|
||||
// return m.backend.ToValue(user)
|
||||
// }
|
||||
|
||||
// func UserModuleFactory(repo repository.UserRepository) app.BackendModuleFactory {
|
||||
// return func(appID app.ID, backend *app.Backend) app.BackendModule {
|
||||
// return &UserModule{
|
||||
// appID: appID,
|
||||
// repo: repo,
|
||||
// backend: backend,
|
||||
// ctx: logger.With(
|
||||
// context.Background(),
|
||||
// logger.F("appID", appID),
|
||||
// ),
|
||||
// }
|
||||
// }
|
||||
// }
|
70
pkg/module/user_test.go
Normal file
70
pkg/module/user_test.go
Normal file
@ -0,0 +1,70 @@
|
||||
package module
|
||||
|
||||
// import (
|
||||
// "errors"
|
||||
// "io/ioutil"
|
||||
// "testing"
|
||||
|
||||
// "gitlab.com/arcadbox/arcad/internal/app"
|
||||
// "gitlab.com/arcadbox/arcad/internal/repository"
|
||||
// )
|
||||
|
||||
// func TestUserModuleGetUserByID(t *testing.T) {
|
||||
// repo := &fakeUserRepository{}
|
||||
|
||||
// appID := app.ID("test")
|
||||
// backend := app.NewBackend(appID,
|
||||
// ConsoleModuleFactory(),
|
||||
// UserModuleFactory(repo),
|
||||
// )
|
||||
|
||||
// data, err := ioutil.ReadFile("testdata/user_getbyid.js")
|
||||
// if err != nil {
|
||||
// t.Fatal(err)
|
||||
// }
|
||||
|
||||
// if err := backend.Load(string(data)); err != nil {
|
||||
// t.Fatal(err)
|
||||
// }
|
||||
|
||||
// backend.Start()
|
||||
// defer backend.Stop()
|
||||
|
||||
// if err := backend.OnInit(); err != nil {
|
||||
// t.Error(err)
|
||||
// }
|
||||
// }
|
||||
|
||||
// type fakeUserRepository struct{}
|
||||
|
||||
// func (r *fakeUserRepository) Create() (*repository.User, error) {
|
||||
// return nil, errors.New("not implemented")
|
||||
// }
|
||||
|
||||
// func (r *fakeUserRepository) Save(user *repository.User) error {
|
||||
// return errors.New("not implemented")
|
||||
// }
|
||||
|
||||
// func (r *fakeUserRepository) Get(userID repository.UserID) (*repository.User, error) {
|
||||
// if userID == "0" {
|
||||
// return &repository.User{}, nil
|
||||
// }
|
||||
|
||||
// return nil, errors.New("not implemented")
|
||||
// }
|
||||
|
||||
// func (r *fakeUserRepository) Delete(userID repository.UserID) error {
|
||||
// return errors.New("not implemented")
|
||||
// }
|
||||
|
||||
// func (r *fakeUserRepository) Touch(userID repository.UserID, rawUserAgent string) error {
|
||||
// return errors.New("not implemented")
|
||||
// }
|
||||
|
||||
// func (r *fakeUserRepository) List() ([]*repository.User, error) {
|
||||
// return nil, errors.New("not implemented")
|
||||
// }
|
||||
|
||||
// func (r *fakeUserRepository) ListByID(userIDs ...repository.UserID) ([]*repository.User, error) {
|
||||
// return nil, errors.New("not implemented")
|
||||
// }
|
Reference in New Issue
Block a user