Add record procedure exec capabilities
This will allow us to perform bulk operations on record & record-values
This commit is contained in:
parent
05dfa30500
commit
dc99bc2cb8
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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)
|
||||
})
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 ""
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
161
tests/compose/record_exec_test.go
Normal file
161
tests/compose/record_exec_test.go
Normal file
@ -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)
|
||||
}
|
||||
@ -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")
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user