fix(module,store): allow querying on _id, _createdAt, _updatedAt attributes

This commit is contained in:
2023-02-21 15:05:01 +01:00
parent f01b1ef3b2
commit b9f985ab0c
5 changed files with 284 additions and 7 deletions

216
pkg/module/store/module.go Normal file
View File

@ -0,0 +1,216 @@
package store
import (
"fmt"
"forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/module/util"
"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 Module struct {
server *app.Server
store storage.DocumentStore
}
func (m *Module) Name() string {
return "store"
}
func (m *Module) 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 *Module) upsert(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
ctx := util.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 *Module) get(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
ctx := util.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 *Module) query(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
ctx := util.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 != nil {
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 *Module) delete(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
ctx := util.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 *Module) 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 *Module) assertFilter(value goja.Value, rt *goja.Runtime) *filter.Filter {
if value.Export() == nil {
return nil
}
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 *Module) 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 *Module) assertQueryOptions(value goja.Value, rt *goja.Runtime) *queryOptions {
if value.Export() == nil {
return nil
}
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 *Module) 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 ModuleFactory(store storage.DocumentStore) app.ServerModuleFactory {
return func(server *app.Server) app.ServerModule {
return &Module{
server: server,
store: store,
}
}
}

View File

@ -0,0 +1,42 @@
package store
import (
"io/ioutil"
"testing"
"forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/module"
"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(
module.ContextModuleFactory(),
module.ConsoleModuleFactory(),
ModuleFactory(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/store/testdata/store.js vendored Normal file
View 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");
}
}