From dc99bc2cb8653be8d928d09faf38ac4d0c65c10f Mon Sep 17 00:00:00 2001 From: Denis Arh Date: Thu, 12 Sep 2019 18:34:09 +0200 Subject: [PATCH] Add record procedure exec capabilities This will allow us to perform bulk operations on record & record-values --- api/compose/spec.json | 24 +++++ api/compose/spec/record.json | 24 +++++ compose/repository/record.go | 11 +- compose/rest/handlers/record.go | 23 +++++ compose/rest/record.go | 22 ++++ compose/rest/request/record.go | 59 +++++++++++ compose/rest/request/util.go | 25 +++++ compose/service/record.go | 158 +++++++++++++++++++++++++++-- compose/types/record_value.go | 10 +- docs/compose/README.md | 18 ++++ tests/compose/main_test.go | 5 + tests/compose/module_test.go | 4 +- tests/compose/record_exec_test.go | 161 ++++++++++++++++++++++++++++++ tests/compose/record_test.go | 74 +++++++------- 14 files changed, 565 insertions(+), 53 deletions(-) create mode 100644 tests/compose/record_exec_test.go diff --git a/api/compose/spec.json b/api/compose/spec.json index 18b3166bb..080427024 100644 --- a/api/compose/spec.json +++ b/api/compose/spec.json @@ -765,6 +765,30 @@ ] } }, + { + "name": "exec", + "path": "/exec/{procedure}", + "method": "POST", + "title": "Executes server-side procedure over one or more module records", + "parameters": { + "path": [ + { + "type": "string", + "name": "procedure", + "required": true, + "title": "Name of procedure to execute" + } + ], + "post": [ + { + "type": "[]ProcedureArg", + "name": "args", + "required": false, + "title": "Procedure arguments" + } + ] + } + }, { "name": "create", "method": "POST", diff --git a/api/compose/spec/record.json b/api/compose/spec/record.json index 9b26fd875..71c7b6dda 100644 --- a/api/compose/spec/record.json +++ b/api/compose/spec/record.json @@ -189,6 +189,30 @@ ] } }, + { + "Name": "exec", + "Method": "POST", + "Title": "Executes server-side procedure over one or more module records", + "Path": "/exec/{procedure}", + "Parameters": { + "path": [ + { + "name": "procedure", + "required": true, + "title": "Name of procedure to execute", + "type": "string" + } + ], + "post": [ + { + "name": "args", + "required": false, + "title": "Procedure arguments", + "type": "[]ProcedureArg" + } + ] + } + }, { "Name": "create", "Method": "POST", diff --git a/compose/repository/record.go b/compose/repository/record.go index dfb0bef82..b817443d6 100644 --- a/compose/repository/record.go +++ b/compose/repository/record.go @@ -32,6 +32,7 @@ type ( LoadValues(fieldNames []string, IDs []uint64) (rvs types.RecordValueSet, err error) DeleteValues(record *types.Record) error UpdateValues(recordID uint64, rvs types.RecordValueSet) (err error) + PartialUpdateValues(rvs ...*types.RecordValue) (err error) } record struct { @@ -118,7 +119,6 @@ func (r record) Report(module *types.Module, metrics, dimensions, filter string) func (r record) Find(module *types.Module, filter types.RecordFilter) (set types.RecordSet, f types.RecordFilter, err error) { var query squirrel.SelectBuilder f = filter - f.PageFilter.NormalizePerPageWithDefaults() query, err = r.buildQuery(module, filter) if err != nil { @@ -314,6 +314,15 @@ func (r record) UpdateValues(recordID uint64, rvs types.RecordValueSet) (err err err = rvs.Walk(func(value *types.RecordValue) error { value.RecordID = recordID + return r.db().Insert("compose_record_value", value) + }) + + return errors.Wrap(err, "could not insert record values") + +} + +func (r record) PartialUpdateValues(rvs ...*types.RecordValue) (err error) { + err = types.RecordValueSet(rvs).Walk(func(value *types.RecordValue) error { return r.db().Replace("compose_record_value", value) }) diff --git a/compose/rest/handlers/record.go b/compose/rest/handlers/record.go index 495a337f0..4c87bdf12 100644 --- a/compose/rest/handlers/record.go +++ b/compose/rest/handlers/record.go @@ -35,6 +35,7 @@ type RecordAPI interface { ImportRun(context.Context, *request.RecordImportRun) (interface{}, error) ImportProgress(context.Context, *request.RecordImportProgress) (interface{}, error) Export(context.Context, *request.RecordExport) (interface{}, error) + Exec(context.Context, *request.RecordExec) (interface{}, error) Create(context.Context, *request.RecordCreate) (interface{}, error) Read(context.Context, *request.RecordRead) (interface{}, error) Update(context.Context, *request.RecordUpdate) (interface{}, error) @@ -50,6 +51,7 @@ type Record struct { ImportRun func(http.ResponseWriter, *http.Request) ImportProgress func(http.ResponseWriter, *http.Request) Export func(http.ResponseWriter, *http.Request) + Exec func(http.ResponseWriter, *http.Request) Create func(http.ResponseWriter, *http.Request) Read func(http.ResponseWriter, *http.Request) Update func(http.ResponseWriter, *http.Request) @@ -179,6 +181,26 @@ func NewRecord(h RecordAPI) *Record { resputil.JSON(w, value) } }, + Exec: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewRecordExec() + if err := params.Fill(r); err != nil { + logger.LogParamError("Record.Exec", r, err) + resputil.JSON(w, err) + return + } + + value, err := h.Exec(r.Context(), params) + if err != nil { + logger.LogControllerError("Record.Exec", r, err, params.Auditable()) + resputil.JSON(w, err) + return + } + logger.LogControllerCall("Record.Exec", r, params.Auditable()) + if !serveHTTP(value, w, r) { + resputil.JSON(w, value) + } + }, Create: func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() params := request.NewRecordCreate() @@ -291,6 +313,7 @@ func (h Record) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http r.Patch("/namespace/{namespaceID}/module/{moduleID}/record/import/{sessionID}", h.ImportRun) r.Get("/namespace/{namespaceID}/module/{moduleID}/record/import/{sessionID}", h.ImportProgress) r.Get("/namespace/{namespaceID}/module/{moduleID}/record/export{filename}.{ext}", h.Export) + r.Post("/namespace/{namespaceID}/module/{moduleID}/record/exec/{procedure}", h.Exec) r.Post("/namespace/{namespaceID}/module/{moduleID}/record/", h.Create) r.Get("/namespace/{namespaceID}/module/{moduleID}/record/{recordID}", h.Read) r.Post("/namespace/{namespaceID}/module/{moduleID}/record/{recordID}", h.Update) diff --git a/compose/rest/record.go b/compose/rest/record.go index cd440088b..7bbdcab78 100644 --- a/compose/rest/record.go +++ b/compose/rest/record.go @@ -347,6 +347,28 @@ func (ctrl *Record) Export(ctx context.Context, r *request.RecordExport) (interf }, nil } +func (ctrl Record) Exec(ctx context.Context, r *request.RecordExec) (interface{}, error) { + aa := request.ProcedureArgs(r.Args) + + switch r.Procedure { + case "organize": + return resputil.OK(), ctrl.record.With(ctx).Organize( + r.NamespaceID, + r.ModuleID, + aa.GetUint64("recordID"), + aa.Get("sortingField"), + aa.Get("sortingValue"), + aa.Get("sortingFilter"), + aa.Get("valueField"), + aa.Get("value"), + ) + default: + return nil, errors.New("unknown procedure") + } + + return nil, nil +} + func (ctrl Record) makePayload(ctx context.Context, m *types.Module, r *types.Record, err error) (*recordPayload, error) { if err != nil || r == nil { return nil, err diff --git a/compose/rest/request/record.go b/compose/rest/request/record.go index bc4bf8a5a..9b4268e2b 100644 --- a/compose/rest/request/record.go +++ b/compose/rest/request/record.go @@ -435,6 +435,65 @@ func (r *RecordExport) Fill(req *http.Request) (err error) { var _ RequestFiller = NewRecordExport() +// Record exec request parameters +type RecordExec struct { + Procedure string + NamespaceID uint64 `json:",string"` + ModuleID uint64 `json:",string"` + Args []ProcedureArg +} + +func NewRecordExec() *RecordExec { + return &RecordExec{} +} + +func (r RecordExec) Auditable() map[string]interface{} { + var out = map[string]interface{}{} + + out["procedure"] = r.Procedure + out["namespaceID"] = r.NamespaceID + out["moduleID"] = r.ModuleID + out["args"] = r.Args + + return out +} + +func (r *RecordExec) Fill(req *http.Request) (err error) { + if strings.ToLower(req.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(req.Body).Decode(r) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = req.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := req.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := req.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + r.Procedure = chi.URLParam(req, "procedure") + r.NamespaceID = parseUInt64(chi.URLParam(req, "namespaceID")) + r.ModuleID = parseUInt64(chi.URLParam(req, "moduleID")) + + return err +} + +var _ RequestFiller = NewRecordExec() + // Record create request parameters type RecordCreate struct { Values types.RecordValueSet diff --git a/compose/rest/request/util.go b/compose/rest/request/util.go index 9274431f8..a452fb19c 100644 --- a/compose/rest/request/util.go +++ b/compose/rest/request/util.go @@ -12,6 +12,15 @@ import ( "github.com/pkg/errors" ) +type ( + ProcedureArgs []ProcedureArg + + ProcedureArg struct { + Name string `json:"name"` + Value string `json:"value"` + } +) + var truthy = regexp.MustCompile(`^\s*(t(rue)?|y(es)?|1)\s*$`) func parseJSONTextWithErr(s string) (types.JSONText, error) { @@ -97,3 +106,19 @@ func is(s string, matches ...string) bool { } return false } + +func (args ProcedureArgs) GetUint64(name string) uint64 { + u, _ := strconv.ParseUint(args.Get(name), 10, 64) + return u +} + +func (args ProcedureArgs) Get(name string) string { + name = strings.ToLower(name) + for _, arg := range args { + if strings.ToLower(arg.Name) == name { + return arg.Value + } + } + + return "" +} diff --git a/compose/service/record.go b/compose/service/record.go index 0c656fcac..1c3e50963 100644 --- a/compose/service/record.go +++ b/compose/service/record.go @@ -2,6 +2,7 @@ package service import ( "context" + "fmt" "regexp" "strconv" "time" @@ -69,6 +70,8 @@ type ( Update(record *types.Record) (*types.Record, error) DeleteByID(namespaceID, recordID uint64) error + + Organize(namespaceID, moduleID, recordID uint64, sortingField, sortingValue, sortingFilter, valueField, value string) error } Encoder interface { @@ -164,14 +167,14 @@ func (svc record) loadModule(namespaceID, moduleID uint64) (m *types.Module, err return } - if m.Fields, err = svc.moduleRepo.FindFields(m.ID); err != nil { - return - } - if !svc.ac.CanReadModule(svc.ctx, m) { return nil, ErrNoReadPermissions.withStack() } + if m.Fields, err = svc.moduleRepo.FindFields(m.ID); err != nil { + return + } + return } @@ -202,6 +205,8 @@ func (svc record) Report(namespaceID, moduleID uint64, metrics, dimensions, filt } func (svc record) Find(filter types.RecordFilter) (set types.RecordSet, f types.RecordFilter, err error) { + filter.PageFilter.NormalizePerPageWithDefaults() + var m *types.Module if m, err = svc.loadModule(filter.NamespaceID, filter.ModuleID); err != nil { return @@ -357,9 +362,7 @@ func (svc record) Update(mod *types.Record) (r *types.Record, err error) { return nil, ErrStaleData.withStack() } - now := time.Now() - r.UpdatedAt = &now - r.UpdatedBy = auth.GetIdentityFromContext(svc.ctx).Identity() + svc.recordInfoUpdate(r) if err = svc.copyChanges(m, mod, r); err != nil { return @@ -390,6 +393,12 @@ func (svc record) Update(mod *types.Record) (r *types.Record, err error) { }) } +func (svc record) recordInfoUpdate(r *types.Record) { + now := time.Now() + r.UpdatedAt = &now + r.UpdatedBy = auth.GetIdentityFromContext(svc.ctx).Identity() +} + func (svc record) DeleteByID(namespaceID, recordID uint64) (err error) { if recordID == 0 { return ErrInvalidID.withStack() @@ -433,6 +442,141 @@ func (svc record) DeleteByID(namespaceID, recordID uint64) (err error) { return errors.Wrap(err, "unable to delete record") } +// Organize - Record organizer +// +// Reorders records & sets field value +func (svc record) Organize(namespaceID, moduleID, recordID uint64, sField, sValue, sFilter, vField, vValue string) error { + var ( + _, module, record, err = svc.loadCombo(namespaceID, moduleID, recordID) + + recordValues = types.RecordValueSet{} + + reorderingRecords bool + ) + if err != nil { + return err + } + + if !svc.ac.CanUpdateRecord(svc.ctx, module) { + return ErrNoUpdatePermissions.withStack() + } + + if sField != "" { + reorderingRecords = true + + if !regexp.MustCompile(`^[0-9]+$`).MatchString(sValue) { + return errors.Errorf("expecting number for sorting position %q", sField) + } + + // Check field existence and permissions + // check if numeric -- we can not reorder on any other field type + + sf := module.Fields.FindByName(sField) + if sf == nil { + return errors.Errorf("no such field %q", sField) + } + + if !sf.IsNumeric() { + return errors.Errorf("can not reorder on non numeric field %q", sField) + } + + if sf.Multi { + return errors.Errorf("can not reorder on multi-value field %q", sField) + } + + if !svc.ac.CanUpdateRecordValue(svc.ctx, sf) { + return ErrNoUpdatePermissions.withStack() + } + + // Set new position + recordValues = recordValues.Set(&types.RecordValue{ + RecordID: recordID, + Name: sField, + Value: sValue, + }) + } + + if vField != "" { + // Check field existence and permissions + + vf := module.Fields.FindByName(vField) + if vf == nil { + return errors.Errorf("no such field %q", vField) + } + + if vf.Multi { + return errors.Errorf("can not update multi-value field %q", sField) + } + + if !svc.ac.CanUpdateRecordValue(svc.ctx, vf) { + return ErrNoUpdatePermissions.withStack() + } + + // Set new value + recordValues = recordValues.Set(&types.RecordValue{ + RecordID: recordID, + Name: vField, + Value: vValue, + }) + } + + return svc.db.Transaction(func() (err error) { + if len(recordValues) > 0 { + svc.recordInfoUpdate(record) + if _, err = svc.recordRepo.Update(record); err != nil { + return + } + + if err = svc.recordRepo.PartialUpdateValues(recordValues...); err != nil { + return + } + } + + if reorderingRecords { + var ( + set types.RecordSet + recordOrderPlace uint64 + ) + + // If we already have filter, wrap it in parenthesis + if sFilter != "" { + sFilter = fmt.Sprintf("(%s) AND ", sFilter) + } + + if recordOrderPlace, err = strconv.ParseUint(sValue, 0, 64); err != nil { + return + } + + // Assemble record filter: + // We are interested only in records that have value of a sorting field greater than + // the place we're moving our record to. + // and sort the set with sorting field + set, _, err = svc.recordRepo.Find(module, types.RecordFilter{ + Filter: fmt.Sprintf("%s(%s >= %d)", sFilter, sField, recordOrderPlace), + Sort: sField, + }) + + if err != nil { + return + } + + // Update value on each record + return set.Walk(func(r *types.Record) error { + recordOrderPlace++ + + // Update each and every set + return svc.recordRepo.PartialUpdateValues(&types.RecordValue{ + RecordID: r.ID, + Name: sField, + Value: strconv.FormatUint(recordOrderPlace, 10), + }) + }) + } + + return + }) +} + // loadCombo Loads everything we need for record manipulation // // Loads namespace, module, record and set of triggers. diff --git a/compose/types/record_value.go b/compose/types/record_value.go index c2106fc3f..1751a3dea 100644 --- a/compose/types/record_value.go +++ b/compose/types/record_value.go @@ -74,13 +74,13 @@ func (set RecordValueSet) Has(name string, place uint) bool { return false } -func (meta *RecordValueSet) Scan(value interface{}) error { +func (set *RecordValueSet) Scan(value interface{}) error { //lint:ignore S1034 This typecast is intentional, we need to get []byte out of a []uint8 switch value.(type) { case nil: - *meta = RecordValueSet{} + *set = RecordValueSet{} case []uint8: - if err := json.Unmarshal(value.([]byte), meta); err != nil { + if err := json.Unmarshal(value.([]byte), set); err != nil { return errors.Wrapf(err, "Can not scan '%v' into RecordValueSet", value) } } @@ -88,6 +88,6 @@ func (meta *RecordValueSet) Scan(value interface{}) error { return nil } -func (meta RecordValueSet) Value() (driver.Value, error) { - return json.Marshal(meta) +func (set RecordValueSet) Value() (driver.Value, error) { + return json.Marshal(set) } diff --git a/docs/compose/README.md b/docs/compose/README.md index 2400c9e89..cf42f1aed 100644 --- a/docs/compose/README.md +++ b/docs/compose/README.md @@ -960,6 +960,7 @@ Compose records | `PATCH` | `/namespace/{namespaceID}/module/{moduleID}/record/import/{sessionID}` | Run record import | | `GET` | `/namespace/{namespaceID}/module/{moduleID}/record/import/{sessionID}` | Get import progress | | `GET` | `/namespace/{namespaceID}/module/{moduleID}/record/export{filename}.{ext}` | Exports records that match | +| `POST` | `/namespace/{namespaceID}/module/{moduleID}/record/exec/{procedure}` | Executes server-side procedure over one or more module records | | `POST` | `/namespace/{namespaceID}/module/{moduleID}/record/` | Create record in module section | | `GET` | `/namespace/{namespaceID}/module/{moduleID}/record/{recordID}` | Read records by ID from module section | | `POST` | `/namespace/{namespaceID}/module/{moduleID}/record/{recordID}` | Update records in module section | @@ -1072,6 +1073,23 @@ Compose records | namespaceID | uint64 | PATH | Namespace ID | N/A | YES | | moduleID | uint64 | PATH | Module ID | N/A | YES | +## Executes server-side procedure over one or more module records + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/namespace/{namespaceID}/module/{moduleID}/record/exec/{procedure}` | HTTP/S | POST | | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| procedure | string | PATH | Name of procedure to execute | N/A | YES | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | +| moduleID | uint64 | PATH | Module ID | N/A | YES | +| args | []ProcedureArg | POST | Procedure arguments | N/A | NO | + ## Create record in module section #### Method diff --git a/tests/compose/main_test.go b/tests/compose/main_test.go index a2be81aee..5c2e17ae8 100644 --- a/tests/compose/main_test.go +++ b/tests/compose/main_test.go @@ -121,6 +121,11 @@ func newHelper(t *testing.T) helper { return h } +// Returns context w/ security details +func (h helper) secCtx() context.Context { + return auth.SetIdentityToContext(context.Background(), h.cUser) +} + // apitest basics, initialize, set handler, add auth func (h helper) apiInit() *apitest.APITest { InitApp() diff --git a/tests/compose/module_test.go b/tests/compose/module_test.go index 25c3319e0..349b2733d 100644 --- a/tests/compose/module_test.go +++ b/tests/compose/module_test.go @@ -17,10 +17,10 @@ func (h helper) repoModule() repository.ModuleRepository { return repository.Module(context.Background(), db()) } -func (h helper) repoMakeModule(ns *types.Namespace, name string) *types.Module { +func (h helper) repoMakeModule(ns *types.Namespace, name string, ff ...*types.ModuleField) *types.Module { m, err := h. repoModule(). - Create(&types.Module{Name: name, NamespaceID: ns.ID}) + Create(&types.Module{Name: name, NamespaceID: ns.ID, Fields: ff}) h.a.NoError(err) return m diff --git a/tests/compose/record_exec_test.go b/tests/compose/record_exec_test.go new file mode 100644 index 000000000..c277303af --- /dev/null +++ b/tests/compose/record_exec_test.go @@ -0,0 +1,161 @@ +package compose + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + "testing" + + "github.com/steinfletcher/apitest" + + "github.com/cortezaproject/corteza-server/compose/rest/request" + "github.com/cortezaproject/corteza-server/compose/service" + "github.com/cortezaproject/corteza-server/compose/types" + "github.com/cortezaproject/corteza-server/pkg/rh" + "github.com/cortezaproject/corteza-server/tests/helpers" +) + +func (h helper) apiSendRecordExec(nsID, modID uint64, proc string, args []request.ProcedureArg) *apitest.Response { + payload, err := json.Marshal(request.RecordExec{Args: args}) + h.a.NoError(err) + + return h.apiInit(). + Post(fmt.Sprintf("/namespace/%d/module/%d/record/exec/%s", nsID, modID, proc)). + JSON(string(payload)). + Expect(h.t) +} + +func TestRecordExecUnknownProcedure(t *testing.T) { + h := newHelper(t) + + h.apiInit(). + Post("/namespace/0/module/0/record/exec/test-unexisting-proc"). + Expect(t). + Status(http.StatusOK). + Assert(helpers.AssertError("unknown procedure")). + End() +} + +func TestRecordExec(t *testing.T) { + h := newHelper(t) + + h.allow(types.ModulePermissionResource.AppendWildcard(), "record.update") + + module := h.repoMakeRecordModuleWithFields( + "record testing module", + &types.ModuleField{Name: "position", Kind: "Number"}, + &types.ModuleField{Name: "handle"}, + &types.ModuleField{Name: "category"}, + ) + + makeRecord := func(position int, handle, cat string) *types.Record { + return h.repoMakeRecord(module, + &types.RecordValue{Name: "position", Value: strconv.Itoa(position)}, + &types.RecordValue{Name: "handle", Value: handle}, + &types.RecordValue{Name: "category", Value: cat}, + ) + } + + assertSort := func(expectedHandles string) { + // Using record service for fetching to avoid value pre-fetching etc.. + set, _, err := service.DefaultRecord.With(h.secCtx()).Find(types.RecordFilter{ + ModuleID: module.ID, + NamespaceID: module.NamespaceID, + Sort: "position ASC", + PageFilter: rh.PageFilter{}, + }) + + h.a.NoError(err) + h.a.NotNil(set) + + actualHandles := "" + _ = set.Walk(func(r *types.Record) error { + v := r.Values.FilterByName("handle") + + if len(v) == 1 { + actualHandles += v[0].Value + } else { + actualHandles += strconv.Itoa(len(v)) + } + + return nil + }) + + h.a.Equal(expectedHandles, actualHandles) + } + + var ( + aRec = makeRecord(1, "a", "CAT1") + bRec = makeRecord(2, "b", "CAT1") + cRec = makeRecord(3, "c", "CAT1") + dRec = makeRecord(4, "d", "CAT2") + eRec = makeRecord(5, "e", "CAT2") + fRec = makeRecord(6, "f", "CAT2") + gRec = makeRecord(7, "g", "CAT3") + hRec = makeRecord(8, "h", "CAT3") + iRec = makeRecord(9, "i", "CAT3") + ) + + // map handle to record ID so we can use it for reordering + rr := map[string]string{ + "a": strconv.FormatUint(aRec.ID, 10), + "b": strconv.FormatUint(bRec.ID, 10), + "c": strconv.FormatUint(cRec.ID, 10), + "d": strconv.FormatUint(dRec.ID, 10), + "e": strconv.FormatUint(eRec.ID, 10), + "f": strconv.FormatUint(fRec.ID, 10), + "g": strconv.FormatUint(gRec.ID, 10), + "h": strconv.FormatUint(hRec.ID, 10), + "i": strconv.FormatUint(iRec.ID, 10), + } + + assertSort("abcdefghi") + + h.apiSendRecordExec(module.NamespaceID, module.ID, "organize", request.ProcedureArgs{ + {"recordID", rr["a"]}, + {"sortingField", "position"}, + {"sortingValue", "5"}}). + Status(http.StatusOK). + Assert(helpers.AssertNoErrors). + End() + + assertSort("bcdeafghi") + + h.apiSendRecordExec(module.NamespaceID, module.ID, "organize", request.ProcedureArgs{ + {"recordID", rr["i"]}, + {"sortingField", "position"}, + {"sortingValue", "0"}}). + Status(http.StatusOK). + Assert(helpers.AssertNoErrors). + End() + + assertSort("ibcdeafgh") + + h.apiSendRecordExec(module.NamespaceID, module.ID, "organize", request.ProcedureArgs{ + {"recordID", rr["b"]}, + {"sortingFilter", "category = 'CAT1'"}, + {"sortingField", "position"}, + {"sortingValue", "5"}}). + Status(http.StatusOK). + Assert(helpers.AssertNoErrors). + End() + + assertSort("idecbfgah") + + h.apiSendRecordExec(module.NamespaceID, module.ID, "organize", request.ProcedureArgs{ + {"recordID", rr["b"]}, + {"valueField", "category"}, + {"value", "CAT2"}, + {"sortingValue", "5"}}). + Status(http.StatusOK). + Assert(helpers.AssertNoErrors). + End() + + assertSort("idecbfgah") + rsv, err := h.repoRecord().LoadValues([]string{"category"}, []uint64{bRec.ID}) + h.a.NoError(err) + h.a.NotNil(rsv) + h.a.Len(rsv.FilterByName("category"), 1) + h.a.Equal("CAT2", rsv.FilterByName("category")[0].Value) +} diff --git a/tests/compose/record_test.go b/tests/compose/record_test.go index 25ff1c7be..8a0925d53 100644 --- a/tests/compose/record_test.go +++ b/tests/compose/record_test.go @@ -18,45 +18,40 @@ func (h helper) repoRecord() repository.RecordRepository { return repository.Record(context.Background(), db()) } -func (h helper) repoMakeRecordModuleWithFields(name string) *types.Module { +func (h helper) repoMakeRecordModuleWithFields(name string, ff ...*types.ModuleField) *types.Module { namespace := h.repoMakeNamespace("record testing namespace") h.allow(types.NamespacePermissionResource.AppendWildcard(), "read") h.allow(types.ModulePermissionResource.AppendWildcard(), "read") h.allow(types.ModulePermissionResource.AppendWildcard(), "record.read") - m, err := h. - repoModule(). - Create(&types.Module{ - Name: name, - NamespaceID: namespace.ID, - Fields: types.ModuleFieldSet{ - &types.ModuleField{ - Name: "name", - }, - &types.ModuleField{ - Name: "email", - }, - &types.ModuleField{ - Name: "options", - Multi: true, - }, - &types.ModuleField{ - Name: "description", - }, - &types.ModuleField{ - Name: "another_record", - Kind: "Record", - }, + if len(ff) == 0 { + // Default fields + ff = types.ModuleFieldSet{ + &types.ModuleField{ + Name: "name", }, - }) + &types.ModuleField{ + Name: "email", + }, + &types.ModuleField{ + Name: "options", + Multi: true, + }, + &types.ModuleField{ + Name: "description", + }, + &types.ModuleField{ + Name: "another_record", + Kind: "Record", + }, + } + } - h.a.NoError(err) - - return m + return h.repoMakeModule(namespace, name, ff...) } -func (h helper) repoMakeRecord(module *types.Module, name string) *types.Record { +func (h helper) repoMakeRecord(module *types.Module, rvs ...*types.RecordValue) *types.Record { record, err := h. repoRecord(). Create(&types.Record{ @@ -68,6 +63,9 @@ func (h helper) repoMakeRecord(module *types.Module, name string) *types.Record }) h.a.NoError(err) + err = h.repoRecord().UpdateValues(record.ID, rvs) + h.a.NoError(err) + return record } @@ -75,7 +73,7 @@ func TestRecordRead(t *testing.T) { h := newHelper(t) module := h.repoMakeRecordModuleWithFields("record testing module") - record := h.repoMakeRecord(module, "some-record") + record := h.repoMakeRecord(module) h.apiInit(). Get(fmt.Sprintf("/namespace/%d/module/%d/record/%d", module.NamespaceID, module.ID, record.ID)). @@ -91,8 +89,8 @@ func TestRecordList(t *testing.T) { module := h.repoMakeRecordModuleWithFields("record testing module") - h.repoMakeRecord(module, "app") - h.repoMakeRecord(module, "app") + h.repoMakeRecord(module) + h.repoMakeRecord(module) h.apiInit(). Get(fmt.Sprintf("/namespace/%d/module/%d/record/", module.NamespaceID, module.ID)). @@ -109,7 +107,7 @@ func TestRecordCreateForbidden(t *testing.T) { h.apiInit(). Post(fmt.Sprintf("/namespace/%d/module/%d/record/", module.NamespaceID, module.ID)). - FormData("name", "some-record"). + FormData("name"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertError("compose.service.NoCreatePermissions")). @@ -124,7 +122,7 @@ func TestRecordCreate(t *testing.T) { h.apiInit(). Post(fmt.Sprintf("/namespace/%d/module/%d/record/", module.NamespaceID, module.ID)). - FormData("name", "some-record"). + FormData("name"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). @@ -135,7 +133,7 @@ func TestRecordUpdateForbidden(t *testing.T) { h := newHelper(t) module := h.repoMakeRecordModuleWithFields("record testing module") - record := h.repoMakeRecord(module, "some-record") + record := h.repoMakeRecord(module) h.apiInit(). Post(fmt.Sprintf("/namespace/%d/module/%d/record/%d", module.NamespaceID, module.ID, record.ID)). @@ -150,7 +148,7 @@ func TestRecordUpdate(t *testing.T) { h := newHelper(t) module := h.repoMakeRecordModuleWithFields("record testing module") - record := h.repoMakeRecord(module, "some-record") + record := h.repoMakeRecord(module) h.allow(types.ModulePermissionResource.AppendWildcard(), "record.update") h.apiInit(). @@ -171,7 +169,7 @@ func TestRecordDeleteForbidden(t *testing.T) { h := newHelper(t) module := h.repoMakeRecordModuleWithFields("record testing module") - record := h.repoMakeRecord(module, "some-record") + record := h.repoMakeRecord(module) h.apiInit(). Delete(fmt.Sprintf("/namespace/%d/module/%d/record/%d", module.NamespaceID, module.ID, record.ID)). @@ -185,7 +183,7 @@ func TestRecordDelete(t *testing.T) { h := newHelper(t) module := h.repoMakeRecordModuleWithFields("record testing module") - record := h.repoMakeRecord(module, "some-record") + record := h.repoMakeRecord(module) h.allow(types.ModulePermissionResource.AppendWildcard(), "record.delete")