feat: initial commit

This commit is contained in:
2023-02-09 12:16:36 +01:00
commit 65a866efe1
113 changed files with 17880 additions and 0 deletions

1
pkg/module/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*.sqlite

109
pkg/module/authorization.go Normal file
View 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
// }
// }

View 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
View 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
View 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
View 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
View 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
View 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
View 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{}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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")
// }