3
0

Add ability to select and perform bulk operations on all records

This change affects the recordlist and the bulk operations are edit,delete and undelete
This commit is contained in:
Mumbi Francis
2023-05-23 18:01:06 +03:00
committed by Mumbi Francis
parent 0d47243e5d
commit 89f621c035
13 changed files with 343 additions and 171 deletions
@@ -72,6 +72,7 @@
:query="query"
:prefilter="prefilter"
:selection="selected"
:selected-all-records-count="selectedAllRecords ? pagination.count : 0"
:processing="processing"
:preselected-fields="fields.map(({ moduleField }) => moduleField)"
class="mr-1 float-left"
@@ -164,37 +165,37 @@
</div>
<div
class="d-none flex-wrap align-items-center mt-1"
:class="{ 'd-flex': options.selectable && selected.length }"
v-if="options.selectable && selected.length"
class="d-flex align-items-center flex-wrap align-items-center mt-2"
>
<div
class="d-flex align-items-baseline my-auto pt-1 text-nowrap h-100"
>
{{ $t('recordList.selected', { count: selected.length, total: items.length }) }}
<b-button
variant="link"
class="p-0 text-decoration-none"
@click.prevent="handleSelectAllOnPage({ isChecked: false })"
>
({{ $t('recordList.cancelSelection') }})
</b-button>
<div class="mr-1">
{{ selectedRecordsDisplayText }}
</div>
<b-button
size="sm"
variant="outline-light"
class="text-primary border-0"
@click="selectAllRecords()"
>
{{ selectedAllRecords ? $t('recordList.unselectAllRecords') : $t('recordList.selectAllRecords') }}
</b-button>
<div class="d-flex align-items-center ml-auto">
<automation-buttons
class="d-inline m-0 mr-2"
:buttons="options.selectionButtons"
:module="recordListModule"
:extra-event-args="{ selected, filter }"
:extra-event-args="{ selected, filter}"
v-bind="$props"
@refresh="refresh()"
/>
<bulk-edit-modal
v-show="options.bulkRecordEditEnabled && canUpdateSelectedRecords"
v-show="options.bulkRecordEditEnabled && canUpdateSelectedRecords && !showingDeletedRecords"
:module="recordListModule"
:namespace="namespace"
:selected-records="selected"
:selected-records="selectedAllRecords ? [] : selected"
@save="onBulkUpdate()"
/>
@@ -433,7 +434,7 @@
:extra-options="options"
/>
<div
v-if="options.inlineRecordEditEnabled && field.canEdit"
v-if="options.inlineRecordEditEnabled && field.canEdit && !showingDeletedRecords"
class="inline-actions"
>
<b-button
@@ -844,6 +845,7 @@ export default {
customPresetFilters: [],
currentCustomPresetFilter: undefined,
showCustomPresetFilterModal: false,
selectedAllRecords: false,
}
},
@@ -1020,6 +1022,14 @@ export default {
authUserRoles () {
return this.$auth.user.roles
},
selectedRecordsDisplayText () {
const count = this.selectedAllRecords ? (this.options.showTotalCount ? this.pagination.count : undefined) : this.selected.length
const total = this.items.length
const key = this.selectedAllRecords ? 'selectedFromAllPages' : 'selected'
return this.$t(`recordList.${key}`, { count, total })
},
},
watch: {
@@ -1150,6 +1160,8 @@ export default {
return
}
this.selected.splice(i, 1)
this.selectedAllRecords = false
}
},
@@ -1174,6 +1186,7 @@ export default {
handleShowDeleted () {
this.showingDeletedRecords = !this.showingDeletedRecords
this.selectedAllRecords = false
this.refresh(true)
},
@@ -1381,7 +1394,7 @@ export default {
query: {
fields: e.fields,
// url.Make already URL encodes the the values, so the filter shouldn't be encoded
filter: filter,
filter: this.selectedAllRecords ? '' : filter,
jwt: this.$auth.accessToken,
timezone: timezone ? timezone.tzCode : undefined,
},
@@ -1468,9 +1481,15 @@ export default {
this.selected = this.items.map(({ id }) => id)
} else {
this.selected = []
this.selectedAllRecords = isChecked
}
},
selectAllRecords () {
this.selectedAllRecords = !this.selectedAllRecords
this.handleSelectAllOnPage({ isChecked: this.selectedAllRecords })
},
handleRestoreSelectedRecords () {
if (this.inlineEditing) {
const sel = new Set(this.selected)
@@ -1482,15 +1501,19 @@ export default {
} else {
const { moduleID, namespaceID } = this.items[0].r
// filter undeletable records from the selected list
const recordIDs = this.items
.filter(({ id, r }) => r.canUndeleteRecord && this.selected.includes(id))
.map(({ id }) => id)
let query = ''
this.processing = true
if (!this.selectedAllRecords) {
// filter deletable records from the selected list
const recordIDs = this.items
.filter(({ id, r }) => r.canUndeleteRecord && this.selected.includes(id))
.map(({ id }) => id)
query = recordIDs.map(r => `recordID='${r}'`).join(' OR ')
}
this.$ComposeAPI
.recordBulkUndelete({ moduleID, namespaceID, recordIDs })
.recordBulkUndelete({ moduleID, namespaceID, query })
.then(() => {
this.refresh(true)
this.toastSuccess(this.$t('notification:record.undeleteBulkSuccess'))
@@ -1498,6 +1521,7 @@ export default {
.catch(this.toastErrorHandler(this.$t('notification:record.undeleteBulkFailed')))
.finally(() => {
this.processing = false
this.selectedAllRecords = false
})
}
},
@@ -1521,15 +1545,21 @@ export default {
// same module so this should be safe to do.
const { moduleID, namespaceID } = this.items[0].r
// filter deletable records from the selected list
const recordIDs = this.items
.filter(({ id, r }) => r.canDeleteRecord && selected.includes(id))
.map(({ id }) => id)
let query = ''
if (!this.selectedAllRecords) {
// filter deletable records from the selected list
const recordIDs = this.items
.filter(({ id, r }) => r.canDeleteRecord && selected.includes(id))
.map(({ id }) => id)
query = recordIDs.map(r => `recordID='${r}'`).join(' OR ')
}
this.processing = true
this.$ComposeAPI
.recordBulkDelete({ moduleID, namespaceID, recordIDs })
.recordBulkDelete({ moduleID, namespaceID, query })
.then(() => this.refresh(true))
.then(() => {
this.toastSuccess(this.$t('notification:record.deleteBulkSuccess'))
@@ -1537,6 +1567,7 @@ export default {
.catch(this.toastErrorHandler(this.$t('notification:record.deleteBulkFailed')))
.finally(() => {
this.processing = false
this.selectedAllRecords = false
})
}
},
@@ -1762,6 +1793,7 @@ export default {
},
onBulkUpdate () {
this.selectedAllRecords = false
this.refresh(true)
},
@@ -232,6 +232,11 @@ export default {
required: false,
default: () => [],
},
selectedAllRecordsCount: {
type: Number,
required: false,
default: 0,
},
filterRangeType: {
type: String,
default: 'all',
@@ -375,6 +380,10 @@ export default {
getExportableCount () {
// when exporting selection, only selected records are applicable
if (this.rangeType === 'selection') {
if (this.selectedAllRecordsCount !== 0) {
return this.selectedAllRecordsCount
}
return this.selection.length
}
+3 -1
View File
@@ -281,8 +281,10 @@ export default {
const { moduleID, namespaceID } = this.module
const query = records.map(r => `recordID='${r}'`).join(' OR ')
return this
.$ComposeAPI.recordPatch({ moduleID, namespaceID, records, values })
.$ComposeAPI.recordPatch({ moduleID, namespaceID, values, query })
.catch(err => {
const { details = undefined } = err
if (!!details && Array.isArray(details) && details.length > 0) {
+6 -6
View File
@@ -2185,8 +2185,8 @@ export default class Compose {
const {
namespaceID,
moduleID,
records,
values,
query,
} = (a as KV) || {}
if (!namespaceID) {
throw Error('field namespaceID is empty')
@@ -2202,8 +2202,8 @@ export default class Compose {
}),
}
cfg.data = {
records,
values,
query,
}
return this.api().request(cfg).then(result => stdResolve(result))
}
@@ -2221,8 +2221,8 @@ export default class Compose {
const {
namespaceID,
moduleID,
recordIDs,
truncate,
query,
} = (a as KV) || {}
if (!namespaceID) {
throw Error('field namespaceID is empty')
@@ -2238,8 +2238,8 @@ export default class Compose {
}),
}
cfg.data = {
recordIDs,
truncate,
query,
}
return this.api().request(cfg).then(result => stdResolve(result))
}
@@ -2329,7 +2329,7 @@ export default class Compose {
const {
namespaceID,
moduleID,
recordIDs,
query,
} = (a as KV) || {}
if (!namespaceID) {
throw Error('field namespaceID is empty')
@@ -2345,7 +2345,7 @@ export default class Compose {
}),
}
cfg.data = {
recordIDs,
query,
}
return this.api().request(cfg).then(result => stdResolve(result))
}
@@ -484,6 +484,9 @@ recordList:
label: Parent field
selectable: Users will be able to select records
selected: '{{count}} of {{total}} records selected'
selectedFromAllPages: 'All {{count}} records from all pages selected'
selectAllRecords: Select all records on all pages
unselectAllRecords: Clear selection
sort:
tooltip: Sort column
showRecords:
+44 -33
View File
@@ -81,9 +81,10 @@ func (a recordsLookupArgs) GetRecord() (bool, uint64, *types.Record) {
// Lookup function Compose record lookup
//
// expects implementation of lookup function:
// func (h recordsHandler) lookup(ctx context.Context, args *recordsLookupArgs) (results *recordsLookupResults, err error) {
// return
// }
//
// func (h recordsHandler) lookup(ctx context.Context, args *recordsLookupArgs) (results *recordsLookupResults, err error) {
// return
// }
func (h recordsHandler) Lookup() *atypes.Function {
return &atypes.Function{
Ref: "composeRecordsLookup",
@@ -255,9 +256,10 @@ func (a recordsSearchArgs) GetNamespace() (bool, uint64, string, *types.Namespac
// Search function Compose records search
//
// expects implementation of search function:
// func (h recordsHandler) search(ctx context.Context, args *recordsSearchArgs) (results *recordsSearchResults, err error) {
// return
// }
//
// func (h recordsHandler) search(ctx context.Context, args *recordsSearchArgs) (results *recordsSearchResults, err error) {
// return
// }
func (h recordsHandler) Search() *atypes.Function {
return &atypes.Function{
Ref: "composeRecordsSearch",
@@ -508,9 +510,10 @@ func (a recordsFirstArgs) GetNamespace() (bool, uint64, string, *types.Namespace
// First function Compose record lookup (first created)
//
// expects implementation of first function:
// func (h recordsHandler) first(ctx context.Context, args *recordsFirstArgs) (results *recordsFirstResults, err error) {
// return
// }
//
// func (h recordsHandler) first(ctx context.Context, args *recordsFirstArgs) (results *recordsFirstResults, err error) {
// return
// }
func (h recordsHandler) First() *atypes.Function {
return &atypes.Function{
Ref: "composeRecordsFirst",
@@ -637,9 +640,10 @@ func (a recordsLastArgs) GetNamespace() (bool, uint64, string, *types.Namespace)
// Last function Compose record lookup (last created)
//
// expects implementation of last function:
// func (h recordsHandler) last(ctx context.Context, args *recordsLastArgs) (results *recordsLastResults, err error) {
// return
// }
//
// func (h recordsHandler) last(ctx context.Context, args *recordsLastArgs) (results *recordsLastResults, err error) {
// return
// }
func (h recordsHandler) Last() *atypes.Function {
return &atypes.Function{
Ref: "composeRecordsLast",
@@ -792,9 +796,10 @@ func (a recordsEachArgs) GetNamespace() (bool, uint64, string, *types.Namespace)
// Each function Compose records
//
// expects implementation of each function:
// func (h recordsHandler) each(ctx context.Context, args *recordsEachArgs) (results *recordsEachResults, err error) {
// return
// }
//
// func (h recordsHandler) each(ctx context.Context, args *recordsEachArgs) (results *recordsEachResults, err error) {
// return
// }
func (h recordsHandler) Each() *atypes.Function {
return &atypes.Function{
Ref: "composeRecordsEach",
@@ -959,9 +964,10 @@ func (a recordsNewArgs) GetNamespace() (bool, uint64, string, *types.Namespace)
// New function Compose record maker
//
// expects implementation of new function:
// func (h recordsHandler) new(ctx context.Context, args *recordsNewArgs) (results *recordsNewResults, err error) {
// return
// }
//
// func (h recordsHandler) new(ctx context.Context, args *recordsNewArgs) (results *recordsNewResults, err error) {
// return
// }
func (h recordsHandler) New() *atypes.Function {
return &atypes.Function{
Ref: "composeRecordsNew",
@@ -1072,9 +1078,10 @@ type (
// Validate function Compose record validator
//
// expects implementation of validate function:
// func (h recordsHandler) validate(ctx context.Context, args *recordsValidateArgs) (results *recordsValidateResults, err error) {
// return
// }
//
// func (h recordsHandler) validate(ctx context.Context, args *recordsValidateArgs) (results *recordsValidateResults, err error) {
// return
// }
func (h recordsHandler) Validate() *atypes.Function {
return &atypes.Function{
Ref: "composeRecordsValidate",
@@ -1152,9 +1159,10 @@ type (
// Create function Compose record create
//
// expects implementation of create function:
// func (h recordsHandler) create(ctx context.Context, args *recordsCreateArgs) (results *recordsCreateResults, err error) {
// return
// }
//
// func (h recordsHandler) create(ctx context.Context, args *recordsCreateArgs) (results *recordsCreateResults, err error) {
// return
// }
func (h recordsHandler) Create() *atypes.Function {
return &atypes.Function{
Ref: "composeRecordsCreate",
@@ -1229,9 +1237,10 @@ type (
// Update function Compose record update
//
// expects implementation of update function:
// func (h recordsHandler) update(ctx context.Context, args *recordsUpdateArgs) (results *recordsUpdateResults, err error) {
// return
// }
//
// func (h recordsHandler) update(ctx context.Context, args *recordsUpdateArgs) (results *recordsUpdateResults, err error) {
// return
// }
func (h recordsHandler) Update() *atypes.Function {
return &atypes.Function{
Ref: "composeRecordsUpdate",
@@ -1328,9 +1337,10 @@ func (a recordsDeleteArgs) GetRecord() (bool, uint64, *types.Record) {
// Delete function Compose record delete
//
// expects implementation of delete function:
// func (h recordsHandler) delete(ctx context.Context, args *recordsDeleteArgs) (err error) {
// return
// }
//
// func (h recordsHandler) delete(ctx context.Context, args *recordsDeleteArgs) (err error) {
// return
// }
func (h recordsHandler) Delete() *atypes.Function {
return &atypes.Function{
Ref: "composeRecordsDelete",
@@ -1454,9 +1464,10 @@ func (a recordsReportArgs) GetNamespace() (bool, uint64, string, *types.Namespac
// Report function Report
//
// expects implementation of report function:
// func (h recordsHandler) report(ctx context.Context, args *recordsReportArgs) (results *recordsReportResults, err error) {
// return
// }
//
// func (h recordsHandler) report(ctx context.Context, args *recordsReportArgs) (results *recordsReportResults, err error) {
// return
// }
func (h recordsHandler) Report() *atypes.Function {
return &atypes.Function{
Ref: "composeRecordsReport",
+8 -8
View File
@@ -1167,22 +1167,22 @@ endpoints:
path: "/"
parameters:
post:
- { name: records, type: "[]string", title: Records to update }
- { name: values, type: "types.RecordValueSet", title: Fields to update and their values }
- { name: query, type: "string", title: Search query for records to operate on }
- 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: query
type: string
required: false
title: Search query for records to operate on
- name: delete
method: DELETE
title: Delete record row from module section
@@ -1209,10 +1209,10 @@ endpoints:
path: "/undelete"
parameters:
post:
- type: "[]string"
name: recordIDs
- name: query
type: string
required: false
title: IDs of records to undelete
title: Search query for records to operate on
- name: upload
path: "/attachment"
method: POST
+29 -27
View File
@@ -11,9 +11,6 @@ import (
"strings"
"time"
"github.com/cortezaproject/corteza/server/pkg/revisions"
"github.com/spf13/cast"
"github.com/cortezaproject/corteza/server/compose/rest/request"
"github.com/cortezaproject/corteza/server/compose/service"
"github.com/cortezaproject/corteza/server/compose/types"
@@ -26,7 +23,7 @@ import (
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
estore "github.com/cortezaproject/corteza/server/pkg/envoy/store"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/payload"
"github.com/cortezaproject/corteza/server/pkg/revisions"
"github.com/cortezaproject/corteza/server/store"
)
@@ -233,34 +230,29 @@ func (ctrl *Record) Create(ctx context.Context, r *request.RecordCreate) (interf
func (ctrl *Record) Patch(ctx context.Context, req *request.RecordPatch) (interface{}, error) {
var (
f = types.RecordFilter{
Query: req.Query,
NamespaceID: req.NamespaceID,
ModuleID: req.ModuleID,
Deleted: filter.State(0),
}
err error
)
oo := make([]*types.RecordBulkOperation, 0)
counters := make(map[string]uint)
for _, v := range req.Values {
v.Place = counters[v.Name]
counters[v.Name]++
}
for _, r := range req.Records {
oo = append(oo, &types.RecordBulkOperation{
RecordID: cast.ToUint64(r),
NamespaceID: req.NamespaceID,
ModuleID: req.ModuleID,
err = ctrl.record.BulkModifyByFilter(ctx, f, req.Values, types.OperationTypePatch)
Record: &types.Record{Values: req.Values},
Operation: types.OperationTypePatch,
})
}
results, err := ctrl.record.Bulk(ctx, true, oo...)
if rve := types.IsRecordValueErrorSet(err); rve != nil {
return ctrl.handleValidationError(rve), nil
}
return ctrl.makeRecordBulkPatchPayload(ctx, results, err)
return api.OK(), err
}
func (ctrl *Record) Update(ctx context.Context, r *request.RecordUpdate) (interface{}, error) {
@@ -329,15 +321,20 @@ func (ctrl *Record) Delete(ctx context.Context, r *request.RecordDelete) (interf
}
func (ctrl *Record) BulkDelete(ctx context.Context, r *request.RecordBulkDelete) (interface{}, error) {
var (
f = types.RecordFilter{
Query: r.Query,
NamespaceID: r.NamespaceID,
ModuleID: r.ModuleID,
Deleted: filter.State(0),
}
)
if r.Truncate {
return nil, fmt.Errorf("pending implementation")
}
return api.OK(), ctrl.record.DeleteByID(ctx,
r.NamespaceID,
r.ModuleID,
payload.ParseUint64s(r.RecordIDs)...,
)
return api.OK(), ctrl.record.BulkModifyByFilter(ctx, f, nil, types.OperationTypeDelete)
}
func (ctrl *Record) Undelete(ctx context.Context, r *request.RecordUndelete) (interface{}, error) {
@@ -345,11 +342,16 @@ func (ctrl *Record) Undelete(ctx context.Context, r *request.RecordUndelete) (in
}
func (ctrl *Record) BulkUndelete(ctx context.Context, r *request.RecordBulkUndelete) (interface{}, error) {
return api.OK(), ctrl.record.UndeleteByID(ctx,
r.NamespaceID,
r.ModuleID,
payload.ParseUint64s(r.RecordIDs)...,
var (
f = types.RecordFilter{
Query: r.Query,
NamespaceID: r.NamespaceID,
ModuleID: r.ModuleID,
Deleted: filter.State(1),
}
)
return api.OK(), ctrl.record.BulkModifyByFilter(ctx, f, nil, types.OperationTypeUndelete)
}
func (ctrl *Record) Upload(ctx context.Context, r *request.RecordUpload) (interface{}, error) {
+65 -46
View File
@@ -336,15 +336,15 @@ type (
// Module ID
ModuleID uint64 `json:",string"`
// Records POST parameter
//
// Records to update
Records []string
// Values POST parameter
//
// Fields to update and their values
Values types.RecordValueSet
// Query POST parameter
//
// Search query for records to operate on
Query string
}
RecordBulkDelete struct {
@@ -358,15 +358,15 @@ type (
// Module ID
ModuleID uint64 `json:",string"`
// RecordIDs POST parameter
//
// IDs of records to delete
RecordIDs []string
// Truncate POST parameter
//
// Remove ALL records of a specified module (pending implementation)
Truncate bool
// Query POST parameter
//
// Search query for records to operate on
Query string
}
RecordDelete struct {
@@ -414,10 +414,10 @@ type (
// Module ID
ModuleID uint64 `json:",string"`
// RecordIDs POST parameter
// Query POST parameter
//
// IDs of records to undelete
RecordIDs []string
// Search query for records to operate on
Query string
}
RecordUpload struct {
@@ -1653,8 +1653,8 @@ func (r RecordPatch) Auditable() map[string]interface{} {
return map[string]interface{}{
"namespaceID": r.NamespaceID,
"moduleID": r.ModuleID,
"records": r.Records,
"values": r.Values,
"query": r.Query,
}
}
@@ -1669,13 +1669,13 @@ func (r RecordPatch) GetModuleID() uint64 {
}
// Auditable returns all auditable/loggable parameters
func (r RecordPatch) GetRecords() []string {
return r.Records
func (r RecordPatch) GetValues() types.RecordValueSet {
return r.Values
}
// Auditable returns all auditable/loggable parameters
func (r RecordPatch) GetValues() types.RecordValueSet {
return r.Values
func (r RecordPatch) GetQuery() string {
return r.Query
}
// Fill processes request and fills internal variables
@@ -1699,6 +1699,12 @@ func (r *RecordPatch) Fill(req *http.Request) (err error) {
} else if err == nil {
// Multipart params
if val, ok := req.MultipartForm.Value["query"]; ok && len(val) > 0 {
r.Query, err = val[0], nil
if err != nil {
return err
}
}
}
}
@@ -1709,19 +1715,19 @@ func (r *RecordPatch) Fill(req *http.Request) (err error) {
// POST params
//if val, ok := req.Form["records[]"]; ok && len(val) > 0 {
// r.Records, err = val, nil
// if err != nil {
// return err
// }
//}
//if val, ok := req.Form["values[]"]; ok && len(val) > 0 {
// r.Values, err = types.RecordValueSet(val), nil
// if err != nil {
// return err
// }
//}
if val, ok := req.Form["query"]; ok && len(val) > 0 {
r.Query, err = val[0], nil
if err != nil {
return err
}
}
}
{
@@ -1755,8 +1761,8 @@ func (r RecordBulkDelete) Auditable() map[string]interface{} {
return map[string]interface{}{
"namespaceID": r.NamespaceID,
"moduleID": r.ModuleID,
"recordIDs": r.RecordIDs,
"truncate": r.Truncate,
"query": r.Query,
}
}
@@ -1771,13 +1777,13 @@ func (r RecordBulkDelete) GetModuleID() uint64 {
}
// Auditable returns all auditable/loggable parameters
func (r RecordBulkDelete) GetRecordIDs() []string {
return r.RecordIDs
func (r RecordBulkDelete) GetTruncate() bool {
return r.Truncate
}
// Auditable returns all auditable/loggable parameters
func (r RecordBulkDelete) GetTruncate() bool {
return r.Truncate
func (r RecordBulkDelete) GetQuery() string {
return r.Query
}
// Fill processes request and fills internal variables
@@ -1807,6 +1813,13 @@ func (r *RecordBulkDelete) Fill(req *http.Request) (err error) {
return err
}
}
if val, ok := req.MultipartForm.Value["query"]; ok && len(val) > 0 {
r.Query, err = val[0], nil
if err != nil {
return err
}
}
}
}
@@ -1817,19 +1830,19 @@ func (r *RecordBulkDelete) Fill(req *http.Request) (err error) {
// POST params
//if val, ok := req.Form["recordIDs[]"]; ok && len(val) > 0 {
// r.RecordIDs, err = val, nil
// if err != nil {
// return err
// }
//}
if val, ok := req.Form["truncate"]; ok && len(val) > 0 {
r.Truncate, err = payload.ParseBool(val[0]), nil
if err != nil {
return err
}
}
if val, ok := req.Form["query"]; ok && len(val) > 0 {
r.Query, err = val[0], nil
if err != nil {
return err
}
}
}
{
@@ -1981,7 +1994,7 @@ func (r RecordBulkUndelete) Auditable() map[string]interface{} {
return map[string]interface{}{
"namespaceID": r.NamespaceID,
"moduleID": r.ModuleID,
"recordIDs": r.RecordIDs,
"query": r.Query,
}
}
@@ -1996,8 +2009,8 @@ func (r RecordBulkUndelete) GetModuleID() uint64 {
}
// Auditable returns all auditable/loggable parameters
func (r RecordBulkUndelete) GetRecordIDs() []string {
return r.RecordIDs
func (r RecordBulkUndelete) GetQuery() string {
return r.Query
}
// Fill processes request and fills internal variables
@@ -2021,6 +2034,12 @@ func (r *RecordBulkUndelete) Fill(req *http.Request) (err error) {
} else if err == nil {
// Multipart params
if val, ok := req.MultipartForm.Value["query"]; ok && len(val) > 0 {
r.Query, err = val[0], nil
if err != nil {
return err
}
}
}
}
@@ -2031,12 +2050,12 @@ func (r *RecordBulkUndelete) Fill(req *http.Request) (err error) {
// POST params
//if val, ok := req.Form["recordIDs[]"]; ok && len(val) > 0 {
// r.RecordIDs, err = val, nil
// if err != nil {
// return err
// }
//}
if val, ok := req.Form["query"]; ok && len(val) > 0 {
r.Query, err = val[0], nil
if err != nil {
return err
}
}
}
{
+106 -14
View File
@@ -126,6 +126,8 @@ type (
Update(ctx context.Context, record *types.Record) (*types.Record, *types.RecordValueErrorSet, error)
Bulk(ctx context.Context, skipFailed bool, oo ...*types.RecordBulkOperation) ([]types.RecordBulkOperationResult, error)
BulkModifyByFilter(ctx context.Context, f types.RecordFilter, values types.RecordValueSet, operation types.OperationType) (err error)
Validate(ctx context.Context, rec *types.Record) error
DeleteByID(ctx context.Context, namespaceID, moduleID uint64, recordID ...uint64) error
@@ -714,6 +716,82 @@ func (svc record) Bulk(ctx context.Context, skipFailed bool, oo ...*types.Record
}
}
// BulkModifyByFilter performs bulk record operations based on the provided filter query.
// It's able to update, delete or undelete records in a single transaction.
func (svc record) BulkModifyByFilter(ctx context.Context, f types.RecordFilter, values types.RecordValueSet, operation types.OperationType) (err error) {
var (
ns *types.Namespace
m *types.Module
r *types.Record
records types.RecordSet
recordFilter types.RecordFilter
aProps = &recordActionProps{
namespace: &types.Namespace{ID: f.NamespaceID},
module: &types.Module{ID: f.ModuleID},
}
action func(props ...*recordActionProps) *recordAction
valueError *types.RecordValueErrorSet
)
return store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) error {
// load both the namespace and module
if ns, m, err = loadModuleCombo(ctx, s, f.NamespaceID, f.ModuleID); err != nil {
return err
}
aProps.setNamespace(ns)
aProps.setModule(m)
f.Limit = 500
// performing a batched search for IDs, processing them in batches of 500 for update.
for {
records, recordFilter, err = svc.Find(ctx, f)
if err != nil {
return err
}
for _, r = range records {
aProps.setRecord(r)
switch operation {
case types.OperationTypePatch:
action = RecordActionPatch
r, valueError, err = svc.patch(ctx, r, values)
case types.OperationTypeDelete:
action = RecordActionDelete
r, err = svc.processDelete(ctx, r, ns, m)
case types.OperationTypeUndelete:
action = RecordActionUndelete
r, err = svc.processUndelete(ctx, r, ns, m)
}
aProps.setChanged(r)
if valueError != nil && !valueError.IsValid() {
return RecordErrValueInput().Wrap(valueError)
}
_ = svc.recordAction(ctx, aProps, action, err)
if err != nil {
return err
}
}
if recordFilter.NextPage == nil {
break
}
f.NextPage = recordFilter.NextPage
}
return nil
})
}
// Raw create function that is responsible for value validation, event dispatching
// and creation.
func (svc record) create(ctx context.Context, new *types.Record) (rec *types.Record, dd *types.RecordValueErrorSet, err error) {
@@ -1316,8 +1394,6 @@ func (svc record) delete(ctx context.Context, namespaceID, moduleID, recordID ui
var (
ns *types.Namespace
m *types.Module
invokerID = auth.GetIdentityFromContext(ctx).Identity()
)
if namespaceID == 0 {
@@ -1335,6 +1411,14 @@ func (svc record) delete(ctx context.Context, namespaceID, moduleID, recordID ui
return nil, err
}
return svc.processDelete(ctx, del, ns, m)
}
func (svc record) processDelete(ctx context.Context, del *types.Record, namespace *types.Namespace, module *types.Module) (record *types.Record, err error) {
var (
invokerID = auth.GetIdentityFromContext(ctx).Identity()
)
if !svc.ac.CanDeleteRecord(ctx, del) {
return nil, RecordErrNotAllowedToDelete()
}
@@ -1343,7 +1427,7 @@ func (svc record) delete(ctx context.Context, namespaceID, moduleID, recordID ui
del.DeletedBy = invokerID
// ensure module ref is set before running through records workflows and scripts
del.SetModule(m)
del.SetModule(module)
// deleted, revision need to be set when RecordBeforeDelete is triggered
del.DeletedAt = nowUTC()
@@ -1352,27 +1436,27 @@ func (svc record) delete(ctx context.Context, namespaceID, moduleID, recordID ui
{
// Calling before-record-delete scripts
if err = svc.eventbus.WaitFor(ctx, event.RecordBeforeDelete(nil, del, m, ns, nil, nil)); err != nil {
if err = svc.eventbus.WaitFor(ctx, event.RecordBeforeDelete(nil, del, module, namespace, nil, nil)); err != nil {
return nil, err
}
}
if m.Config.RecordRevisions.Enabled {
if module.Config.RecordRevisions.Enabled {
// Prepare record revision for update
if err = svc.revisions.softDeleted(ctx, del); err != nil {
return
}
}
if err = dalutils.ComposeRecordSoftDelete(ctx, svc.dal, m, del); err != nil {
if err = dalutils.ComposeRecordSoftDelete(ctx, svc.dal, module, del); err != nil {
return nil, err
}
// ensure module ref is set before running through records workflows and scripts
del.SetModule(m)
del.SetModule(module)
{
_ = svc.eventbus.WaitFor(ctx, event.RecordAfterDeleteImmutable(nil, del, m, ns, nil, nil))
_ = svc.eventbus.WaitFor(ctx, event.RecordAfterDeleteImmutable(nil, del, module, namespace, nil, nil))
}
return del, nil
@@ -1389,6 +1473,14 @@ func (svc record) undelete(ctx context.Context, namespaceID, moduleID, recordID
return nil, err
}
return svc.processUndelete(ctx, undel, ns, m)
}
func (svc record) processUndelete(ctx context.Context, undel *types.Record, namespace *types.Namespace, module *types.Module) (record *types.Record, err error) {
if err != nil {
return nil, err
}
if !svc.ac.CanUndeleteRecord(ctx, undel) {
return nil, RecordErrNotAllowedToUndelete()
}
@@ -1397,7 +1489,7 @@ func (svc record) undelete(ctx context.Context, namespaceID, moduleID, recordID
undel.DeletedBy = 0
// ensure module ref is set before running through records workflows and scripts
undel.SetModule(m)
undel.SetModule(module)
undel.DeletedAt = nil
undel.DeletedBy = 0
@@ -1405,27 +1497,27 @@ func (svc record) undelete(ctx context.Context, namespaceID, moduleID, recordID
{
// Calling before-record-undelete scripts
if err = svc.eventbus.WaitFor(ctx, event.RecordBeforeUndelete(nil, undel, m, ns, nil, nil)); err != nil {
if err = svc.eventbus.WaitFor(ctx, event.RecordBeforeUndelete(nil, undel, module, namespace, nil, nil)); err != nil {
return nil, err
}
}
if m.Config.RecordRevisions.Enabled {
if module.Config.RecordRevisions.Enabled {
// Prepare record revision for update
if err = svc.revisions.undeleted(ctx, undel); err != nil {
return
}
}
if err = dalutils.ComposeRecordUndelete(ctx, svc.dal, m, undel); err != nil {
if err = dalutils.ComposeRecordUndelete(ctx, svc.dal, module, undel); err != nil {
return nil, err
}
// ensure module ref is set before running through records workflows and scripts
undel.SetModule(m)
undel.SetModule(module)
{
_ = svc.eventbus.WaitFor(ctx, event.RecordAfterUndeleteImmutable(nil, undel, m, ns, nil, nil))
_ = svc.eventbus.WaitFor(ctx, event.RecordAfterUndeleteImmutable(nil, undel, module, namespace, nil, nil))
}
return undel, nil
+5 -4
View File
@@ -101,10 +101,11 @@ type (
)
const (
OperationTypeCreate OperationType = "create"
OperationTypeUpdate OperationType = "update"
OperationTypeDelete OperationType = "delete"
OperationTypePatch OperationType = "patch"
OperationTypeCreate OperationType = "create"
OperationTypeUpdate OperationType = "update"
OperationTypeDelete OperationType = "delete"
OperationTypePatch OperationType = "patch"
OperationTypeUndelete OperationType = "undelete"
)
func (f RecordFilter) ToConstraintedFilter(c map[string][]any) filter.Filter {
+2 -2
View File
@@ -12,8 +12,8 @@ type (
)
func HasUint64(ss []uint64, s uint64) bool {
for i := range ss {
if ss[i] == s {
for _, value := range ss {
if value == s {
return true
}
}
+2 -1
View File
@@ -35,7 +35,8 @@ func TestHasUint64(t *testing.T) {
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
assert.EqualValues(t, HasUint64(c.ss, c.s), c.o)
ok := HasUint64(c.ss, c.s)
assert.EqualValues(t, ok, c.o)
})
}
}