Add support for viewing and undeleting soft-deleted records
This commit is contained in:
committed by
Mumbi Francis
parent
8cbae6ef29
commit
ad46c275f9
@@ -32,7 +32,7 @@
|
||||
class="d-flex wrap-with-vertical-gutters align-items-center ml-auto"
|
||||
>
|
||||
<c-input-confirm
|
||||
v-if="isCreated && !settings.hideDelete"
|
||||
v-if="(isCreated && !settings.hideDelete && !isDeleted)"
|
||||
:disabled="!canDeleteRecord"
|
||||
size="lg"
|
||||
size-confirm="lg"
|
||||
@@ -51,6 +51,27 @@
|
||||
</span>
|
||||
</c-input-confirm>
|
||||
|
||||
<c-input-confirm
|
||||
v-if="isDeleted"
|
||||
:disabled="!canUndeleteRecord"
|
||||
size="lg"
|
||||
size-confirm="lg"
|
||||
variant="warning"
|
||||
variant-ok="warning"
|
||||
:borderless="false"
|
||||
@confirmed="$emit('undelete')"
|
||||
>
|
||||
<b-spinner
|
||||
v-if="processingUndelete"
|
||||
small
|
||||
type="grow"
|
||||
/>
|
||||
|
||||
<span v-else>
|
||||
{{ $t('label.restore') }}
|
||||
</span>
|
||||
</c-input-confirm>
|
||||
|
||||
<b-button
|
||||
v-if="!inEditing && module.canCreateRecord && !hideClone && isCreated && !settings.hideClone"
|
||||
data-test-id="button-clone"
|
||||
@@ -150,6 +171,11 @@ export default {
|
||||
default: false,
|
||||
},
|
||||
|
||||
processingUndelete: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
processingSubmit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -207,6 +233,14 @@ export default {
|
||||
|
||||
return !this.isDeleted && this.record.canDeleteRecord && !this.processing && this.record.recordID !== NoID
|
||||
},
|
||||
|
||||
canUndeleteRecord () {
|
||||
if (!this.module || !this.record) {
|
||||
return false
|
||||
}
|
||||
|
||||
return this.isDeleted && this.record.canUndeleteRecord && !this.processing && this.record.recordID !== NoID
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -125,17 +125,19 @@
|
||||
v-bind="$props"
|
||||
@refresh="refresh()"
|
||||
/>
|
||||
<span v-if="canDeleteSelectedRecords">
|
||||
<template v-if="canDeleteSelectedRecords && !areAllRowsDeleted">
|
||||
<c-input-confirm
|
||||
v-if="!inlineEditing"
|
||||
:tooltip="$t('recordList.tooltip.deleteSelected')"
|
||||
class="ml-2"
|
||||
variant="link-light"
|
||||
@confirmed="handleDeleteSelectedRecords()"
|
||||
/>
|
||||
<b-button
|
||||
v-else-if="!areAllRowsDeleted"
|
||||
v-else
|
||||
variant="link"
|
||||
size="md"
|
||||
:title="$t('recordList.tooltip.deleteSelected')"
|
||||
class="text-danger"
|
||||
@click.prevent="handleDeleteSelectedRecords()"
|
||||
>
|
||||
<font-awesome-icon
|
||||
@@ -143,10 +145,24 @@
|
||||
:icon="['far', 'trash-alt']"
|
||||
/>
|
||||
</b-button>
|
||||
</template>
|
||||
|
||||
<template v-if="canUndeleteSelectedRecords && areAllRowsDeleted">
|
||||
<c-input-confirm
|
||||
v-if="!inlineEditing"
|
||||
:tooltip="$t('recordList.tooltip.undeleteSelected')"
|
||||
class="ml-2"
|
||||
@confirmed="handleRestoreSelectedRecords()"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fa', 'trash-restore']"
|
||||
/>
|
||||
</c-input-confirm>
|
||||
<b-button
|
||||
v-else
|
||||
variant="link"
|
||||
size="md"
|
||||
:title="$t('recordList.tooltip.undeleteSelected')"
|
||||
class="text-danger"
|
||||
@click.prevent="handleRestoreSelectedRecords()"
|
||||
>
|
||||
@@ -154,7 +170,7 @@
|
||||
:icon="['fa', 'trash-restore']"
|
||||
/>
|
||||
</b-button>
|
||||
</span>
|
||||
</template>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-container>
|
||||
@@ -173,7 +189,7 @@
|
||||
class="border-top mh-100 h-100 mb-0"
|
||||
>
|
||||
<b-thead>
|
||||
<b-tr>
|
||||
<b-tr :variant="showingDeletedRecords ? 'warning' : ''">
|
||||
<b-th v-if="options.draggable && inlineEditing" />
|
||||
<b-th
|
||||
v-if="options.selectable"
|
||||
@@ -267,7 +283,6 @@
|
||||
<b-tr
|
||||
v-for="(item, index) in items"
|
||||
:key="`${index}${item.r.recordID}`"
|
||||
:variant="!!item.r.deletedAt ? 'danger' : undefined"
|
||||
:class="{ 'pointer': !(options.editable && editing) }"
|
||||
@click="handleRowClicked(item)"
|
||||
>
|
||||
@@ -482,6 +497,7 @@
|
||||
ref="footer"
|
||||
fluid
|
||||
class="m-0 p-2"
|
||||
:class="showingDeletedRecords ? 'bg-warning' : ''"
|
||||
>
|
||||
<b-row no-gutters>
|
||||
<b-col class="d-flex justify-content-between align-items-center">
|
||||
@@ -504,6 +520,7 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!options.hidePaging && !inlineEditing"
|
||||
>
|
||||
@@ -569,6 +586,16 @@
|
||||
</b-button>
|
||||
</b-button-group>
|
||||
</div>
|
||||
|
||||
<div v-if="options.showDeletedRecordsOption">
|
||||
<b-button
|
||||
variant="light"
|
||||
class="mx-2 text-nowrap"
|
||||
@click="handleShowDeleted()"
|
||||
>
|
||||
{{ showingDeletedRecords ? $t('recordList.showRecords.existing') : $t('recordList.showRecords.deleted') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-container>
|
||||
@@ -665,6 +692,7 @@ export default {
|
||||
items: [],
|
||||
idPrefix: `rl:${this.blockIndex}`,
|
||||
recordListFilter: [],
|
||||
showingDeletedRecords: false,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -807,6 +835,10 @@ export default {
|
||||
return this.items.filter(({ id, r }) => this.selected.includes(id) && r.canDeleteRecord).length
|
||||
},
|
||||
|
||||
canUndeleteSelectedRecords () {
|
||||
return this.items.filter(({ id, r }) => this.selected.includes(id) && r.canUndeleteRecord).length
|
||||
},
|
||||
|
||||
newRecordRoute () {
|
||||
const refRecord = this.options.linkToParent ? this.record : undefined
|
||||
const pageID = this.recordPageID
|
||||
@@ -921,6 +953,11 @@ export default {
|
||||
return isSorted ? { color: 'black' } : {}
|
||||
},
|
||||
|
||||
handleShowDeleted () {
|
||||
this.showingDeletedRecords = !this.showingDeletedRecords
|
||||
this.refresh(true)
|
||||
},
|
||||
|
||||
// Grabs errors specific to this record item
|
||||
recordErrors (item, field) {
|
||||
if (field) {
|
||||
@@ -939,7 +976,6 @@ export default {
|
||||
return {
|
||||
r,
|
||||
id: id || (r.recordID !== NoID ? r.recordID : `${this.idPrefix}:${this.ctr++}`),
|
||||
_rowVariant: r.deletedAt ? 'danger' : undefined,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1204,11 +1240,33 @@ export default {
|
||||
},
|
||||
|
||||
handleRestoreSelectedRecords () {
|
||||
const sel = new Set(this.selected)
|
||||
for (let i = 0; i < this.items.length; i++) {
|
||||
if (sel.has(this.items[i].id)) {
|
||||
this.handleRestoreInline(this.items[i], i)
|
||||
}
|
||||
if (this.inlineEditing) {
|
||||
const sel = new Set(this.selected)
|
||||
this.items.forEach((item, index) => {
|
||||
if (sel.has(item.id)) {
|
||||
this.handleRestoreInline(item, index)
|
||||
}
|
||||
})
|
||||
} 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)
|
||||
|
||||
this.processing = true
|
||||
|
||||
this.$ComposeAPI
|
||||
.recordBulkUndelete({ moduleID, namespaceID, recordIDs })
|
||||
.then(() => {
|
||||
this.refresh(true)
|
||||
this.toastSuccess(this.$t('notification:record.undeleteBulkSuccess'))
|
||||
})
|
||||
.catch(this.toastErrorHandler(this.$t('notification:record.undeleteBulkFailed')))
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1291,6 +1349,9 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
// Filter's out deleted records when filter.deleted is 2, and undeleted records when filter.deleted is 0
|
||||
this.showingDeletedRecords ? this.filter.deleted = 2 : this.filter.deleted = 0
|
||||
|
||||
await this.$ComposeAPI.recordList({ ...this.filter, moduleID, namespaceID, query, ...paginationOptions })
|
||||
.then(({ set, filter }) => {
|
||||
const records = set.map(r => new compose.Record(r, this.recordListModule))
|
||||
|
||||
@@ -241,6 +241,9 @@
|
||||
>
|
||||
{{ $t('recordList.record.showTotalCount') }}
|
||||
</b-form-checkbox>
|
||||
<b-form-checkbox v-model="options.showDeletedRecordsOption">
|
||||
{{ $t('recordList.record.showDeletedRecordsOption') }}
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
#cell(adt)="row"
|
||||
>
|
||||
<b-button
|
||||
v-if="row.item.changes.length > 0"
|
||||
v-if="row.item.changes && row.item.changes.length > 0"
|
||||
variant="link"
|
||||
class="py-0 m-0"
|
||||
@click="row.toggleDetails"
|
||||
|
||||
@@ -10,6 +10,7 @@ export default {
|
||||
inEditing: false,
|
||||
processing: false,
|
||||
processingDelete: false,
|
||||
processingUndelete: false,
|
||||
processingSubmit: false,
|
||||
record: undefined,
|
||||
errors: new validator.Validated(),
|
||||
@@ -238,6 +239,7 @@ export default {
|
||||
})
|
||||
.then(() => this.dispatchUiEvent('afterDelete'))
|
||||
.then(() => this.updatePrompts())
|
||||
.then(this.loadRecord)
|
||||
.catch(this.toastErrorHandler(this.$t('notification:record.deleteFailed')))
|
||||
.finally(() => {
|
||||
this.processingDelete = false
|
||||
@@ -245,6 +247,23 @@ export default {
|
||||
})
|
||||
}, 500),
|
||||
|
||||
handleUndelete: throttle(function () {
|
||||
this.processingUndelete = true
|
||||
this.processing = true
|
||||
|
||||
return this
|
||||
.dispatchUiEvent('beforeUndelete')
|
||||
.then(() => this.$ComposeAPI.recordUndelete(this.record))
|
||||
.then(() => this.dispatchUiEvent('afterUndelete'))
|
||||
.then(() => this.updatePrompts())
|
||||
.then(this.loadRecord)
|
||||
.catch(this.toastErrorHandler(this.$t('notification:record.undeleteFailed')))
|
||||
.finally(() => {
|
||||
this.processingUndelete = false
|
||||
this.processing = false
|
||||
})
|
||||
}, 500),
|
||||
|
||||
/**
|
||||
* Validates record and dispatches onFormSubmitError
|
||||
*
|
||||
|
||||
@@ -8,6 +8,8 @@ const recordPageEventTypes = [
|
||||
'afterFormSubmit',
|
||||
'beforeDelete',
|
||||
'afterDelete',
|
||||
'beforeUndelete',
|
||||
'afterUndelete',
|
||||
]
|
||||
|
||||
export default {
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
:processing="processing"
|
||||
:processing-submit="processingSubmit"
|
||||
:processing-delete="processingDelete"
|
||||
:processing-undelete="processingUndelete"
|
||||
:is-deleted="isDeleted"
|
||||
:in-editing="inEditing"
|
||||
:hide-clone="inCreating"
|
||||
@@ -44,6 +45,7 @@
|
||||
@clone="handleClone()"
|
||||
@edit="handleEdit()"
|
||||
@delete="handleDelete()"
|
||||
@undelete="handleUndelete()"
|
||||
@back="handleBack()"
|
||||
@submit="handleFormSubmit('page.record')"
|
||||
/>
|
||||
@@ -69,7 +71,7 @@ export default {
|
||||
},
|
||||
|
||||
mixins: [
|
||||
// The record mixin contains all of the logic for creating/editing/deleting the record
|
||||
// The record mixin contains all of the logic for creating/editing/deleting/undeleting the record
|
||||
record,
|
||||
],
|
||||
|
||||
|
||||
@@ -1727,6 +1727,76 @@ export default class Compose {
|
||||
return `/namespace/${namespaceID}/module/${moduleID}/record/${recordID}`
|
||||
}
|
||||
|
||||
// Undelete soft-deleted record from module section
|
||||
async recordUndelete (a: KV, extra: AxiosRequestConfig = {}): Promise<KV> {
|
||||
const {
|
||||
namespaceID,
|
||||
moduleID,
|
||||
recordID,
|
||||
} = (a as KV) || {}
|
||||
if (!namespaceID) {
|
||||
throw Error('field namespaceID is empty')
|
||||
}
|
||||
if (!moduleID) {
|
||||
throw Error('field moduleID is empty')
|
||||
}
|
||||
if (!recordID) {
|
||||
throw Error('field recordID is empty')
|
||||
}
|
||||
const cfg: AxiosRequestConfig = {
|
||||
...extra,
|
||||
method: 'post',
|
||||
url: this.recordUndeleteEndpoint({
|
||||
namespaceID, moduleID, recordID,
|
||||
}),
|
||||
}
|
||||
|
||||
return this.api().request(cfg).then(result => stdResolve(result))
|
||||
}
|
||||
|
||||
recordUndeleteEndpoint (a: KV): string {
|
||||
const {
|
||||
namespaceID,
|
||||
moduleID,
|
||||
recordID,
|
||||
} = a || {}
|
||||
return `/namespace/${namespaceID}/module/${moduleID}/record/${recordID}/undelete`
|
||||
}
|
||||
|
||||
// Undelete soft-deleted records from module section
|
||||
async recordBulkUndelete (a: KV, extra: AxiosRequestConfig = {}): Promise<KV> {
|
||||
const {
|
||||
namespaceID,
|
||||
moduleID,
|
||||
recordIDs,
|
||||
} = (a as KV) || {}
|
||||
if (!namespaceID) {
|
||||
throw Error('field namespaceID is empty')
|
||||
}
|
||||
if (!moduleID) {
|
||||
throw Error('field moduleID is empty')
|
||||
}
|
||||
const cfg: AxiosRequestConfig = {
|
||||
...extra,
|
||||
method: 'patch',
|
||||
url: this.recordBulkUndeleteEndpoint({
|
||||
namespaceID, moduleID,
|
||||
}),
|
||||
}
|
||||
cfg.data = {
|
||||
recordIDs,
|
||||
}
|
||||
return this.api().request(cfg).then(result => stdResolve(result))
|
||||
}
|
||||
|
||||
recordBulkUndeleteEndpoint (a: KV): string {
|
||||
const {
|
||||
namespaceID,
|
||||
moduleID,
|
||||
} = a || {}
|
||||
return `/namespace/${namespaceID}/module/${moduleID}/record/undelete`
|
||||
}
|
||||
|
||||
// Uploads attachment and validates it against record field requirements
|
||||
async recordUpload (a: KV, extra: AxiosRequestConfig = {}): Promise<KV> {
|
||||
const {
|
||||
|
||||
@@ -28,6 +28,7 @@ interface Options {
|
||||
|
||||
fullPageNavigation: boolean;
|
||||
showTotalCount: boolean;
|
||||
showDeletedRecordsOption: boolean;
|
||||
refreshRate: number;
|
||||
refreshEnabled: boolean;
|
||||
|
||||
@@ -76,6 +77,7 @@ const defaults: Readonly<Options> = Object.freeze({
|
||||
|
||||
fullPageNavigation: true,
|
||||
showTotalCount: true,
|
||||
showDeletedRecordsOption: false,
|
||||
|
||||
editable: false,
|
||||
draggable: false,
|
||||
@@ -132,6 +134,7 @@ export class PageBlockRecordList extends PageBlock {
|
||||
'hidePaging',
|
||||
'fullPageNavigation',
|
||||
'showTotalCount',
|
||||
'showDeletedRecordsOption',
|
||||
'hideSorting',
|
||||
'allowExport',
|
||||
'selectable',
|
||||
|
||||
@@ -84,6 +84,7 @@ export class Record {
|
||||
public canUpdateRecord = false;
|
||||
public canReadRecord = false;
|
||||
public canDeleteRecord = false;
|
||||
public canUndeleteRecord = false;
|
||||
public canManageOwnerOnRecord = false;
|
||||
public canSearchRevision = false;
|
||||
public canGrant = false;
|
||||
@@ -174,6 +175,7 @@ export class Record {
|
||||
'canUpdateRecord',
|
||||
'canReadRecord',
|
||||
'canDeleteRecord',
|
||||
'canUndeleteRecord',
|
||||
'canManageOwnerOnRecord',
|
||||
'canGrant',
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
:variant="variant"
|
||||
:size="size"
|
||||
:disabled="disabled"
|
||||
:title="tooltip"
|
||||
:class="`${buttonClass} ${borderless ? 'border-0' : ''}`"
|
||||
@click.stop.prevent="onPrompt"
|
||||
>
|
||||
@@ -86,6 +87,10 @@ export default {
|
||||
type: String,
|
||||
default: 'sm',
|
||||
},
|
||||
tooltip: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
|
||||
@@ -219,6 +219,9 @@ record:
|
||||
recordList:
|
||||
addRecord: Add
|
||||
cancelSelection: Cancel
|
||||
tooltip:
|
||||
deleteSelected: Delete selected records
|
||||
undeleteSelected: Undelete selected records
|
||||
editFields: Editable module fields
|
||||
export:
|
||||
all: Export all records
|
||||
@@ -371,6 +374,7 @@ recordList:
|
||||
presortLabel: Presort records
|
||||
presortPlaceholder: field1 DESC, field2 ASC
|
||||
showTotalCount: Show total record count
|
||||
showDeletedRecordsOption: Show option to see deleted records
|
||||
openInSameTab: Open records in the same tab
|
||||
openInNewTab: Open records in a new tab
|
||||
openInModal: Open records in a modal
|
||||
@@ -390,6 +394,9 @@ recordList:
|
||||
selected: '{{count}} of {{total}} records selected'
|
||||
sort:
|
||||
tooltip: Sort column
|
||||
showRecords:
|
||||
deleted: Show deleted records
|
||||
existing: Show existing records
|
||||
recordOrganizer:
|
||||
descriptionField:
|
||||
footnote: Field value will be used as record description
|
||||
|
||||
@@ -22,6 +22,7 @@ label:
|
||||
close: Close
|
||||
create: Create
|
||||
delete: Delete
|
||||
restore: Restore
|
||||
descending: Descending
|
||||
description: Description
|
||||
download: Download
|
||||
|
||||
@@ -87,8 +87,11 @@ page:
|
||||
record:
|
||||
createFailed: Could not create record
|
||||
deleteFailed: Could not delete record
|
||||
undeleteFailed: Could not undelete record
|
||||
deleteBulkFailed: Could not delete selected records
|
||||
deleteBulkSuccess: Successfully deleted selected records
|
||||
undeleteBulkFailed: Could not restore selected records
|
||||
undeleteBulkSuccess: Successfully restored selected records
|
||||
invalidOwnerVar: Can not use ${ownerID} variable in non-record pages
|
||||
invalidRecordVar: Can not use ${record...} variable in non-record pages
|
||||
listLoadFailed: Could not load record list
|
||||
|
||||
Generated
+154
@@ -1261,6 +1261,83 @@ func getEventTypeDefinitions() []eventTypeDef {
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
ResourceType: "compose:record",
|
||||
EventType: "beforeUndelete",
|
||||
Properties: []eventTypePropertyDef{
|
||||
|
||||
{
|
||||
Name: "record",
|
||||
Type: "ComposeRecord",
|
||||
Immutable: false,
|
||||
},
|
||||
|
||||
{
|
||||
Name: "oldRecord",
|
||||
Type: "ComposeRecord",
|
||||
Immutable: true,
|
||||
},
|
||||
|
||||
{
|
||||
Name: "module",
|
||||
Type: "ComposeModule",
|
||||
Immutable: true,
|
||||
},
|
||||
|
||||
{
|
||||
Name: "namespace",
|
||||
Type: "ComposeNamespace",
|
||||
Immutable: true,
|
||||
},
|
||||
|
||||
{
|
||||
Name: "recordValueErrors",
|
||||
Type: "ComposeRecordValueErrorSet",
|
||||
Immutable: false,
|
||||
},
|
||||
|
||||
{
|
||||
Name: "selected",
|
||||
Type: "",
|
||||
Immutable: true,
|
||||
},
|
||||
},
|
||||
Constraints: []eventTypeConstraintDef{
|
||||
|
||||
{
|
||||
Name: "namespace.handle",
|
||||
},
|
||||
|
||||
{
|
||||
Name: "namespace.name",
|
||||
},
|
||||
|
||||
{
|
||||
Name: "module.handle",
|
||||
},
|
||||
|
||||
{
|
||||
Name: "module.name",
|
||||
},
|
||||
|
||||
{
|
||||
Name: "record.created-at",
|
||||
},
|
||||
|
||||
{
|
||||
Name: "record.updated-at",
|
||||
},
|
||||
|
||||
{
|
||||
Name: "record.deleted-at",
|
||||
},
|
||||
|
||||
{
|
||||
Name: "record.values.*",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
ResourceType: "compose:record",
|
||||
EventType: "afterCreate",
|
||||
@@ -1492,6 +1569,83 @@ func getEventTypeDefinitions() []eventTypeDef {
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
ResourceType: "compose:record",
|
||||
EventType: "afterUndelete",
|
||||
Properties: []eventTypePropertyDef{
|
||||
|
||||
{
|
||||
Name: "record",
|
||||
Type: "ComposeRecord",
|
||||
Immutable: false,
|
||||
},
|
||||
|
||||
{
|
||||
Name: "oldRecord",
|
||||
Type: "ComposeRecord",
|
||||
Immutable: true,
|
||||
},
|
||||
|
||||
{
|
||||
Name: "module",
|
||||
Type: "ComposeModule",
|
||||
Immutable: true,
|
||||
},
|
||||
|
||||
{
|
||||
Name: "namespace",
|
||||
Type: "ComposeNamespace",
|
||||
Immutable: true,
|
||||
},
|
||||
|
||||
{
|
||||
Name: "recordValueErrors",
|
||||
Type: "ComposeRecordValueErrorSet",
|
||||
Immutable: false,
|
||||
},
|
||||
|
||||
{
|
||||
Name: "selected",
|
||||
Type: "",
|
||||
Immutable: true,
|
||||
},
|
||||
},
|
||||
Constraints: []eventTypeConstraintDef{
|
||||
|
||||
{
|
||||
Name: "namespace.handle",
|
||||
},
|
||||
|
||||
{
|
||||
Name: "namespace.name",
|
||||
},
|
||||
|
||||
{
|
||||
Name: "module.handle",
|
||||
},
|
||||
|
||||
{
|
||||
Name: "module.name",
|
||||
},
|
||||
|
||||
{
|
||||
Name: "record.created-at",
|
||||
},
|
||||
|
||||
{
|
||||
Name: "record.updated-at",
|
||||
},
|
||||
|
||||
{
|
||||
Name: "record.deleted-at",
|
||||
},
|
||||
|
||||
{
|
||||
Name: "record.values.*",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
ResourceType: "system",
|
||||
EventType: "onManual",
|
||||
|
||||
@@ -76,6 +76,10 @@ func ComposeRecordSoftDelete(ctx context.Context, u updater, mod *types.Module,
|
||||
return u.Update(ctx, mod.ModelRef(), recUpdateOperations(mod), recToGetters(records...)...)
|
||||
}
|
||||
|
||||
func ComposeRecordUndelete(ctx context.Context, u updater, mod *types.Module, records ...*types.Record) (err error) {
|
||||
return u.Update(ctx, mod.ModelRef(), recUpdateOperations(mod), recToGetters(records...)...)
|
||||
}
|
||||
|
||||
func ComposeRecordDelete(ctx context.Context, d deleter, mod *types.Module, records ...*types.Record) (err error) {
|
||||
return d.Delete(ctx, mod.ModelRef(), recDeleteOperations(mod), recToGetters(records...)...)
|
||||
}
|
||||
|
||||
@@ -78,6 +78,7 @@ record: {
|
||||
"read": {}
|
||||
"update": {}
|
||||
"delete": {}
|
||||
"undelete": {}
|
||||
"owner.manage": {}
|
||||
"revisions.search": {}
|
||||
}
|
||||
|
||||
@@ -828,6 +828,26 @@ endpoints:
|
||||
name: recordID
|
||||
required: true
|
||||
title: Record ID
|
||||
- name: undelete
|
||||
method: POST
|
||||
title: Undelete soft-deleted record from module section
|
||||
path: "/{recordID}/undelete"
|
||||
parameters:
|
||||
path:
|
||||
- type: uint64
|
||||
name: recordID
|
||||
required: true
|
||||
title: Record ID
|
||||
- name: bulkUndelete
|
||||
method: PATCH
|
||||
title: Undelete soft-deleted records from module section
|
||||
path: "/undelete"
|
||||
parameters:
|
||||
post:
|
||||
- type: "[]string"
|
||||
name: recordIDs
|
||||
required: false
|
||||
title: IDs of records to undelete
|
||||
- name: upload
|
||||
path: "/attachment"
|
||||
method: POST
|
||||
|
||||
@@ -31,6 +31,8 @@ type (
|
||||
Update(context.Context, *request.RecordUpdate) (interface{}, error)
|
||||
BulkDelete(context.Context, *request.RecordBulkDelete) (interface{}, error)
|
||||
Delete(context.Context, *request.RecordDelete) (interface{}, error)
|
||||
Undelete(context.Context, *request.RecordUndelete) (interface{}, error)
|
||||
BulkUndelete(context.Context, *request.RecordBulkUndelete) (interface{}, error)
|
||||
Upload(context.Context, *request.RecordUpload) (interface{}, error)
|
||||
TriggerScript(context.Context, *request.RecordTriggerScript) (interface{}, error)
|
||||
TriggerScriptOnList(context.Context, *request.RecordTriggerScriptOnList) (interface{}, error)
|
||||
@@ -51,6 +53,8 @@ type (
|
||||
Update func(http.ResponseWriter, *http.Request)
|
||||
BulkDelete func(http.ResponseWriter, *http.Request)
|
||||
Delete func(http.ResponseWriter, *http.Request)
|
||||
Undelete func(http.ResponseWriter, *http.Request)
|
||||
BulkUndelete func(http.ResponseWriter, *http.Request)
|
||||
Upload func(http.ResponseWriter, *http.Request)
|
||||
TriggerScript func(http.ResponseWriter, *http.Request)
|
||||
TriggerScriptOnList func(http.ResponseWriter, *http.Request)
|
||||
@@ -252,6 +256,38 @@ func NewRecord(h RecordAPI) *Record {
|
||||
|
||||
api.Send(w, r, value)
|
||||
},
|
||||
Undelete: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewRecordUndelete()
|
||||
if err := params.Fill(r); err != nil {
|
||||
api.Send(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
value, err := h.Undelete(r.Context(), params)
|
||||
if err != nil {
|
||||
api.Send(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
api.Send(w, r, value)
|
||||
},
|
||||
BulkUndelete: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewRecordBulkUndelete()
|
||||
if err := params.Fill(r); err != nil {
|
||||
api.Send(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
value, err := h.BulkUndelete(r.Context(), params)
|
||||
if err != nil {
|
||||
api.Send(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
api.Send(w, r, value)
|
||||
},
|
||||
Upload: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewRecordUpload()
|
||||
@@ -334,6 +370,8 @@ func (h Record) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http
|
||||
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/{recordID}/undelete", h.Undelete)
|
||||
r.Patch("/namespace/{namespaceID}/module/{moduleID}/record/undelete", h.BulkUndelete)
|
||||
r.Post("/namespace/{namespaceID}/module/{moduleID}/record/attachment", h.Upload)
|
||||
r.Post("/namespace/{namespaceID}/module/{moduleID}/record/{recordID}/trigger", h.TriggerScript)
|
||||
r.Post("/namespace/{namespaceID}/module/{moduleID}/record/trigger", h.TriggerScriptOnList)
|
||||
|
||||
@@ -40,6 +40,7 @@ type (
|
||||
CanUpdateRecord bool `json:"canUpdateRecord"`
|
||||
CanReadRecord bool `json:"canReadRecord"`
|
||||
CanDeleteRecord bool `json:"canDeleteRecord"`
|
||||
CanUndeleteRecord bool `json:"canUndeleteRecord"`
|
||||
CanSearchRevisions bool `json:"canSearchRevisions"`
|
||||
|
||||
CanGrant bool `json:"canGrant"`
|
||||
@@ -65,6 +66,7 @@ type (
|
||||
CanUpdateRecord(context.Context, *types.Record) bool
|
||||
CanReadRecord(context.Context, *types.Record) bool
|
||||
CanDeleteRecord(context.Context, *types.Record) bool
|
||||
CanUndeleteRecord(context.Context, *types.Record) bool
|
||||
CanManageOwnerOnRecord(context.Context, *types.Record) bool
|
||||
CanSearchRevisionsOnRecord(context.Context, *types.Record) bool
|
||||
}
|
||||
@@ -277,6 +279,18 @@ func (ctrl *Record) BulkDelete(ctx context.Context, r *request.RecordBulkDelete)
|
||||
)
|
||||
}
|
||||
|
||||
func (ctrl *Record) Undelete(ctx context.Context, r *request.RecordUndelete) (interface{}, error) {
|
||||
return api.OK(), ctrl.record.UndeleteByID(ctx, r.NamespaceID, r.ModuleID, r.RecordID)
|
||||
}
|
||||
|
||||
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)...,
|
||||
)
|
||||
}
|
||||
|
||||
func (ctrl *Record) Upload(ctx context.Context, r *request.RecordUpload) (interface{}, error) {
|
||||
file, err := r.Upload.Open()
|
||||
if err != nil {
|
||||
@@ -618,6 +632,7 @@ func (ctrl Record) makeBulkPayload(ctx context.Context, m *types.Module, dd *typ
|
||||
CanUpdateRecord: ctrl.ac.CanUpdateRecord(ctx, rr[0]),
|
||||
CanReadRecord: ctrl.ac.CanReadRecord(ctx, rr[0]),
|
||||
CanDeleteRecord: ctrl.ac.CanDeleteRecord(ctx, rr[0]),
|
||||
CanUndeleteRecord: ctrl.ac.CanUndeleteRecord(ctx, rr[0]),
|
||||
CanSearchRevisions: ctrl.ac.CanSearchRevisionsOnRecord(ctx, rr[0]),
|
||||
}, nil
|
||||
}
|
||||
@@ -637,6 +652,7 @@ func (ctrl Record) makePayload(ctx context.Context, m *types.Module, r *types.Re
|
||||
CanUpdateRecord: ctrl.ac.CanUpdateRecord(ctx, r),
|
||||
CanReadRecord: ctrl.ac.CanReadRecord(ctx, r),
|
||||
CanDeleteRecord: ctrl.ac.CanDeleteRecord(ctx, r),
|
||||
CanUndeleteRecord: ctrl.ac.CanUndeleteRecord(ctx, r),
|
||||
CanSearchRevisions: ctrl.ac.CanSearchRevisionsOnRecord(ctx, r),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -358,6 +358,40 @@ type (
|
||||
RecordID uint64 `json:",string"`
|
||||
}
|
||||
|
||||
RecordUndelete struct {
|
||||
// NamespaceID PATH parameter
|
||||
//
|
||||
// Namespace ID
|
||||
NamespaceID uint64 `json:",string"`
|
||||
|
||||
// ModuleID PATH parameter
|
||||
//
|
||||
// Module ID
|
||||
ModuleID uint64 `json:",string"`
|
||||
|
||||
// RecordID PATH parameter
|
||||
//
|
||||
// Record ID
|
||||
RecordID uint64 `json:",string"`
|
||||
}
|
||||
|
||||
RecordBulkUndelete struct {
|
||||
// NamespaceID PATH parameter
|
||||
//
|
||||
// Namespace ID
|
||||
NamespaceID uint64 `json:",string"`
|
||||
|
||||
// ModuleID PATH parameter
|
||||
//
|
||||
// Module ID
|
||||
ModuleID uint64 `json:",string"`
|
||||
|
||||
// RecordIDs POST parameter
|
||||
//
|
||||
// IDs of records to undelete
|
||||
RecordIDs []string
|
||||
}
|
||||
|
||||
RecordUpload struct {
|
||||
// NamespaceID PATH parameter
|
||||
//
|
||||
@@ -1729,6 +1763,154 @@ func (r *RecordDelete) Fill(req *http.Request) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
// NewRecordUndelete request
|
||||
func NewRecordUndelete() *RecordUndelete {
|
||||
return &RecordUndelete{}
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r RecordUndelete) Auditable() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"namespaceID": r.NamespaceID,
|
||||
"moduleID": r.ModuleID,
|
||||
"recordID": r.RecordID,
|
||||
}
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r RecordUndelete) GetNamespaceID() uint64 {
|
||||
return r.NamespaceID
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r RecordUndelete) GetModuleID() uint64 {
|
||||
return r.ModuleID
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r RecordUndelete) GetRecordID() uint64 {
|
||||
return r.RecordID
|
||||
}
|
||||
|
||||
// Fill processes request and fills internal variables
|
||||
func (r *RecordUndelete) Fill(req *http.Request) (err error) {
|
||||
|
||||
{
|
||||
var val string
|
||||
// path params
|
||||
|
||||
val = chi.URLParam(req, "namespaceID")
|
||||
r.NamespaceID, err = payload.ParseUint64(val), nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
val = chi.URLParam(req, "moduleID")
|
||||
r.ModuleID, err = payload.ParseUint64(val), nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
val = chi.URLParam(req, "recordID")
|
||||
r.RecordID, err = payload.ParseUint64(val), nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// NewRecordBulkUndelete request
|
||||
func NewRecordBulkUndelete() *RecordBulkUndelete {
|
||||
return &RecordBulkUndelete{}
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r RecordBulkUndelete) Auditable() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"namespaceID": r.NamespaceID,
|
||||
"moduleID": r.ModuleID,
|
||||
"recordIDs": r.RecordIDs,
|
||||
}
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r RecordBulkUndelete) GetNamespaceID() uint64 {
|
||||
return r.NamespaceID
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r RecordBulkUndelete) GetModuleID() uint64 {
|
||||
return r.ModuleID
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r RecordBulkUndelete) GetRecordIDs() []string {
|
||||
return r.RecordIDs
|
||||
}
|
||||
|
||||
// Fill processes request and fills internal variables
|
||||
func (r *RecordBulkUndelete) Fill(req *http.Request) (err error) {
|
||||
|
||||
if strings.HasPrefix(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 fmt.Errorf("error parsing http request body: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// Caching 32MB to memory, the rest to disk
|
||||
if err = req.ParseMultipartForm(32 << 20); err != nil && err != http.ErrNotMultipart {
|
||||
return err
|
||||
} else if err == nil {
|
||||
// Multipart params
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
if err = req.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// POST params
|
||||
|
||||
//if val, ok := req.Form["recordIDs[]"]; ok && len(val) > 0 {
|
||||
// r.RecordIDs, err = val, nil
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
//}
|
||||
}
|
||||
|
||||
{
|
||||
var val string
|
||||
// path params
|
||||
|
||||
val = chi.URLParam(req, "namespaceID")
|
||||
r.NamespaceID, err = payload.ParseUint64(val), nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
val = chi.URLParam(req, "moduleID")
|
||||
r.ModuleID, err = payload.ParseUint64(val), nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// NewRecordUpload request
|
||||
func NewRecordUpload() *RecordUpload {
|
||||
return &RecordUpload{}
|
||||
|
||||
+13
@@ -286,6 +286,11 @@ func (svc accessControl) List() (out []map[string]string) {
|
||||
"any": types.RecordRbacResource(0, 0, 0),
|
||||
"op": "delete",
|
||||
},
|
||||
{
|
||||
"type": types.RecordResourceType,
|
||||
"any": types.RecordRbacResource(0, 0, 0),
|
||||
"op": "undelete",
|
||||
},
|
||||
{
|
||||
"type": types.RecordResourceType,
|
||||
"any": types.RecordRbacResource(0, 0, 0),
|
||||
@@ -607,6 +612,13 @@ func (svc accessControl) CanDeleteRecord(ctx context.Context, r *types.Record) b
|
||||
return svc.can(ctx, "delete", r)
|
||||
}
|
||||
|
||||
// CanUndeleteRecord checks if current user can undelete
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) CanUndeleteRecord(ctx context.Context, r *types.Record) bool {
|
||||
return svc.can(ctx, "undelete", r)
|
||||
}
|
||||
|
||||
// CanManageOwnerOnRecord checks if current user can owner.manage
|
||||
//
|
||||
// This function is auto-generated
|
||||
@@ -806,6 +818,7 @@ func rbacResourceOperations(r string) map[string]bool {
|
||||
"read": true,
|
||||
"update": true,
|
||||
"delete": true,
|
||||
"undelete": true,
|
||||
"owner.manage": true,
|
||||
"revisions.search": true,
|
||||
}
|
||||
|
||||
+128
@@ -279,6 +279,13 @@ type (
|
||||
*recordBase
|
||||
}
|
||||
|
||||
// recordBeforeUndelete
|
||||
//
|
||||
// This type is auto-generated.
|
||||
recordBeforeUndelete struct {
|
||||
*recordBase
|
||||
}
|
||||
|
||||
// recordAfterCreate
|
||||
//
|
||||
// This type is auto-generated.
|
||||
@@ -299,6 +306,13 @@ type (
|
||||
recordAfterDelete struct {
|
||||
*recordBase
|
||||
}
|
||||
|
||||
// recordAfterUndelete
|
||||
//
|
||||
// This type is auto-generated.
|
||||
recordAfterUndelete struct {
|
||||
*recordBase
|
||||
}
|
||||
)
|
||||
|
||||
// ResourceType returns "compose"
|
||||
@@ -1916,6 +1930,13 @@ func (recordBeforeDelete) EventType() string {
|
||||
return "beforeDelete"
|
||||
}
|
||||
|
||||
// EventType on recordBeforeUndelete returns "beforeUndelete"
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (recordBeforeUndelete) EventType() string {
|
||||
return "beforeUndelete"
|
||||
}
|
||||
|
||||
// EventType on recordAfterCreate returns "afterCreate"
|
||||
//
|
||||
// This function is auto-generated.
|
||||
@@ -1937,6 +1958,13 @@ func (recordAfterDelete) EventType() string {
|
||||
return "afterDelete"
|
||||
}
|
||||
|
||||
// EventType on recordAfterUndelete returns "afterUndelete"
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (recordAfterUndelete) EventType() string {
|
||||
return "afterUndelete"
|
||||
}
|
||||
|
||||
// RecordOnManual creates onManual for compose:record resource
|
||||
//
|
||||
// This function is auto-generated.
|
||||
@@ -2187,6 +2215,56 @@ func RecordBeforeDeleteImmutable(
|
||||
}
|
||||
}
|
||||
|
||||
// RecordBeforeUndelete creates beforeUndelete for compose:record resource
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func RecordBeforeUndelete(
|
||||
argRecord *types.Record,
|
||||
argOldRecord *types.Record,
|
||||
argModule *types.Module,
|
||||
argNamespace *types.Namespace,
|
||||
argRecordValueErrors *types.RecordValueErrorSet,
|
||||
argSelected []interface{},
|
||||
) *recordBeforeUndelete {
|
||||
return &recordBeforeUndelete{
|
||||
recordBase: &recordBase{
|
||||
immutable: false,
|
||||
record: argRecord,
|
||||
oldRecord: argOldRecord,
|
||||
module: argModule,
|
||||
namespace: argNamespace,
|
||||
recordValueErrors: argRecordValueErrors,
|
||||
selected: argSelected,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// RecordBeforeUndeleteImmutable creates beforeUndelete for compose:record resource
|
||||
//
|
||||
// None of the arguments will be mutable!
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func RecordBeforeUndeleteImmutable(
|
||||
argRecord *types.Record,
|
||||
argOldRecord *types.Record,
|
||||
argModule *types.Module,
|
||||
argNamespace *types.Namespace,
|
||||
argRecordValueErrors *types.RecordValueErrorSet,
|
||||
argSelected []interface{},
|
||||
) *recordBeforeUndelete {
|
||||
return &recordBeforeUndelete{
|
||||
recordBase: &recordBase{
|
||||
immutable: true,
|
||||
record: argRecord,
|
||||
oldRecord: argOldRecord,
|
||||
module: argModule,
|
||||
namespace: argNamespace,
|
||||
recordValueErrors: argRecordValueErrors,
|
||||
selected: argSelected,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// RecordAfterCreate creates afterCreate for compose:record resource
|
||||
//
|
||||
// This function is auto-generated.
|
||||
@@ -2337,6 +2415,56 @@ func RecordAfterDeleteImmutable(
|
||||
}
|
||||
}
|
||||
|
||||
// RecordAfterUndelete creates afterUndelete for compose:record resource
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func RecordAfterUndelete(
|
||||
argRecord *types.Record,
|
||||
argOldRecord *types.Record,
|
||||
argModule *types.Module,
|
||||
argNamespace *types.Namespace,
|
||||
argRecordValueErrors *types.RecordValueErrorSet,
|
||||
argSelected []interface{},
|
||||
) *recordAfterUndelete {
|
||||
return &recordAfterUndelete{
|
||||
recordBase: &recordBase{
|
||||
immutable: false,
|
||||
record: argRecord,
|
||||
oldRecord: argOldRecord,
|
||||
module: argModule,
|
||||
namespace: argNamespace,
|
||||
recordValueErrors: argRecordValueErrors,
|
||||
selected: argSelected,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// RecordAfterUndeleteImmutable creates afterUndelete for compose:record resource
|
||||
//
|
||||
// None of the arguments will be mutable!
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func RecordAfterUndeleteImmutable(
|
||||
argRecord *types.Record,
|
||||
argOldRecord *types.Record,
|
||||
argModule *types.Module,
|
||||
argNamespace *types.Namespace,
|
||||
argRecordValueErrors *types.RecordValueErrorSet,
|
||||
argSelected []interface{},
|
||||
) *recordAfterUndelete {
|
||||
return &recordAfterUndelete{
|
||||
recordBase: &recordBase{
|
||||
immutable: true,
|
||||
record: argRecord,
|
||||
oldRecord: argOldRecord,
|
||||
module: argModule,
|
||||
namespace: argNamespace,
|
||||
recordValueErrors: argRecordValueErrors,
|
||||
selected: argSelected,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// SetRecord sets new record value
|
||||
//
|
||||
// This function is auto-generated.
|
||||
|
||||
@@ -55,7 +55,7 @@ compose:module:
|
||||
|
||||
compose:record:
|
||||
on: ['manual', 'iteration']
|
||||
ba: ['create', 'update', 'delete']
|
||||
ba: ['create', 'update', 'delete', 'undelete']
|
||||
props:
|
||||
- name: 'record'
|
||||
type: '*types.Record'
|
||||
|
||||
@@ -97,6 +97,7 @@ type (
|
||||
CanReadRecord(context.Context, *types.Record) bool
|
||||
CanUpdateRecord(context.Context, *types.Record) bool
|
||||
CanDeleteRecord(context.Context, *types.Record) bool
|
||||
CanUndeleteRecord(context.Context, *types.Record) bool
|
||||
CanSearchRevisionsOnRecord(context.Context, *types.Record) bool
|
||||
|
||||
recordManageOwnerAccessController
|
||||
@@ -128,6 +129,7 @@ type (
|
||||
Validate(ctx context.Context, rec *types.Record) error
|
||||
|
||||
DeleteByID(ctx context.Context, namespaceID, moduleID uint64, recordID ...uint64) error
|
||||
UndeleteByID(ctx context.Context, namespaceID, moduleID uint64, recordID ...uint64) error
|
||||
|
||||
Organize(ctx context.Context, namespaceID, moduleID, recordID uint64, sortingField, sortingValue, sortingFilter, valueField, value string) error
|
||||
|
||||
@@ -497,7 +499,6 @@ func (svc record) searchSensitive(ctx context.Context, userID uint64, namespace
|
||||
}
|
||||
|
||||
// SearchRevisions returns iterator for revisions of a record
|
||||
//
|
||||
func (svc record) SearchRevisions(ctx context.Context, namespaceID, moduleID, recordID uint64) (dal.Iterator, error) {
|
||||
var (
|
||||
aProps = &recordActionProps{record: &types.Record{NamespaceID: namespaceID, ModuleID: moduleID, ID: recordID}}
|
||||
@@ -1265,6 +1266,59 @@ func (svc record) delete(ctx context.Context, namespaceID, moduleID, recordID ui
|
||||
return del, nil
|
||||
}
|
||||
|
||||
func (svc record) undelete(ctx context.Context, namespaceID, moduleID, recordID uint64) (undel *types.Record, err error) {
|
||||
var (
|
||||
ns *types.Namespace
|
||||
m *types.Module
|
||||
)
|
||||
|
||||
ns, m, undel, err = loadRecordCombo(ctx, svc.store, svc.dal, namespaceID, moduleID, recordID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !svc.ac.CanUndeleteRecord(ctx, undel) {
|
||||
return nil, RecordErrNotAllowedToUndelete()
|
||||
}
|
||||
|
||||
undel.DeletedAt = nil
|
||||
undel.DeletedBy = 0
|
||||
|
||||
// ensure module ref is set before running through records workflows and scripts
|
||||
undel.SetModule(m)
|
||||
|
||||
undel.DeletedAt = nil
|
||||
undel.DeletedBy = 0
|
||||
undel.Revision = undel.Revision + 1
|
||||
|
||||
{
|
||||
// Calling before-record-undelete scripts
|
||||
if err = svc.eventbus.WaitFor(ctx, event.RecordBeforeUndelete(nil, undel, m, ns, nil, nil)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if m.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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// ensure module ref is set before running through records workflows and scripts
|
||||
undel.SetModule(m)
|
||||
|
||||
{
|
||||
_ = svc.eventbus.WaitFor(ctx, event.RecordAfterUndeleteImmutable(nil, undel, m, ns, nil, nil))
|
||||
}
|
||||
|
||||
return undel, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -1329,6 +1383,66 @@ func (svc record) DeleteByID(ctx context.Context, namespaceID, moduleID uint64,
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc record) UndeleteByID(ctx context.Context, namespaceID, moduleID uint64, recordIDs ...uint64) (err error) {
|
||||
var (
|
||||
aProps = &recordActionProps{
|
||||
namespace: &types.Namespace{ID: namespaceID},
|
||||
module: &types.Module{ID: moduleID},
|
||||
}
|
||||
|
||||
isBulkUndelete = len(recordIDs) > 1
|
||||
|
||||
ns *types.Namespace
|
||||
m *types.Module
|
||||
r *types.Record
|
||||
)
|
||||
|
||||
err = func() error {
|
||||
if namespaceID == 0 {
|
||||
return RecordErrInvalidNamespaceID()
|
||||
}
|
||||
if moduleID == 0 {
|
||||
return RecordErrInvalidModuleID()
|
||||
}
|
||||
|
||||
ns, m, err = loadModuleCombo(ctx, svc.store, namespaceID, moduleID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aProps.setNamespace(ns)
|
||||
aProps.setModule(m)
|
||||
|
||||
return nil
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
return svc.recordAction(ctx, aProps, RecordActionUndelete, err)
|
||||
}
|
||||
|
||||
for _, recordID := range recordIDs {
|
||||
err := func() (err error) {
|
||||
r, err = svc.undelete(ctx, namespaceID, moduleID, recordID)
|
||||
if err != nil {
|
||||
return svc.recordAction(ctx, aProps, RecordActionUndelete, err)
|
||||
}
|
||||
aProps.setRecord(r)
|
||||
|
||||
if err = dalutils.ComposeRecordUpdate(ctx, svc.dal, m, r); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return svc.recordAction(ctx, aProps, RecordActionUndelete, err)
|
||||
}()
|
||||
|
||||
if err != nil && !isBulkUndelete {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc record) Organize(ctx context.Context, namespaceID, moduleID, recordID uint64, posField, position, filter, grpField, group string) (err error) {
|
||||
var (
|
||||
ns *types.Namespace
|
||||
@@ -1542,30 +1656,29 @@ func (svc record) TriggerScript(ctx context.Context, namespaceID, moduleID, reco
|
||||
// - delete: delete records (unless aborted)
|
||||
// - default: only iterates over records, records are not changed, return value is ignored
|
||||
//
|
||||
//
|
||||
// Iterator can be invoked only when defined in corredor script:
|
||||
//
|
||||
// return default {
|
||||
// iterator (each) {
|
||||
// return each({
|
||||
// resourceType: 'compose:record',
|
||||
// // action: 'update',
|
||||
// filter: {
|
||||
// namespace: '122709101053521922',
|
||||
// module: '122709116471783426',
|
||||
// query: 'Status = "foo"',
|
||||
// sort: 'Status DESC',
|
||||
// limit: 3,
|
||||
// },
|
||||
// })
|
||||
// },
|
||||
// return default {
|
||||
// iterator (each) {
|
||||
// return each({
|
||||
// resourceType: 'compose:record',
|
||||
// // action: 'update',
|
||||
// filter: {
|
||||
// namespace: '122709101053521922',
|
||||
// module: '122709116471783426',
|
||||
// query: 'Status = "foo"',
|
||||
// sort: 'Status DESC',
|
||||
// limit: 3,
|
||||
// },
|
||||
// })
|
||||
// },
|
||||
//
|
||||
// // this is required in case of a deferred iterator
|
||||
// // security: { runAs: .... } }
|
||||
// // this is required in case of a deferred iterator
|
||||
// // security: { runAs: .... } }
|
||||
//
|
||||
// // exec gets called for every record found by iterator
|
||||
// exec () { ... }
|
||||
// }
|
||||
// // exec gets called for every record found by iterator
|
||||
// exec () { ... }
|
||||
// }
|
||||
func (svc record) Iterator(ctx context.Context, f types.RecordFilter, fn eventbus.HandlerFn, action string) (err error) {
|
||||
var (
|
||||
invokerID = auth.GetIdentityFromContext(ctx).Identity()
|
||||
@@ -1604,6 +1717,10 @@ func (svc record) Iterator(ctx context.Context, f types.RecordFilter, fn eventbu
|
||||
if !svc.ac.CanDeleteRecord(ctx, rec) {
|
||||
return RecordErrNotAllowedToDelete()
|
||||
}
|
||||
case "undelete":
|
||||
if !svc.ac.CanUndeleteRecord(ctx, rec) {
|
||||
return RecordErrNotAllowedToUndelete()
|
||||
}
|
||||
}
|
||||
recordableAction := RecordActionIteratorIteration
|
||||
|
||||
@@ -1655,6 +1772,14 @@ func (svc record) Iterator(ctx context.Context, f types.RecordFilter, fn eventbu
|
||||
rec.DeletedBy = invokerID
|
||||
return dalutils.ComposeRecordSoftDelete(ctx, svc.dal, m, rec)
|
||||
})
|
||||
case "undelete":
|
||||
recordableAction = RecordActionIteratorUndelete
|
||||
|
||||
return store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) error {
|
||||
rec.DeletedAt = nil
|
||||
rec.DeletedBy = 0
|
||||
return dalutils.ComposeRecordUndelete(ctx, svc.dal, m, rec)
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
+19
@@ -738,6 +738,25 @@ func RecordActionIteratorDelete(props ...*recordActionProps) *recordAction {
|
||||
return a
|
||||
}
|
||||
|
||||
// RecordActionIteratorUndelete returns "compose:record.iteratorUndelete" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func RecordActionIteratorUndelete(props ...*recordActionProps) *recordAction {
|
||||
a := &recordAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "compose:record",
|
||||
action: "iteratorUndelete",
|
||||
log: "undeleted record in iteration",
|
||||
severity: actionlog.Notice,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
// Error constructors
|
||||
|
||||
@@ -100,6 +100,9 @@ actions:
|
||||
- action: iteratorDelete
|
||||
log: "deleted record in iteration"
|
||||
|
||||
- action: iteratorUndelete
|
||||
log: "undeleted record in iteration"
|
||||
|
||||
errors:
|
||||
- error: notFound
|
||||
message: "record not found"
|
||||
|
||||
@@ -91,6 +91,20 @@ func (svc *recordRevisions) softDeleted(ctx context.Context, del *types.Record)
|
||||
return svc.r.Create(ctx, svc.modelRef(del.GetModule()), rev)
|
||||
}
|
||||
|
||||
func (svc *recordRevisions) undeleted(ctx context.Context, undel *types.Record) (err error) {
|
||||
var (
|
||||
invokerID = auth.GetIdentityFromContext(ctx).Identity()
|
||||
rev *revisions.Revision
|
||||
)
|
||||
|
||||
rev = revisions.Make(revisions.Undeleted, undel.Revision, undel.ID, invokerID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return svc.r.Create(ctx, svc.modelRef(undel.GetModule()), rev)
|
||||
}
|
||||
|
||||
func (svc *recordRevisions) skippedField(mod *types.Module) []string {
|
||||
list := []string{
|
||||
"ID",
|
||||
@@ -115,21 +129,6 @@ func (svc *recordRevisions) skippedField(mod *types.Module) []string {
|
||||
return list
|
||||
}
|
||||
|
||||
// @todo uncomment when supported
|
||||
//func (svc *recordRevisions) restored(ctx context.Context, del *types.Record) (err error) {
|
||||
// var (
|
||||
// invokerID = auth.GetIdentityFromContext(ctx).Identity()
|
||||
// rev *revisions.Revision
|
||||
// )
|
||||
//
|
||||
// rev, err = revisions.Make(revisions.Restored, del.Revision, del.ID, invokerID, nil, nil)
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// return svc.r.Create(ctx, svc.modelRef(del.GetModule()), rev)
|
||||
//}
|
||||
|
||||
// @todo uncomment when supported
|
||||
//func (svc *recordRevisions) hardDeleted(ctx context.Context, del *types.Record) (err error) {
|
||||
// var (
|
||||
|
||||
@@ -9,6 +9,6 @@ const (
|
||||
Created = "created"
|
||||
Updated = "updated"
|
||||
SoftDeleted = "soft-deleted"
|
||||
Restored = "restored"
|
||||
Undeleted = "undeleted"
|
||||
HardDeleted = "hard-deleted"
|
||||
)
|
||||
|
||||
@@ -97,7 +97,6 @@ func Model(m *dal.Model, c queryRunner, d drivers.Dialect) *model {
|
||||
//
|
||||
// Alternative solution would introduce a mutes on the internal model but that
|
||||
// is probably the same or worse as this.
|
||||
//
|
||||
func (d *model) parseQuery(q string) (out exp.Expression, err error) {
|
||||
return d.QueryParser().Parse(q)
|
||||
}
|
||||
@@ -108,7 +107,8 @@ func (d *model) convertQuery(n *ql.ASTNode) (out exp.Expression, err error) {
|
||||
|
||||
// QueryParser returns ql struct that allows parsing query strings or converting AST into expression
|
||||
// @todo benchmark to see if this re-init is a bad idea; I don't think it should be
|
||||
// since we're just initializing fairly light structs.
|
||||
//
|
||||
// since we're just initializing fairly light structs.
|
||||
func (d *model) QueryParser() queryParser {
|
||||
return ql.Converter(
|
||||
ql.SymHandler(d.qlConverterGenericSymHandler()),
|
||||
@@ -453,7 +453,7 @@ func (d *model) searchSql(f filter.Filter) *goqu.SelectDataset {
|
||||
switch state {
|
||||
case filter.StateExclusive:
|
||||
// only not-null values
|
||||
cnd = append(cnd, exp.NewLiteralExpression("? IS NULL", attrExpr))
|
||||
cnd = append(cnd, exp.NewLiteralExpression("? IS NOT NULL", attrExpr))
|
||||
|
||||
case filter.StateExcluded:
|
||||
// exclude all non-null values
|
||||
|
||||
Reference in New Issue
Block a user