Add support for bulk record delete
This commit is contained in:
parent
dffeba1cb9
commit
1da269abc7
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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"`
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 | |
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user