fix(module,store): allow querying on _id, _createdAt, _updatedAt attributes
This commit is contained in:
parent
f01b1ef3b2
commit
b9f985ab0c
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,11 @@
|
||||||
package module
|
package store
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/module"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/storage/sqlite"
|
"forge.cadoles.com/arcad/edge/pkg/storage/sqlite"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gitlab.com/wpetit/goweb/logger"
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
@ -15,9 +16,9 @@ func TestStoreModule(t *testing.T) {
|
||||||
|
|
||||||
store := sqlite.NewDocumentStore(":memory:")
|
store := sqlite.NewDocumentStore(":memory:")
|
||||||
server := app.NewServer(
|
server := app.NewServer(
|
||||||
ContextModuleFactory(),
|
module.ContextModuleFactory(),
|
||||||
ConsoleModuleFactory(),
|
module.ConsoleModuleFactory(),
|
||||||
StoreModuleFactory(store),
|
ModuleFactory(store),
|
||||||
)
|
)
|
||||||
|
|
||||||
data, err := ioutil.ReadFile("testdata/store.js")
|
data, err := ioutil.ReadFile("testdata/store.js")
|
|
@ -3,10 +3,36 @@ package sqlite
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/storage/filter/sql"
|
"forge.cadoles.com/arcad/edge/pkg/storage/filter/sql"
|
||||||
)
|
)
|
||||||
|
|
||||||
func transformOperator(operator string, invert bool, key string, value any, option *sql.Option) (string, any, error) {
|
func transformOperator(operator string, invert bool, key string, value any, option *sql.Option) (string, any, error) {
|
||||||
|
isDataAttr := true
|
||||||
|
|
||||||
|
switch key {
|
||||||
|
case storage.DocumentAttrCreatedAt:
|
||||||
|
key = "created_at"
|
||||||
|
isDataAttr = false
|
||||||
|
case storage.DocumentAttrUpdatedAt:
|
||||||
|
key = "updated_at"
|
||||||
|
isDataAttr = false
|
||||||
|
case storage.DocumentAttrID:
|
||||||
|
key = "id"
|
||||||
|
isDataAttr = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isDataAttr {
|
||||||
|
option = &sql.Option{
|
||||||
|
PreparedParameter: option.PreparedParameter,
|
||||||
|
KeyTransform: func(key string) string {
|
||||||
|
return key
|
||||||
|
},
|
||||||
|
ValueTransform: option.ValueTransform,
|
||||||
|
Transform: option.Transform,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch operator {
|
switch operator {
|
||||||
case sql.OpIn:
|
case sql.OpIn:
|
||||||
return transformInOperator(key, value, option)
|
return transformInOperator(key, value, option)
|
||||||
|
|
|
@ -6,7 +6,6 @@ import (
|
||||||
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/storage/filter"
|
"forge.cadoles.com/arcad/edge/pkg/storage/filter"
|
||||||
"github.com/davecgh/go-spew/spew"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -58,6 +57,43 @@ var documentStoreOpsTestCases = []documentStoreOpsTestCase{
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "Query on _id",
|
||||||
|
Run: func(ctx context.Context, store storage.DocumentStore) error {
|
||||||
|
collection := "query_on_id"
|
||||||
|
|
||||||
|
doc := storage.Document{
|
||||||
|
"attr1": "Foo",
|
||||||
|
}
|
||||||
|
|
||||||
|
upsertedDoc, err := store.Upsert(ctx, collection, doc)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
docID, ok := upsertedDoc.ID()
|
||||||
|
if !ok {
|
||||||
|
return errors.Errorf("")
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := filter.New(
|
||||||
|
filter.NewEqOperator(map[string]interface{}{
|
||||||
|
"_id": docID,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
results, err := store.Query(ctx, collection, filter, nil)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, g := 1, len(results); e != g {
|
||||||
|
return errors.Errorf("len(results): expected '%v', got '%v'", e, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "Query with 'IN' operator",
|
Name: "Query with 'IN' operator",
|
||||||
Run: func(ctx context.Context, store storage.DocumentStore) error {
|
Run: func(ctx context.Context, store storage.DocumentStore) error {
|
||||||
|
@ -160,8 +196,6 @@ var documentStoreOpsTestCases = []documentStoreOpsTestCase{
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
spew.Dump(upsertedDoc, upsertedDoc2)
|
|
||||||
|
|
||||||
prevID, _ := upsertedDoc.ID()
|
prevID, _ := upsertedDoc.ID()
|
||||||
newID, _ := upsertedDoc2.ID()
|
newID, _ := upsertedDoc2.ID()
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue