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
|
28
pkg/module/assert.go
Normal file
28
pkg/module/assert.go
Normal file
@ -0,0 +1,28 @@
|
||||
package module
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
func assertType[T any](v goja.Value, rt *goja.Runtime) T {
|
||||
if c, ok := v.Export().(T); ok {
|
||||
return c
|
||||
}
|
||||
|
||||
panic(rt.NewTypeError(fmt.Sprintf("expected value to be a '%T', got '%T'", new(T), v.Export())))
|
||||
}
|
||||
|
||||
func assertContext(v goja.Value, r *goja.Runtime) context.Context {
|
||||
return assertType[context.Context](v, r)
|
||||
}
|
||||
|
||||
func assertObject(v goja.Value, r *goja.Runtime) map[string]any {
|
||||
return assertType[map[string]any](v, r)
|
||||
}
|
||||
|
||||
func assertString(v goja.Value, r *goja.Runtime) string {
|
||||
return assertType[string](v, r)
|
||||
}
|
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.Server
|
||||
// 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.ServerModuleFactory {
|
||||
// return func(appID app.ID, backend *app.Server) app.ServerModule {
|
||||
// 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.NewServer(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)
|
||||
// }
|
||||
// }
|
282
pkg/module/blob.go
Normal file
282
pkg/module/blob.go
Normal file
@ -0,0 +1,282 @@
|
||||
package module
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||
"github.com/dop251/goja"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultBlobBucket string = "default"
|
||||
)
|
||||
|
||||
type BlobModule struct {
|
||||
server *app.Server
|
||||
bus bus.Bus
|
||||
store storage.BlobStore
|
||||
}
|
||||
|
||||
func (m *BlobModule) Name() string {
|
||||
return "blob"
|
||||
}
|
||||
|
||||
func (m *BlobModule) Export(export *goja.Object) {
|
||||
}
|
||||
|
||||
func (m *BlobModule) handleMessages() {
|
||||
ctx := context.Background()
|
||||
|
||||
go func() {
|
||||
err := m.bus.Reply(ctx, MessageNamespaceUploadRequest, func(msg bus.Message) (bus.Message, error) {
|
||||
uploadRequest, ok := msg.(*MessageUploadRequest)
|
||||
if !ok {
|
||||
return nil, errors.Wrapf(bus.ErrUnexpectedMessage, "expected message upload request, got '%T'", msg)
|
||||
}
|
||||
|
||||
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, MessageNamespaceDownloadRequest, func(msg bus.Message) (bus.Message, error) {
|
||||
downloadRequest, ok := msg.(*MessageDownloadRequest)
|
||||
if !ok {
|
||||
return nil, errors.Wrapf(bus.ErrUnexpectedMessage, "expected message download request, got '%T'", msg)
|
||||
}
|
||||
|
||||
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 *BlobModule) handleUploadRequest(req *MessageUploadRequest) (*MessageUploadResponse, error) {
|
||||
blobID := storage.NewBlobID()
|
||||
res := NewMessageUploadResponse(req.RequestID)
|
||||
|
||||
ctx := logger.With(req.Context, logger.F("blobID", blobID))
|
||||
|
||||
blobInfo := map[string]interface{}{
|
||||
"size": req.FileHeader.Size,
|
||||
"filename": req.FileHeader.Filename,
|
||||
"contentType": req.FileHeader.Header.Get("Content-Type"),
|
||||
}
|
||||
|
||||
rawResult, err := m.server.ExecFuncByName("onBlobUpload", ctx, blobID, blobInfo, req.Metadata)
|
||||
if err != nil {
|
||||
if errors.Is(err, app.ErrFuncDoesNotExist) {
|
||||
res.Allow = false
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
result, ok := rawResult.Export().(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, errors.Errorf(
|
||||
"unexpected onBlobUpload result: expected 'map[string]interface{}', got '%T'",
|
||||
rawResult.Export(),
|
||||
)
|
||||
}
|
||||
|
||||
var allow bool
|
||||
|
||||
rawAllow, exists := result["allow"]
|
||||
if !exists {
|
||||
allow = false
|
||||
} else {
|
||||
allow, ok = rawAllow.(bool)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("invalid 'allow' result property: got type '%T', expected type '%T'", rawAllow, false)
|
||||
}
|
||||
}
|
||||
|
||||
res.Allow = allow
|
||||
|
||||
if res.Allow {
|
||||
bucket := DefaultBlobBucket
|
||||
|
||||
rawBucket, exists := result["bucket"]
|
||||
if exists {
|
||||
bucket, ok = rawBucket.(string)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("invalid 'bucket' result property: got type '%T', expected type '%T'", bucket, "")
|
||||
}
|
||||
}
|
||||
|
||||
if err := m.saveBlob(ctx, bucket, blobID, *req.FileHeader); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
res.Bucket = bucket
|
||||
res.BlobID = blobID
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (m *BlobModule) saveBlob(ctx context.Context, bucketName string, blobID storage.BlobID, fileHeader multipart.FileHeader) error {
|
||||
file, err := fileHeader.Open()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := file.Close(); err != nil {
|
||||
logger.Error(ctx, "could not close file", logger.E(errors.WithStack(err)))
|
||||
}
|
||||
}()
|
||||
|
||||
bucket, err := m.store.OpenBucket(ctx, bucketName)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := bucket.Close(); err != nil {
|
||||
logger.Error(ctx, "could not close bucket", logger.E(errors.WithStack(err)))
|
||||
}
|
||||
}()
|
||||
|
||||
writer, err := bucket.NewWriter(ctx, blobID)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := file.Close(); err != nil {
|
||||
logger.Error(ctx, "could not close file", logger.E(errors.WithStack(err)))
|
||||
}
|
||||
}()
|
||||
|
||||
defer func() {
|
||||
if err := writer.Close(); err != nil {
|
||||
logger.Error(ctx, "could not close writer", logger.E(errors.WithStack(err)))
|
||||
}
|
||||
}()
|
||||
|
||||
if _, err := io.Copy(writer, file); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *BlobModule) handleDownloadRequest(req *MessageDownloadRequest) (*MessageDownloadResponse, error) {
|
||||
res := NewMessageDownloadResponse(req.RequestID)
|
||||
|
||||
rawResult, err := m.server.ExecFuncByName("onBlobDownload", req.Context, req.Bucket, req.BlobID)
|
||||
if err != nil {
|
||||
if errors.Is(err, app.ErrFuncDoesNotExist) {
|
||||
res.Allow = false
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
result, ok := rawResult.Export().(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, errors.Errorf(
|
||||
"unexpected onBlobDownload result: expected 'map[string]interface{}', got '%T'",
|
||||
rawResult.Export(),
|
||||
)
|
||||
}
|
||||
|
||||
var allow bool
|
||||
|
||||
rawAllow, exists := result["allow"]
|
||||
if !exists {
|
||||
allow = false
|
||||
} else {
|
||||
allow, ok = rawAllow.(bool)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("invalid 'allow' result property: got type '%T', expected type '%T'", rawAllow, false)
|
||||
}
|
||||
}
|
||||
|
||||
res.Allow = allow
|
||||
|
||||
reader, info, err := m.openBlob(req.Context, req.Bucket, req.BlobID)
|
||||
if err != nil && !errors.Is(err, storage.ErrBlobNotFound) {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if reader != nil {
|
||||
res.Blob = reader
|
||||
}
|
||||
|
||||
if info != nil {
|
||||
res.BlobInfo = info
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (m *BlobModule) openBlob(ctx context.Context, bucketName string, blobID storage.BlobID) (io.ReadSeekCloser, storage.BlobInfo, error) {
|
||||
bucket, err := m.store.OpenBucket(ctx, bucketName)
|
||||
if err != nil {
|
||||
return nil, nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := bucket.Close(); err != nil {
|
||||
logger.Error(ctx, "could not close bucket", logger.E(errors.WithStack(err)), logger.F("bucket", bucket))
|
||||
}
|
||||
}()
|
||||
|
||||
info, err := bucket.Get(ctx, blobID)
|
||||
if err != nil {
|
||||
return nil, nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
reader, err := bucket.NewReader(ctx, blobID)
|
||||
if err != nil {
|
||||
return nil, nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return reader, info, nil
|
||||
}
|
||||
|
||||
func BlobModuleFactory(bus bus.Bus, store storage.BlobStore) app.ServerModuleFactory {
|
||||
return func(server *app.Server) app.ServerModule {
|
||||
mod := &BlobModule{
|
||||
store: store,
|
||||
bus: bus,
|
||||
server: server,
|
||||
}
|
||||
|
||||
go mod.handleMessages()
|
||||
|
||||
return mod
|
||||
}
|
||||
}
|
92
pkg/module/blob_message.go
Normal file
92
pkg/module/blob_message.go
Normal file
@ -0,0 +1,92 @@
|
||||
package module
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||
"github.com/oklog/ulid/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
MessageNamespaceUploadRequest bus.MessageNamespace = "uploadRequest"
|
||||
MessageNamespaceUploadResponse bus.MessageNamespace = "uploadResponse"
|
||||
MessageNamespaceDownloadRequest bus.MessageNamespace = "downloadRequest"
|
||||
MessageNamespaceDownloadResponse bus.MessageNamespace = "downloadResponse"
|
||||
)
|
||||
|
||||
type MessageUploadRequest struct {
|
||||
Context context.Context
|
||||
RequestID string
|
||||
FileHeader *multipart.FileHeader
|
||||
Metadata map[string]interface{}
|
||||
}
|
||||
|
||||
func (m *MessageUploadRequest) MessageNamespace() bus.MessageNamespace {
|
||||
return MessageNamespaceUploadRequest
|
||||
}
|
||||
|
||||
func NewMessageUploadRequest(ctx context.Context, fileHeader *multipart.FileHeader, metadata map[string]interface{}) *MessageUploadRequest {
|
||||
return &MessageUploadRequest{
|
||||
Context: ctx,
|
||||
RequestID: ulid.Make().String(),
|
||||
FileHeader: fileHeader,
|
||||
Metadata: metadata,
|
||||
}
|
||||
}
|
||||
|
||||
type MessageUploadResponse struct {
|
||||
RequestID string
|
||||
BlobID storage.BlobID
|
||||
Bucket string
|
||||
Allow bool
|
||||
}
|
||||
|
||||
func (m *MessageUploadResponse) MessageNamespace() bus.MessageNamespace {
|
||||
return MessageNamespaceDownloadResponse
|
||||
}
|
||||
|
||||
func NewMessageUploadResponse(requestID string) *MessageUploadResponse {
|
||||
return &MessageUploadResponse{
|
||||
RequestID: requestID,
|
||||
}
|
||||
}
|
||||
|
||||
type MessageDownloadRequest struct {
|
||||
Context context.Context
|
||||
RequestID string
|
||||
Bucket string
|
||||
BlobID storage.BlobID
|
||||
}
|
||||
|
||||
func (m *MessageDownloadRequest) MessageNamespace() bus.MessageNamespace {
|
||||
return MessageNamespaceDownloadRequest
|
||||
}
|
||||
|
||||
func NewMessageDownloadRequest(ctx context.Context, bucket string, blobID storage.BlobID) *MessageDownloadRequest {
|
||||
return &MessageDownloadRequest{
|
||||
Context: ctx,
|
||||
RequestID: ulid.Make().String(),
|
||||
Bucket: bucket,
|
||||
BlobID: blobID,
|
||||
}
|
||||
}
|
||||
|
||||
type MessageDownloadResponse struct {
|
||||
RequestID string
|
||||
Allow bool
|
||||
BlobInfo storage.BlobInfo
|
||||
Blob io.ReadSeekCloser
|
||||
}
|
||||
|
||||
func (m *MessageDownloadResponse) MessageNamespace() bus.MessageNamespace {
|
||||
return MessageNamespaceDownloadResponse
|
||||
}
|
||||
|
||||
func NewMessageDownloadResponse(requestID string) *MessageDownloadResponse {
|
||||
return &MessageDownloadResponse{
|
||||
RequestID: requestID,
|
||||
}
|
||||
}
|
51
pkg/module/console.go
Normal file
51
pkg/module/console.go
Normal file
@ -0,0 +1,51 @@
|
||||
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, rt *goja.Runtime) goja.Value {
|
||||
var sb strings.Builder
|
||||
|
||||
fields := make([]logger.Field, 0)
|
||||
|
||||
stack := rt.CaptureCallStack(0, nil)
|
||||
if len(stack) > 1 {
|
||||
fields = append(fields, logger.F("source", stack[1].Position().String()))
|
||||
}
|
||||
|
||||
for _, arg := range call.Arguments {
|
||||
sb.WriteString(fmt.Sprintf("%+v", arg.Export()))
|
||||
sb.WriteString(" ")
|
||||
}
|
||||
|
||||
logger.Debug(context.Background(), sb.String(), fields...)
|
||||
|
||||
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.ServerModuleFactory {
|
||||
return func(server *app.Server) app.ServerModule {
|
||||
return &ConsoleModule{}
|
||||
}
|
||||
}
|
94
pkg/module/context.go
Normal file
94
pkg/module/context.go
Normal file
@ -0,0 +1,94 @@
|
||||
package module
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"github.com/dop251/goja"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type ContextKey string
|
||||
|
||||
const (
|
||||
ContextKeySessionID ContextKey = "sessionId"
|
||||
ContextKeyOriginRequest ContextKey = "originRequest"
|
||||
)
|
||||
|
||||
type ContextModule struct{}
|
||||
|
||||
func (m *ContextModule) Name() string {
|
||||
return "context"
|
||||
}
|
||||
|
||||
func (m *ContextModule) new(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||
return rt.ToValue(context.Background())
|
||||
}
|
||||
|
||||
func (m *ContextModule) with(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||
ctx := assertContext(call.Argument(0), rt)
|
||||
rawValues := assertObject(call.Argument(1), rt)
|
||||
|
||||
values := make(map[ContextKey]any)
|
||||
for k, v := range rawValues {
|
||||
values[ContextKey(k)] = v
|
||||
}
|
||||
|
||||
ctx = WithContext(ctx, values)
|
||||
|
||||
return rt.ToValue(ctx)
|
||||
}
|
||||
|
||||
func (m *ContextModule) get(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||
ctx := assertContext(call.Argument(0), rt)
|
||||
rawKey := assertString(call.Argument(1), rt)
|
||||
|
||||
value := ctx.Value(ContextKey(rawKey))
|
||||
|
||||
return rt.ToValue(value)
|
||||
}
|
||||
|
||||
func (m *ContextModule) Export(export *goja.Object) {
|
||||
if err := export.Set("new", m.new); err != nil {
|
||||
panic(errors.Wrap(err, "could not set 'new' function"))
|
||||
}
|
||||
|
||||
if err := export.Set("get", m.get); err != nil {
|
||||
panic(errors.Wrap(err, "could not set 'get' function"))
|
||||
}
|
||||
|
||||
if err := export.Set("with", m.with); err != nil {
|
||||
panic(errors.Wrap(err, "could not set 'with' function"))
|
||||
}
|
||||
|
||||
if err := export.Set("ORIGIN_REQUEST", string(ContextKeyOriginRequest)); err != nil {
|
||||
panic(errors.Wrap(err, "could not set 'ORIGIN_REQUEST' property"))
|
||||
}
|
||||
|
||||
if err := export.Set("SESSION_ID", string(ContextKeySessionID)); err != nil {
|
||||
panic(errors.Wrap(err, "could not set 'SESSION_ID' property"))
|
||||
}
|
||||
}
|
||||
|
||||
func ContextModuleFactory() app.ServerModuleFactory {
|
||||
return func(server *app.Server) app.ServerModule {
|
||||
return &ContextModule{}
|
||||
}
|
||||
}
|
||||
|
||||
func ContextValue[T any](ctx context.Context, key ContextKey) T {
|
||||
value, ok := ctx.Value(key).(T)
|
||||
if !ok {
|
||||
return *new(T)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
func WithContext(ctx context.Context, values map[ContextKey]any) context.Context {
|
||||
for k, v := range values {
|
||||
ctx = context.WithValue(ctx, k, v)
|
||||
}
|
||||
|
||||
return ctx
|
||||
}
|
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")
|
121
pkg/module/lifecycle.go
Normal file
121
pkg/module/lifecycle.go
Normal file
@ -0,0 +1,121 @@
|
||||
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 {
|
||||
server *app.Server
|
||||
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.server.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 nil
|
||||
}
|
||||
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *LifecycleModule) handleMessages() {
|
||||
ctx := context.Background()
|
||||
|
||||
logger.Debug(
|
||||
ctx,
|
||||
"subscribing to bus messages",
|
||||
)
|
||||
|
||||
clientMessages, err := m.bus.Subscribe(ctx, MessageNamespaceClient)
|
||||
if err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
|
||||
defer func() {
|
||||
logger.Debug(
|
||||
ctx,
|
||||
"unsubscribing from bus messages",
|
||||
)
|
||||
|
||||
m.bus.Unsubscribe(ctx, MessageNamespaceClient, clientMessages)
|
||||
}()
|
||||
|
||||
for {
|
||||
logger.Debug(
|
||||
ctx,
|
||||
"waiting for next message",
|
||||
)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
logger.Debug(
|
||||
ctx,
|
||||
"context done",
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
case msg := <-clientMessages:
|
||||
clientMessage, ok := msg.(*ClientMessage)
|
||||
if !ok {
|
||||
logger.Error(
|
||||
ctx,
|
||||
"unexpected message type",
|
||||
logger.F("message", msg),
|
||||
)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Debug(
|
||||
ctx,
|
||||
"received client message",
|
||||
logger.F("message", clientMessage),
|
||||
)
|
||||
|
||||
if _, err := m.server.ExecFuncByName("onClientMessage", clientMessage.Context, clientMessage.Data); err != nil {
|
||||
if errors.Is(err, app.ErrFuncDoesNotExist) {
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Error(
|
||||
ctx,
|
||||
"on client message error",
|
||||
logger.E(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func LifecycleModuleFactory(bus bus.Bus) app.ServerModuleFactory {
|
||||
return func(server *app.Server) app.ServerModule {
|
||||
module := &LifecycleModule{
|
||||
server: server,
|
||||
bus: bus,
|
||||
}
|
||||
|
||||
go module.handleMessages()
|
||||
|
||||
return module
|
||||
}
|
||||
}
|
||||
|
||||
var _ app.InitializableModule = &LifecycleModule{}
|
38
pkg/module/message.go
Normal file
38
pkg/module/message.go
Normal file
@ -0,0 +1,38 @@
|
||||
package module
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||
)
|
||||
|
||||
const (
|
||||
MessageNamespaceClient bus.MessageNamespace = "client"
|
||||
MessageNamespaceServer bus.MessageNamespace = "server"
|
||||
)
|
||||
|
||||
type ServerMessage struct {
|
||||
Context context.Context
|
||||
Data interface{}
|
||||
}
|
||||
|
||||
func (m *ServerMessage) MessageNamespace() bus.MessageNamespace {
|
||||
return MessageNamespaceServer
|
||||
}
|
||||
|
||||
func NewServerMessage(ctx context.Context, data interface{}) *ServerMessage {
|
||||
return &ServerMessage{ctx, data}
|
||||
}
|
||||
|
||||
type ClientMessage struct {
|
||||
Context context.Context
|
||||
Data map[string]interface{}
|
||||
}
|
||||
|
||||
func (m *ClientMessage) MessageNamespace() bus.MessageNamespace {
|
||||
return MessageNamespaceClient
|
||||
}
|
||||
|
||||
func NewClientMessage(ctx context.Context, data map[string]interface{}) *ClientMessage {
|
||||
return &ClientMessage{ctx, data}
|
||||
}
|
81
pkg/module/net.go
Normal file
81
pkg/module/net.go
Normal file
@ -0,0 +1,81 @@
|
||||
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 {
|
||||
server *app.Server
|
||||
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.server.ToValue("invalid number of argument"))
|
||||
}
|
||||
|
||||
data := call.Argument(0).Export()
|
||||
|
||||
msg := NewServerMessage(nil, data)
|
||||
if err := m.bus.Publish(context.Background(), msg); err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *NetModule) send(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||
if len(call.Arguments) < 2 {
|
||||
panic(m.server.ToValue("invalid number of argument"))
|
||||
}
|
||||
|
||||
var ctx context.Context
|
||||
|
||||
firstArg := call.Argument(0)
|
||||
|
||||
sessionID, ok := firstArg.Export().(string)
|
||||
if ok {
|
||||
ctx = WithContext(context.Background(), map[ContextKey]any{
|
||||
ContextKeySessionID: sessionID,
|
||||
})
|
||||
} else {
|
||||
ctx = assertContext(firstArg, rt)
|
||||
}
|
||||
|
||||
data := call.Argument(1).Export()
|
||||
|
||||
msg := NewServerMessage(ctx, data)
|
||||
if err := m.bus.Publish(ctx, msg); err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NetModuleFactory(bus bus.Bus) app.ServerModuleFactory {
|
||||
return func(server *app.Server) app.ServerModule {
|
||||
return &NetModule{
|
||||
server: server,
|
||||
bus: bus,
|
||||
}
|
||||
}
|
||||
}
|
263
pkg/module/rpc.go
Normal file
263
pkg/module/rpc.go
Normal file
@ -0,0 +1,263 @@
|
||||
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 {
|
||||
server *app.Server
|
||||
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, rt *goja.Runtime) goja.Value {
|
||||
fnName := assertString(call.Argument(0), rt)
|
||||
|
||||
var (
|
||||
callable goja.Callable
|
||||
ok bool
|
||||
)
|
||||
|
||||
if len(call.Arguments) > 1 {
|
||||
callable, ok = goja.AssertFunction(call.Argument(1))
|
||||
} else {
|
||||
callable, ok = goja.AssertFunction(rt.Get(fnName))
|
||||
}
|
||||
|
||||
if !ok {
|
||||
panic(rt.NewTypeError("method should be a valid function"))
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
logger.Debug(ctx, "registering method", logger.F("method", fnName))
|
||||
|
||||
m.callbacks.Store(fnName, callable)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *RPCModule) unregister(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||
fnName := assertString(call.Argument(0), rt)
|
||||
|
||||
m.callbacks.Delete(fnName)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *RPCModule) handleMessages() {
|
||||
ctx := context.Background()
|
||||
|
||||
clientMessages, err := m.bus.Subscribe(ctx, MessageNamespaceClient)
|
||||
if err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
|
||||
defer func() {
|
||||
m.bus.Unsubscribe(ctx, MessageNamespaceClient, clientMessages)
|
||||
}()
|
||||
|
||||
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 clientMessages {
|
||||
clientMessage, ok := msg.(*ClientMessage)
|
||||
if !ok {
|
||||
logger.Warn(ctx, "unexpected bus message", logger.F("message", msg))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
ok, req := m.isRPCRequest(clientMessage)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Debug(ctx, "received rpc request", logger.F("request", req))
|
||||
|
||||
rawCallable, exists := m.callbacks.Load(req.Method)
|
||||
if !exists {
|
||||
logger.Debug(ctx, "method not found", logger.F("req", req))
|
||||
|
||||
if err := m.sendMethodNotFoundResponse(clientMessage.Context, req); err != nil {
|
||||
logger.Error(
|
||||
ctx, "could not send method not found response",
|
||||
logger.E(errors.WithStack(err)),
|
||||
logger.F("request", req),
|
||||
)
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
callable, ok := rawCallable.(goja.Callable)
|
||||
if !ok {
|
||||
logger.Debug(ctx, "invalid method", logger.F("req", req))
|
||||
|
||||
if err := m.sendMethodNotFoundResponse(clientMessage.Context, 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.server.Exec(callable, ctx, req.Params)
|
||||
if err != nil {
|
||||
if err := m.sendErrorResponse(clientMessage.Context, 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.server.IsPromise(result)
|
||||
if ok {
|
||||
go func(ctx context.Context, req *RPCRequest, promise *goja.Promise) {
|
||||
result := m.server.WaitForPromise(promise)
|
||||
sendRes(ctx, req, result)
|
||||
}(clientMessage.Context, req, promise)
|
||||
} else {
|
||||
sendRes(clientMessage.Context, 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 := NewServerMessage(ctx, 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 *ClientMessage) (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.ServerModuleFactory {
|
||||
return func(server *app.Server) app.ServerModule {
|
||||
mod := &RPCModule{
|
||||
server: server,
|
||||
bus: bus,
|
||||
}
|
||||
|
||||
go mod.handleMessages()
|
||||
|
||||
return mod
|
||||
}
|
||||
}
|
205
pkg/module/store.go
Normal file
205
pkg/module/store.go
Normal file
@ -0,0 +1,205 @@
|
||||
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/dop251/goja"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type StoreModule struct {
|
||||
server *app.Server
|
||||
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"))
|
||||
}
|
||||
|
||||
if err := export.Set("DIRECTION_ASC", storage.OrderDirectionAsc); err != nil {
|
||||
panic(errors.Wrap(err, "could not set 'DIRECTION_ASC' property"))
|
||||
}
|
||||
|
||||
if err := export.Set("DIRECTION_DESC", storage.OrderDirectionDesc); err != nil {
|
||||
panic(errors.Wrap(err, "could not set 'DIRECTION_DESC' property"))
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
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.ServerModuleFactory {
|
||||
return func(server *app.Server) app.ServerModule {
|
||||
return &StoreModule{
|
||||
server: server,
|
||||
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:")
|
||||
server := app.NewServer(
|
||||
ContextModuleFactory(),
|
||||
ConsoleModuleFactory(),
|
||||
StoreModuleFactory(store),
|
||||
)
|
||||
|
||||
data, err := ioutil.ReadFile("testdata/store.js")
|
||||
if err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
if err := server.Load("testdata/store.js", string(data)); err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
if err := server.Start(); err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
if _, err := server.ExecFuncByName("testStore"); err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
server.Stop()
|
||||
}
|
32
pkg/module/testdata/store.js
vendored
Normal file
32
pkg/module/testdata/store.js
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
function testStore() {
|
||||
var ctx = context.new()
|
||||
|
||||
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.Server
|
||||
// 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.ServerModuleFactory {
|
||||
// return func(appID app.ID, backend *app.Server) app.ServerModule {
|
||||
// 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.NewServer(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