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")) } } 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, } } }