3
0

Add support for bulk record delete

This commit is contained in:
Denis Arh 2020-02-15 17:55:44 +01:00
parent dffeba1cb9
commit 1da269abc7
8 changed files with 246 additions and 35 deletions

View File

@ -972,6 +972,28 @@
]
}
},
{
"name": "bulkDelete",
"method": "DELETE",
"title": "Delete record row from module section",
"path": "/",
"parameters": {
"post": [
{
"type": "[]string",
"name": "recordIDs",
"required": false,
"title": "IDs of records to delete"
},
{
"type": "bool",
"name": "truncate",
"required": false,
"title": "Remove ALL records of a specified module (pending implementation)"
}
]
}
},
{
"name": "delete",
"method": "DELETE",

View File

@ -269,6 +269,28 @@
]
}
},
{
"Name": "bulkDelete",
"Method": "DELETE",
"Title": "Delete record row from module section",
"Path": "/",
"Parameters": {
"post": [
{
"name": "recordIDs",
"required": false,
"title": "IDs of records to delete",
"type": "[]string"
},
{
"name": "truncate",
"required": false,
"title": "Remove ALL records of a specified module (pending implementation)",
"type": "bool"
}
]
}
},
{
"Name": "delete",
"Method": "DELETE",

View File

@ -39,6 +39,7 @@ type RecordAPI interface {
Create(context.Context, *request.RecordCreate) (interface{}, error)
Read(context.Context, *request.RecordRead) (interface{}, error)
Update(context.Context, *request.RecordUpdate) (interface{}, error)
BulkDelete(context.Context, *request.RecordBulkDelete) (interface{}, error)
Delete(context.Context, *request.RecordDelete) (interface{}, error)
Upload(context.Context, *request.RecordUpload) (interface{}, error)
TriggerScript(context.Context, *request.RecordTriggerScript) (interface{}, error)
@ -56,6 +57,7 @@ type Record struct {
Create func(http.ResponseWriter, *http.Request)
Read func(http.ResponseWriter, *http.Request)
Update func(http.ResponseWriter, *http.Request)
BulkDelete func(http.ResponseWriter, *http.Request)
Delete func(http.ResponseWriter, *http.Request)
Upload func(http.ResponseWriter, *http.Request)
TriggerScript func(http.ResponseWriter, *http.Request)
@ -263,6 +265,26 @@ func NewRecord(h RecordAPI) *Record {
resputil.JSON(w, value)
}
},
BulkDelete: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewRecordBulkDelete()
if err := params.Fill(r); err != nil {
logger.LogParamError("Record.BulkDelete", r, err)
resputil.JSON(w, err)
return
}
value, err := h.BulkDelete(r.Context(), params)
if err != nil {
logger.LogControllerError("Record.BulkDelete", r, err, params.Auditable())
resputil.JSON(w, err)
return
}
logger.LogControllerCall("Record.BulkDelete", r, params.Auditable())
if !serveHTTP(value, w, r) {
resputil.JSON(w, value)
}
},
Delete: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewRecordDelete()
@ -339,6 +361,7 @@ func (h Record) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http
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)
r.Delete("/namespace/{namespaceID}/module/{moduleID}/record/", h.BulkDelete)
r.Delete("/namespace/{namespaceID}/module/{moduleID}/record/{recordID}", h.Delete)
r.Post("/namespace/{namespaceID}/module/{moduleID}/record/attachment", h.Upload)
r.Post("/namespace/{namespaceID}/module/{moduleID}/record/{recordID}/trigger", h.TriggerScript)

View File

@ -5,6 +5,7 @@ import (
"encoding/csv"
"encoding/json"
"fmt"
"github.com/cortezaproject/corteza-server/pkg/payload"
"net/http"
"strings"
@ -151,7 +152,19 @@ func (ctrl *Record) Update(ctx context.Context, r *request.RecordUpdate) (interf
}
func (ctrl *Record) Delete(ctx context.Context, r *request.RecordDelete) (interface{}, error) {
return resputil.OK(), ctrl.record.With(ctx).DeleteByID(r.NamespaceID, r.RecordID)
return resputil.OK(), ctrl.record.With(ctx).DeleteByID(r.NamespaceID, r.ModuleID, r.RecordID)
}
func (ctrl *Record) BulkDelete(ctx context.Context, r *request.RecordBulkDelete) (interface{}, error) {
if r.Truncate {
return nil, errors.New("pending implementation")
}
return resputil.OK(), ctrl.record.With(ctx).DeleteByID(
r.NamespaceID,
r.ModuleID,
payload.ParseUInt64s(r.RecordIDs)...,
)
}
func (ctrl *Record) Upload(ctx context.Context, r *request.RecordUpload) (interface{}, error) {

View File

@ -666,6 +666,71 @@ func (r *RecordUpdate) Fill(req *http.Request) (err error) {
var _ RequestFiller = NewRecordUpdate()
// Record bulkDelete request parameters
type RecordBulkDelete struct {
RecordIDs []string
Truncate bool
NamespaceID uint64 `json:",string"`
ModuleID uint64 `json:",string"`
}
func NewRecordBulkDelete() *RecordBulkDelete {
return &RecordBulkDelete{}
}
func (r RecordBulkDelete) Auditable() map[string]interface{} {
var out = map[string]interface{}{}
out["recordIDs"] = r.RecordIDs
out["truncate"] = r.Truncate
out["namespaceID"] = r.NamespaceID
out["moduleID"] = r.ModuleID
return out
}
func (r *RecordBulkDelete) 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])
}
if val, ok := req.Form["recordIDs"]; ok {
r.RecordIDs = parseStrings(val)
}
if val, ok := post["truncate"]; ok {
r.Truncate = parseBool(val)
}
r.NamespaceID = parseUInt64(chi.URLParam(req, "namespaceID"))
r.ModuleID = parseUInt64(chi.URLParam(req, "moduleID"))
return err
}
var _ RequestFiller = NewRecordBulkDelete()
// Record delete request parameters
type RecordDelete struct {
RecordID uint64 `json:",string"`

View File

@ -20,6 +20,7 @@ const (
ErrNoDeletePermissions serviceError = "NoDeletePermissions"
ErrNoTriggerManagementPermissions serviceError = "NoTriggerManagementPermissions"
ErrNamespaceRequired serviceError = "NamespaceRequired"
ErrInvalidModuleID serviceError = "InvalidModuleID"
ErrModulePageExists serviceError = "ModulePageExists"
ErrNotImplemented serviceError = "NotImplemented"
ErrRecordImportSessionNotFound serviceError = "RecordImportSessionNotFound"

View File

@ -64,7 +64,7 @@ type (
Create(record *types.Record) (*types.Record, error)
Update(record *types.Record) (*types.Record, error)
DeleteByID(namespaceID, recordID uint64) error
DeleteByID(namespaceID, moduleID uint64, recordID ...uint64) error
Organize(namespaceID, moduleID, recordID uint64, sortingField, sortingValue, sortingFilter, valueField, value string) error
}
@ -395,49 +395,92 @@ func (svc record) recordInfoUpdate(r *types.Record) {
r.UpdatedBy = auth.GetIdentityFromContext(svc.ctx).Identity()
}
func (svc record) DeleteByID(namespaceID, recordID uint64) (err error) {
if recordID == 0 {
// DeleteByID removes one or more records (all from the same module and namespace)
//
// Before and after each record is deleted beforeDelete and afterDelete events are emitted
// If beforeRecord aborts the action it does so for that specific record only
func (svc record) DeleteByID(namespaceID, moduleID uint64, recordIDs ...uint64) error {
if namespaceID == 0 {
return ErrInvalidID.withStack()
}
ns, m, del, err := svc.loadCombo(namespaceID, 0, recordID)
if moduleID == 0 {
return ErrInvalidID.withStack()
}
var (
isBulkDelete = len(recordIDs) > 0
now = time.Now()
ns *types.Namespace
m *types.Module
err error
)
ns, m, _, err = svc.loadCombo(namespaceID, moduleID, 0)
if err != nil {
return
return err
}
if !svc.ac.CanDeleteRecord(svc.ctx, m) {
return ErrNoDeletePermissions.withStack()
}
// Preload old record values so we can send it together with event
if err = svc.preloadValues(m, del); err != nil {
return
}
// Calling before-record-delete scripts
if err = svc.eventbus.WaitFor(svc.ctx, event.RecordBeforeDelete(nil, del, m, ns)); err != nil {
return
}
err = svc.db.Transaction(func() (err error) {
now := time.Now()
del.DeletedAt = &now
del.DeletedBy = auth.GetIdentityFromContext(svc.ctx).Identity()
if err = svc.recordRepo.Delete(del); err != nil {
return
for _, recordID := range recordIDs {
if recordID == 0 {
return ErrInvalidID.withStack()
}
if err = svc.recordRepo.DeleteValues(del); err != nil {
return
err := svc.db.Transaction(func() (err error) {
var (
del *types.Record
)
del, err = svc.FindByID(namespaceID, recordID)
if err != nil {
return err
}
// Preload old record values so we can send it together with event
if err = svc.preloadValues(m, del); err != nil {
return err
}
// Calling before-record-delete scripts
if err = svc.eventbus.WaitFor(svc.ctx, event.RecordBeforeDelete(nil, del, m, ns)); err != nil {
if isBulkDelete {
// Not considered fatal,
// continue with next record
return nil
} else {
return err
}
}
del.DeletedAt = &now
del.DeletedBy = auth.GetIdentityFromContext(svc.ctx).Identity()
if err = svc.recordRepo.Delete(del); err != nil {
return err
}
if err = svc.recordRepo.DeleteValues(del); err != nil {
return err
}
defer svc.eventbus.Dispatch(svc.ctx, event.RecordAfterDelete(nil, del, m, ns))
return err
})
if err != nil {
return errors.Wrap(err, "failed to delete record")
}
}
defer svc.eventbus.Dispatch(svc.ctx, event.RecordAfterDelete(nil, del, m, ns))
return
})
return errors.Wrap(err, "unable to delete record")
return nil
}
// Organize - Record organizer
@ -595,7 +638,7 @@ func (svc record) Organize(namespaceID, moduleID, recordID uint64, posField, pos
// Loads namespace, module, record and set of triggers.
func (svc record) loadCombo(namespaceID, moduleID, recordID uint64) (ns *types.Namespace, m *types.Module, r *types.Record, err error) {
if namespaceID == 0 {
err = ErrNamespaceRequired
err = ErrNamespaceRequired.withStack()
return
}
if ns, err = svc.loadNamespace(namespaceID); err != nil {
@ -607,11 +650,15 @@ func (svc record) loadCombo(namespaceID, moduleID, recordID uint64) (ns *types.N
return
}
moduleID = r.ModuleID
if r.ModuleID != moduleID && moduleID > 0 {
return nil, nil, nil, ErrInvalidModuleID.withStack()
}
}
if m, err = svc.loadModule(ns.ID, moduleID); err != nil {
return
if moduleID > 0 {
if m, err = svc.loadModule(ns.ID, moduleID); err != nil {
return
}
}
return

View File

@ -823,6 +823,7 @@ Compose 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 |
| `DELETE` | `/namespace/{namespaceID}/module/{moduleID}/record/` | Delete record row from module section |
| `DELETE` | `/namespace/{namespaceID}/module/{moduleID}/record/{recordID}` | Delete record row from module section |
| `POST` | `/namespace/{namespaceID}/module/{moduleID}/record/attachment` | Uploads attachment and validates it against record field requirements |
| `POST` | `/namespace/{namespaceID}/module/{moduleID}/record/{recordID}/trigger` | Fire compose:record trigger |
@ -1003,6 +1004,23 @@ Compose records
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/namespace/{namespaceID}/module/{moduleID}/record/` | HTTP/S | DELETE | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| recordIDs | []string | POST | IDs of records to delete | N/A | NO |
| truncate | bool | POST | Remove ALL records of a specified module (pending implementation) | N/A | NO |
| namespaceID | uint64 | PATH | Namespace ID | N/A | YES |
| moduleID | uint64 | PATH | Module ID | N/A | YES |
## Delete record row from module section
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/namespace/{namespaceID}/module/{moduleID}/record/{recordID}` | HTTP/S | DELETE | |