mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-18 05:01:07 +00:00
Merge branch 'feature/bulk-edit-from-file' into integration
This commit is contained in:
@ -57,7 +57,7 @@ public class BulkTableActionProcessPermissionChecker implements CustomPermission
|
||||
switch(bulkActionName)
|
||||
{
|
||||
case "bulkInsert" -> PermissionsHelper.checkTablePermissionThrowing(tableActionInput, TablePermissionSubType.INSERT);
|
||||
case "bulkEdit" -> PermissionsHelper.checkTablePermissionThrowing(tableActionInput, TablePermissionSubType.EDIT);
|
||||
case "bulkEdit", "bulkEditWithFile" -> PermissionsHelper.checkTablePermissionThrowing(tableActionInput, TablePermissionSubType.EDIT);
|
||||
case "bulkDelete" -> PermissionsHelper.checkTablePermissionThrowing(tableActionInput, TablePermissionSubType.DELETE);
|
||||
default -> LOG.warn("Unexpected bulk action name when checking permissions for process: " + processName);
|
||||
}
|
||||
|
@ -48,6 +48,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
|
||||
import com.kingsrook.qqq.backend.core.instances.enrichment.plugins.QInstanceEnricherPluginInterface;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.bulk.TableKeyFieldsPossibleValueSource;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QSupplementalInstanceMetaData;
|
||||
@ -901,6 +902,11 @@ public class QInstanceEnricher
|
||||
*******************************************************************************/
|
||||
private void defineTableBulkProcesses(QInstance qInstance)
|
||||
{
|
||||
if(qInstance.getPossibleValueSource(TableKeyFieldsPossibleValueSource.NAME) == null)
|
||||
{
|
||||
qInstance.addPossibleValueSource(defineTableKeyFieldsPossibleValueSource());
|
||||
}
|
||||
|
||||
for(QTableMetaData table : qInstance.getTables().values())
|
||||
{
|
||||
if(table.getFields() == null)
|
||||
@ -924,6 +930,12 @@ public class QInstanceEnricher
|
||||
defineTableBulkEdit(qInstance, table, bulkEditProcessName);
|
||||
}
|
||||
|
||||
String bulkEditWithFileProcessName = table.getName() + ".bulkEditWithFile";
|
||||
if(qInstance.getProcess(bulkEditWithFileProcessName) == null)
|
||||
{
|
||||
defineTableBulkEditWithFile(qInstance, table, bulkEditWithFileProcessName);
|
||||
}
|
||||
|
||||
String bulkDeleteProcessName = table.getName() + ".bulkDelete";
|
||||
if(qInstance.getProcess(bulkDeleteProcessName) == null)
|
||||
{
|
||||
@ -1104,6 +1116,122 @@ public class QInstanceEnricher
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void defineTableBulkEditWithFile(QInstance qInstance, QTableMetaData table, String processName)
|
||||
{
|
||||
Map<String, Serializable> values = new HashMap<>();
|
||||
values.put(StreamedETLWithFrontendProcess.FIELD_DESTINATION_TABLE, table.getName());
|
||||
values.put(StreamedETLWithFrontendProcess.FIELD_PREVIEW_MESSAGE, "This is a preview of the records that will be updated.");
|
||||
|
||||
QProcessMetaData process = StreamedETLWithFrontendProcess.defineProcessMetaData(
|
||||
BulkInsertExtractStep.class,
|
||||
BulkInsertTransformStep.class,
|
||||
BulkEditLoadStep.class,
|
||||
values
|
||||
)
|
||||
.withName(processName)
|
||||
.withLabel(table.getLabel() + " Bulk Edit With File")
|
||||
.withTableName(table.getName())
|
||||
.withIsHidden(true)
|
||||
.withPermissionRules(qInstance.getDefaultPermissionRules().clone()
|
||||
.withCustomPermissionChecker(new QCodeReference(BulkTableActionProcessPermissionChecker.class)));
|
||||
|
||||
List<QFieldMetaData> editableFields = table.getFields().values().stream()
|
||||
.filter(QFieldMetaData::getIsEditable)
|
||||
.filter(f -> !f.getType().equals(QFieldType.BLOB))
|
||||
.toList();
|
||||
|
||||
QBackendStepMetaData prepareFileUploadStep = new QBackendStepMetaData()
|
||||
.withName("prepareFileUpload")
|
||||
.withCode(new QCodeReference(BulkInsertPrepareFileUploadStep.class));
|
||||
|
||||
QFrontendStepMetaData uploadScreen = new QFrontendStepMetaData()
|
||||
.withName("upload")
|
||||
.withLabel("Upload File")
|
||||
.withFormField(new QFieldMetaData("theFile", QFieldType.BLOB)
|
||||
.withFieldAdornment(FileUploadAdornment.newFieldAdornment()
|
||||
.withValue(FileUploadAdornment.formatDragAndDrop())
|
||||
.withValue(FileUploadAdornment.widthFull()))
|
||||
.withLabel(table.getLabel() + " File")
|
||||
.withIsRequired(true))
|
||||
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.HTML))
|
||||
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.EDIT_FORM));
|
||||
|
||||
QBackendStepMetaData prepareFileMappingStep = new QBackendStepMetaData()
|
||||
.withName("prepareFileMapping")
|
||||
.withCode(new QCodeReference(BulkInsertPrepareFileMappingStep.class));
|
||||
|
||||
QFrontendStepMetaData fileMappingScreen = new QFrontendStepMetaData()
|
||||
.withName("fileMapping")
|
||||
.withLabel("File Mapping")
|
||||
.withBackStepName("prepareFileUpload")
|
||||
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.BULK_LOAD_FILE_MAPPING_FORM))
|
||||
.withFormField(new QFieldMetaData("hasHeaderRow", QFieldType.BOOLEAN))
|
||||
.withFormField(new QFieldMetaData("layout", QFieldType.STRING)) // is actually PVS, but, this field is only added to help support helpContent, so :shrug:
|
||||
.withFormField(new QFieldMetaData("tableKeyFields", QFieldType.STRING).withPossibleValueSourceName(TableKeyFieldsPossibleValueSource.NAME));
|
||||
|
||||
QBackendStepMetaData receiveFileMappingStep = new QBackendStepMetaData()
|
||||
.withName("receiveFileMapping")
|
||||
.withCode(new QCodeReference(BulkInsertReceiveFileMappingStep.class));
|
||||
|
||||
QBackendStepMetaData prepareValueMappingStep = new QBackendStepMetaData()
|
||||
.withName("prepareValueMapping")
|
||||
.withCode(new QCodeReference(BulkInsertPrepareValueMappingStep.class));
|
||||
|
||||
QFrontendStepMetaData valueMappingScreen = new QFrontendStepMetaData()
|
||||
.withName("valueMapping")
|
||||
.withLabel("Value Mapping")
|
||||
.withBackStepName("prepareFileMapping")
|
||||
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.BULK_LOAD_VALUE_MAPPING_FORM));
|
||||
|
||||
QBackendStepMetaData receiveValueMappingStep = new QBackendStepMetaData()
|
||||
.withName("receiveValueMapping")
|
||||
.withCode(new QCodeReference(BulkInsertReceiveValueMappingStep.class));
|
||||
|
||||
int i = 0;
|
||||
process.withStep(i++, prepareFileUploadStep);
|
||||
process.withStep(i++, uploadScreen);
|
||||
|
||||
process.withStep(i++, prepareFileMappingStep);
|
||||
process.withStep(i++, fileMappingScreen);
|
||||
process.withStep(i++, receiveFileMappingStep);
|
||||
|
||||
process.withStep(i++, prepareValueMappingStep);
|
||||
process.withStep(i++, valueMappingScreen);
|
||||
process.withStep(i++, receiveValueMappingStep);
|
||||
|
||||
process.getFrontendStep(StreamedETLWithFrontendProcess.STEP_NAME_REVIEW).setRecordListFields(editableFields);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
// put the bulk-load profile form (e.g., for saving it) on the review & result screens) //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
process.getFrontendStep(StreamedETLWithFrontendProcess.STEP_NAME_REVIEW)
|
||||
.withBackStepName("prepareFileMapping")
|
||||
.getComponents().add(0, new QFrontendComponentMetaData().withType(QComponentType.BULK_LOAD_PROFILE_FORM));
|
||||
|
||||
process.getFrontendStep(StreamedETLWithFrontendProcess.STEP_NAME_RESULT)
|
||||
.getComponents().add(0, new QFrontendComponentMetaData().withType(QComponentType.BULK_LOAD_PROFILE_FORM));
|
||||
|
||||
qInstance.addProcess(process);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private QPossibleValueSource defineTableKeyFieldsPossibleValueSource()
|
||||
{
|
||||
return (new QPossibleValueSource()
|
||||
.withName(TableKeyFieldsPossibleValueSource.NAME)
|
||||
.withType(QPossibleValueSourceType.CUSTOM)
|
||||
.withCustomCodeReference(new QCodeReference(TableKeyFieldsPossibleValueSource.class)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -35,11 +35,13 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
*******************************************************************************/
|
||||
public class SearchPossibleValueSourceInput extends AbstractActionInput implements Cloneable
|
||||
{
|
||||
private String possibleValueSourceName;
|
||||
private QQueryFilter defaultQueryFilter;
|
||||
private String searchTerm;
|
||||
private List<Serializable> idList;
|
||||
private List<String> labelList;
|
||||
private String possibleValueSourceName;
|
||||
private QQueryFilter defaultQueryFilter;
|
||||
private String searchTerm;
|
||||
private List<Serializable> idList;
|
||||
private List<String> labelList;
|
||||
private Map<String, String> pathParamMap;
|
||||
private Map<String, List<String>> queryParamMap;
|
||||
|
||||
private Map<String, Serializable> otherValues;
|
||||
|
||||
@ -319,6 +321,68 @@ public class SearchPossibleValueSourceInput extends AbstractActionInput implemen
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for pathParamMap
|
||||
*******************************************************************************/
|
||||
public Map<String, String> getPathParamMap()
|
||||
{
|
||||
return (this.pathParamMap);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for pathParamMap
|
||||
*******************************************************************************/
|
||||
public void setPathParamMap(Map<String, String> pathParamMap)
|
||||
{
|
||||
this.pathParamMap = pathParamMap;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for pathParamMap
|
||||
*******************************************************************************/
|
||||
public SearchPossibleValueSourceInput withPathParamMap(Map<String, String> pathParamMap)
|
||||
{
|
||||
this.pathParamMap = pathParamMap;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for queryParamMap
|
||||
*******************************************************************************/
|
||||
public Map<String, List<String>> getQueryParamMap()
|
||||
{
|
||||
return (this.queryParamMap);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for queryParamMap
|
||||
*******************************************************************************/
|
||||
public void setQueryParamMap(Map<String, List<String>> queryParamMap)
|
||||
{
|
||||
this.queryParamMap = queryParamMap;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for queryParamMap
|
||||
*******************************************************************************/
|
||||
public SearchPossibleValueSourceInput withQueryParamMap(Map<String, List<String>> queryParamMap)
|
||||
{
|
||||
this.queryParamMap = queryParamMap;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for otherValues
|
||||
*******************************************************************************/
|
||||
|
@ -0,0 +1,153 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.bulk;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class TableKeyFieldsPossibleValueSource implements QCustomPossibleValueProvider<String>
|
||||
{
|
||||
public static final String NAME = "tableKeyFields";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public QPossibleValue<String> getPossibleValue(Serializable tableAndKey)
|
||||
{
|
||||
QPossibleValue<String> possibleValue = null;
|
||||
|
||||
/////////////////////////////////////////////////////////////
|
||||
// keys are in the format <tableName>-<key1>|<key2>|<key3> //
|
||||
/////////////////////////////////////////////////////////////
|
||||
String[] keyParts = tableAndKey.toString().split("-");
|
||||
String tableName = keyParts[0];
|
||||
String key = keyParts[1];
|
||||
|
||||
QTableMetaData table = QContext.getQInstance().getTable(tableName);
|
||||
if(table.getPrimaryKeyField().equals(key))
|
||||
{
|
||||
String id = table.getPrimaryKeyField();
|
||||
String label = table.getField(table.getPrimaryKeyField()).getLabel();
|
||||
possibleValue = new QPossibleValue<>(id, label);
|
||||
}
|
||||
else
|
||||
{
|
||||
for(UniqueKey uniqueKey : table.getUniqueKeys())
|
||||
{
|
||||
String potentialMatch = getIdFromUniqueKey(uniqueKey);
|
||||
if(potentialMatch.equals(key))
|
||||
{
|
||||
String id = potentialMatch;
|
||||
String label = getLabelFromUniqueKey(table, uniqueKey);
|
||||
possibleValue = new QPossibleValue<>(id, label);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (possibleValue);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<QPossibleValue<String>> search(SearchPossibleValueSourceInput input) throws QException
|
||||
{
|
||||
List<QPossibleValue<String>> rs = new ArrayList<>();
|
||||
if(!CollectionUtils.nonNullMap(input.getPathParamMap()).containsKey("processName") || input.getPathParamMap().get("processName") == null || input.getPathParamMap().get("processName").isEmpty())
|
||||
{
|
||||
throw (new QException("Path Param of processName was not found."));
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
// process name will be like tnt.bulkEditWithFile //
|
||||
////////////////////////////////////////////////////
|
||||
String processName = input.getPathParamMap().get("processName");
|
||||
String tableName = processName.split("\\.")[0];
|
||||
|
||||
QTableMetaData table = QContext.getQInstance().getTable(tableName);
|
||||
for(UniqueKey uniqueKey : CollectionUtils.nonNullList(table.getUniqueKeys()))
|
||||
{
|
||||
String id = getIdFromUniqueKey(uniqueKey);
|
||||
String label = getLabelFromUniqueKey(table, uniqueKey);
|
||||
if(!StringUtils.hasContent(input.getSearchTerm()) || input.getSearchTerm().equals(id))
|
||||
{
|
||||
rs.add(new QPossibleValue<>(id, label));
|
||||
}
|
||||
}
|
||||
rs.sort(Comparator.comparing(QPossibleValue::getLabel));
|
||||
|
||||
///////////////////////////////
|
||||
// put the primary key first //
|
||||
///////////////////////////////
|
||||
if(!StringUtils.hasContent(input.getSearchTerm()) || input.getSearchTerm().equals(table.getPrimaryKeyField()))
|
||||
{
|
||||
rs.add(0, new QPossibleValue<>(table.getPrimaryKeyField(), table.getField(table.getPrimaryKeyField()).getLabel()));
|
||||
}
|
||||
|
||||
return rs;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private String getIdFromUniqueKey(UniqueKey uniqueKey)
|
||||
{
|
||||
return (StringUtils.join("|", uniqueKey.getFieldNames()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private String getLabelFromUniqueKey(QTableMetaData tableMetaData, UniqueKey uniqueKey)
|
||||
{
|
||||
List<String> fieldLabels = new ArrayList<>(uniqueKey.getFieldNames().stream().map(f -> tableMetaData.getField(f).getLabel()).toList());
|
||||
fieldLabels.sort(Comparator.naturalOrder());
|
||||
return (StringUtils.joinWithCommasAndAnd(fieldLabels));
|
||||
}
|
||||
}
|
@ -60,6 +60,9 @@ public class SavedBulkLoadProfile extends QRecordEntity
|
||||
@QField(label = "Mapping JSON")
|
||||
private String mappingJson;
|
||||
|
||||
@QField()
|
||||
private Boolean isBulkEdit;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -251,7 +254,6 @@ public class SavedBulkLoadProfile extends QRecordEntity
|
||||
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for mappingJson
|
||||
*******************************************************************************/
|
||||
@ -282,4 +284,34 @@ public class SavedBulkLoadProfile extends QRecordEntity
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for isBulkEdit
|
||||
*******************************************************************************/
|
||||
public Boolean getIsBulkEdit()
|
||||
{
|
||||
return (this.isBulkEdit);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for isBulkEdit
|
||||
*******************************************************************************/
|
||||
public void setIsBulkEdit(Boolean isBulkEdit)
|
||||
{
|
||||
this.isBulkEdit = isBulkEdit;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for isBulkEdit
|
||||
*******************************************************************************/
|
||||
public SavedBulkLoadProfile withIsBulkEdit(Boolean isBulkEdit)
|
||||
{
|
||||
this.isBulkEdit = isBulkEdit;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -113,7 +113,7 @@ public class SavedBulkLoadProfileMetaDataProvider
|
||||
.withFieldsFromEntity(SavedBulkLoadProfile.class)
|
||||
.withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.FIELD))
|
||||
.withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "label", "tableName")))
|
||||
.withSection(new QFieldSection("details", new QIcon().withName("text_snippet"), Tier.T2, List.of("userId", "mappingJson")))
|
||||
.withSection(new QFieldSection("details", new QIcon().withName("text_snippet"), Tier.T2, List.of("userId", "mappingJson", "isBulkEdit")))
|
||||
.withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate")));
|
||||
|
||||
table.getField("mappingJson").withBehavior(SavedBulkLoadProfileJsonFieldDisplayValueFormatter.getInstance());
|
||||
|
@ -36,10 +36,12 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.InputSource;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertTransformStep;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.LoadViaUpdateStep;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ProcessSummaryProviderInterface;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.general.ProcessSummaryWarningsAndErrorsRollup;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import org.apache.commons.lang.BooleanUtils;
|
||||
import static com.kingsrook.qqq.backend.core.processes.implementations.bulk.edit.BulkEditTransformStep.buildInfoSummaryLines;
|
||||
|
||||
|
||||
@ -53,6 +55,9 @@ public class BulkEditLoadStep extends LoadViaUpdateStep implements ProcessSummar
|
||||
private ProcessSummaryLine okSummary = new ProcessSummaryLine(Status.OK);
|
||||
private List<ProcessSummaryLine> infoSummaries = new ArrayList<>();
|
||||
|
||||
private Serializable firstInsertedPrimaryKey = null;
|
||||
private Serializable lastInsertedPrimaryKey = null;
|
||||
|
||||
private ProcessSummaryWarningsAndErrorsRollup processSummaryWarningsAndErrorsRollup = ProcessSummaryWarningsAndErrorsRollup.build("edited");
|
||||
|
||||
private String tableLabel;
|
||||
@ -106,7 +111,15 @@ public class BulkEditLoadStep extends LoadViaUpdateStep implements ProcessSummar
|
||||
tableLabel = table.getLabel();
|
||||
}
|
||||
|
||||
buildInfoSummaryLines(runBackendStepInput, table, infoSummaries, true);
|
||||
boolean isBulkEdit = BooleanUtils.isTrue(runBackendStepInput.getValueBoolean("isBulkEdit"));
|
||||
if(isBulkEdit)
|
||||
{
|
||||
buildBulkUpdateWithFileInfoSummaryLines(runBackendStepOutput, table);
|
||||
}
|
||||
else
|
||||
{
|
||||
buildInfoSummaryLines(runBackendStepInput, table, infoSummaries, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -146,4 +159,83 @@ public class BulkEditLoadStep extends LoadViaUpdateStep implements ProcessSummar
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private void buildBulkUpdateWithFileInfoSummaryLines(RunBackendStepOutput runBackendStepOutput, QTableMetaData table)
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// the transform step builds summary lines that it predicts will update successfully. //
|
||||
// but those lines don't have ids, which we'd like to have (e.g., for a process trace that //
|
||||
// might link to the built record). also, it's possible that there was a fail that only //
|
||||
// happened in the actual update, so, basically, re-do the summary here //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
BulkInsertTransformStep transformStep = (BulkInsertTransformStep) getTransformStep();
|
||||
ProcessSummaryLine okSummary = transformStep.okSummary;
|
||||
okSummary.setCount(0);
|
||||
okSummary.setPrimaryKeys(new ArrayList<>());
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// but - since errors from the transform step don't even make it through to us here in the load step, //
|
||||
// do re-use the ProcessSummaryWarningsAndErrorsRollup from transform step as follows: //
|
||||
// clear out its warnings - we'll completely rebuild them here (with primary keys) //
|
||||
// and add new error lines, e.g., in case of errors that only happened past the validation if possible. //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
ProcessSummaryWarningsAndErrorsRollup processSummaryWarningsAndErrorsRollup = transformStep.processSummaryWarningsAndErrorsRollup;
|
||||
processSummaryWarningsAndErrorsRollup.resetWarnings();
|
||||
|
||||
List<QRecord> updatedRecords = runBackendStepOutput.getRecords();
|
||||
for(QRecord updatedRecord : updatedRecords)
|
||||
{
|
||||
Serializable primaryKey = updatedRecord.getValue(table.getPrimaryKeyField());
|
||||
if(CollectionUtils.nullSafeIsEmpty(updatedRecord.getErrors()) && primaryKey != null)
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
// if the record had no errors, and we have a primary key for it, then //
|
||||
// keep track of the range of primary keys (first and last) //
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
if(firstInsertedPrimaryKey == null)
|
||||
{
|
||||
firstInsertedPrimaryKey = primaryKey;
|
||||
}
|
||||
|
||||
lastInsertedPrimaryKey = primaryKey;
|
||||
|
||||
if(!CollectionUtils.nullSafeIsEmpty(updatedRecord.getWarnings()))
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// if there were warnings on the updated record, put it in a warning line //
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
String message = updatedRecord.getWarnings().get(0).getMessage();
|
||||
processSummaryWarningsAndErrorsRollup.addWarning(message, primaryKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// if no warnings for the updated record, then put it in the OK line //
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
okSummary.incrementCountAndAddPrimaryKey(primaryKey);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// else if there were errors or no primary key, build an error line //
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
String message = "Failed to update";
|
||||
if(!CollectionUtils.nullSafeIsEmpty(updatedRecord.getErrors()))
|
||||
{
|
||||
//////////////////////////////////////////////////////////
|
||||
// use the error message from the record if we have one //
|
||||
//////////////////////////////////////////////////////////
|
||||
message = updatedRecord.getErrors().get(0).getMessage();
|
||||
}
|
||||
processSummaryWarningsAndErrorsRollup.addError(message, primaryKey);
|
||||
}
|
||||
}
|
||||
|
||||
okSummary.pickMessage(true);
|
||||
}
|
||||
}
|
||||
|
@ -48,6 +48,7 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
import org.apache.commons.lang.BooleanUtils;
|
||||
import org.json.JSONObject;
|
||||
|
||||
|
||||
@ -65,9 +66,11 @@ public class BulkInsertPrepareFileMappingStep implements BackendStep
|
||||
{
|
||||
buildFileDetailsForMappingStep(runBackendStepInput, runBackendStepOutput);
|
||||
|
||||
boolean isBulkEdit = BooleanUtils.isTrue(runBackendStepInput.getValueBoolean("isBulkEdit"));
|
||||
String tableName = runBackendStepInput.getValueString("tableName");
|
||||
BulkLoadTableStructure tableStructure = BulkLoadTableStructureBuilder.buildTableStructure(tableName);
|
||||
BulkLoadTableStructure tableStructure = BulkLoadTableStructureBuilder.buildTableStructure(tableName, isBulkEdit);
|
||||
runBackendStepOutput.addValue("tableStructure", tableStructure);
|
||||
runBackendStepOutput.addValue("isBulkEdit", isBulkEdit);
|
||||
|
||||
boolean needSuggestedMapping = true;
|
||||
if(runBackendStepOutput.getProcessState().getIsStepBack())
|
||||
@ -81,7 +84,7 @@ public class BulkInsertPrepareFileMappingStep implements BackendStep
|
||||
{
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> headerValues = (List<String>) runBackendStepOutput.getValue("headerValues");
|
||||
buildSuggestedMapping(headerValues, getPrepopulatedValues(runBackendStepInput), tableStructure, runBackendStepOutput);
|
||||
buildSuggestedMapping(isBulkEdit, headerValues, getPrepopulatedValues(runBackendStepInput), tableStructure, runBackendStepOutput);
|
||||
}
|
||||
}
|
||||
|
||||
@ -95,8 +98,8 @@ public class BulkInsertPrepareFileMappingStep implements BackendStep
|
||||
String prepopulatedValuesJson = runBackendStepInput.getValueString("prepopulatedValues");
|
||||
if(StringUtils.hasContent(prepopulatedValuesJson))
|
||||
{
|
||||
Map<String, Serializable> rs = new LinkedHashMap<>();
|
||||
JSONObject jsonObject = JsonUtils.toJSONObject(prepopulatedValuesJson);
|
||||
Map<String, Serializable> rs = new LinkedHashMap<>();
|
||||
JSONObject jsonObject = JsonUtils.toJSONObject(prepopulatedValuesJson);
|
||||
for(String key : jsonObject.keySet())
|
||||
{
|
||||
rs.put(key, jsonObject.optString(key, null));
|
||||
@ -112,16 +115,16 @@ public class BulkInsertPrepareFileMappingStep implements BackendStep
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private void buildSuggestedMapping(List<String> headerValues, Map<String, Serializable> prepopulatedValues, BulkLoadTableStructure tableStructure, RunBackendStepOutput runBackendStepOutput)
|
||||
private void buildSuggestedMapping(boolean isBulkEdit, List<String> headerValues, Map<String, Serializable> prepopulatedValues, BulkLoadTableStructure tableStructure, RunBackendStepOutput runBackendStepOutput)
|
||||
{
|
||||
BulkLoadMappingSuggester bulkLoadMappingSuggester = new BulkLoadMappingSuggester();
|
||||
BulkLoadProfile bulkLoadProfile = bulkLoadMappingSuggester.suggestBulkLoadMappingProfile(tableStructure, headerValues);
|
||||
BulkLoadProfile bulkLoadProfile = bulkLoadMappingSuggester.suggestBulkLoadMappingProfile(tableStructure, headerValues, isBulkEdit);
|
||||
|
||||
if(CollectionUtils.nullSafeHasContents(prepopulatedValues))
|
||||
{
|
||||
for(Map.Entry<String, Serializable> entry : prepopulatedValues.entrySet())
|
||||
{
|
||||
String fieldName = entry.getKey();
|
||||
String fieldName = entry.getKey();
|
||||
boolean foundFieldInProfile = false;
|
||||
|
||||
for(BulkLoadProfileField bulkLoadProfileField : bulkLoadProfile.getFieldList())
|
||||
|
@ -65,10 +65,12 @@ public class BulkInsertPrepareFileUploadStep implements BackendStep
|
||||
runBackendStepOutput.addValue("theFile", null);
|
||||
}
|
||||
|
||||
boolean isBulkEdit = runBackendStepInput.getProcessName().endsWith("EditWithFile");
|
||||
String tableName = runBackendStepInput.getValueString("tableName");
|
||||
QTableMetaData table = QContext.getQInstance().getTable(tableName);
|
||||
BulkLoadTableStructure tableStructure = BulkLoadTableStructureBuilder.buildTableStructure(tableName);
|
||||
runBackendStepOutput.addValue("tableStructure", tableStructure);
|
||||
runBackendStepOutput.addValue("isBulkEdit", isBulkEdit);
|
||||
|
||||
List<QFieldMetaData> requiredFields = new ArrayList<>();
|
||||
List<QFieldMetaData> additionalFields = new ArrayList<>();
|
||||
@ -84,6 +86,14 @@ public class BulkInsertPrepareFileUploadStep implements BackendStep
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////
|
||||
// bulk edit allows primary key as a field //
|
||||
/////////////////////////////////////////////
|
||||
if(isBulkEdit)
|
||||
{
|
||||
requiredFields.add(0, table.getField(table.getPrimaryKeyField()));
|
||||
}
|
||||
|
||||
StringBuilder html;
|
||||
String childTableLabels = "";
|
||||
|
||||
@ -96,11 +106,11 @@ public class BulkInsertPrepareFileUploadStep implements BackendStep
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
boolean listFieldsInHelpText = false;
|
||||
|
||||
if(!CollectionUtils.nullSafeHasContents(tableStructure.getAssociations()))
|
||||
if(isBulkEdit || !CollectionUtils.nullSafeHasContents(tableStructure.getAssociations()))
|
||||
{
|
||||
html = new StringBuilder("""
|
||||
<p>Upload either a CSV or Excel (.xlsx) file, with one row for each record you want to
|
||||
insert in the ${tableLabel} table.</p><br />
|
||||
${action} in the ${tableLabel} table.</p><br />
|
||||
|
||||
<p>Your file can contain any number of columns. You will be prompted to map fields from
|
||||
the ${tableLabel} table to columns from your file or default values for all records that
|
||||
@ -204,6 +214,7 @@ public class BulkInsertPrepareFileUploadStep implements BackendStep
|
||||
finishCSV(flatCSV);
|
||||
|
||||
String htmlString = html.toString()
|
||||
.replace("${action}", (isBulkEdit ? "edit" : "insert"))
|
||||
.replace("${tableLabel}", table.getLabel())
|
||||
.replace("${childTableLabels}", childTableLabels)
|
||||
.replace("${flatCSV}", Base64.getEncoder().encodeToString(flatCSV.toString().getBytes(StandardCharsets.UTF_8)))
|
||||
|
@ -113,6 +113,8 @@ public class BulkInsertStepUtils
|
||||
{
|
||||
String layout = runBackendStepInput.getValueString("layout");
|
||||
Boolean hasHeaderRow = runBackendStepInput.getValueBoolean("hasHeaderRow");
|
||||
String keyFields = runBackendStepInput.getValueString("keyFields");
|
||||
Boolean isBulkEdit = runBackendStepInput.getValueBoolean("isBulkEdit");
|
||||
|
||||
ArrayList<BulkLoadProfileField> fieldList = new ArrayList<>();
|
||||
|
||||
@ -127,6 +129,7 @@ public class BulkInsertStepUtils
|
||||
bulkLoadProfileField.setColumnIndex(jsonObject.has("columnIndex") ? jsonObject.getInt("columnIndex") : null);
|
||||
bulkLoadProfileField.setDefaultValue((Serializable) jsonObject.opt("defaultValue"));
|
||||
bulkLoadProfileField.setDoValueMapping(jsonObject.optBoolean("doValueMapping"));
|
||||
bulkLoadProfileField.setClearIfEmpty(jsonObject.optBoolean("clearIfEmpty"));
|
||||
|
||||
if(BooleanUtils.isTrue(bulkLoadProfileField.getDoValueMapping()) && jsonObject.has("valueMappings"))
|
||||
{
|
||||
@ -140,6 +143,8 @@ public class BulkInsertStepUtils
|
||||
}
|
||||
|
||||
BulkLoadProfile bulkLoadProfile = new BulkLoadProfile()
|
||||
.withIsBulkEdit(isBulkEdit)
|
||||
.withKeyFields(keyFields)
|
||||
.withVersion(version)
|
||||
.withFieldList(fieldList)
|
||||
.withHasHeaderRow(hasHeaderRow)
|
||||
@ -213,7 +218,7 @@ public class BulkInsertStepUtils
|
||||
{
|
||||
return (processTracerKeyRecordMessage);
|
||||
}
|
||||
|
||||
|
||||
return (null);
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
@ -32,12 +33,13 @@ import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreInsertCustomizer;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreInsertCustomizer.WhenToRun;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.helpers.UniqueKeyHelper;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
@ -48,6 +50,11 @@ import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutp
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.Status;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.WidgetType;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
|
||||
@ -68,6 +75,9 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ListingHash;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
import org.apache.commons.lang.BooleanUtils;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -75,9 +85,9 @@ import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
*******************************************************************************/
|
||||
public class BulkInsertTransformStep extends AbstractTransformStep
|
||||
{
|
||||
ProcessSummaryLine okSummary = new ProcessSummaryLine(Status.OK);
|
||||
public ProcessSummaryLine okSummary = new ProcessSummaryLine(Status.OK);
|
||||
|
||||
ProcessSummaryWarningsAndErrorsRollup processSummaryWarningsAndErrorsRollup = ProcessSummaryWarningsAndErrorsRollup.build("inserted")
|
||||
public ProcessSummaryWarningsAndErrorsRollup processSummaryWarningsAndErrorsRollup = ProcessSummaryWarningsAndErrorsRollup.build("inserted")
|
||||
.withDoReplaceSingletonCountLinesWithSuffixOnly(false);
|
||||
|
||||
private ListingHash<String, RowValue> errorToExampleRowValueMap = new ListingHash<>();
|
||||
@ -190,6 +200,252 @@ public class BulkInsertTransformStep extends AbstractTransformStep
|
||||
QTableMetaData table = QContext.getQInstance().getTable(runBackendStepInput.getTableName());
|
||||
List<QRecord> records = runBackendStepInput.getRecords();
|
||||
|
||||
if(BooleanUtils.isTrue(runBackendStepInput.getValueBoolean("isBulkEdit")))
|
||||
{
|
||||
handleBulkEdit(runBackendStepInput, runBackendStepOutput, records, table);
|
||||
runBackendStepOutput.addValue("isBulkEdit", true);
|
||||
}
|
||||
else
|
||||
{
|
||||
handleBulkLoad(runBackendStepInput, runBackendStepOutput, records, table);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void handleBulkEdit(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput, List<QRecord> records, QTableMetaData table) throws QException
|
||||
{
|
||||
///////////////////////////////////////////
|
||||
// get the key fields for this bulk edit //
|
||||
///////////////////////////////////////////
|
||||
String keyFieldsString = runBackendStepInput.getValueString("keyFields");
|
||||
List<String> keyFields = Arrays.asList(keyFieldsString.split("\\|"));
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// if the key field is the primary key, then just look up those records //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
List<QRecord> nonMatchingRecords = new ArrayList<>();
|
||||
List<QRecord> oldRecords = new ArrayList<>();
|
||||
List<QRecord> recordsToUpdate = new ArrayList<>();
|
||||
if(keyFields.size() == 1 && table.getPrimaryKeyField().equals(keyFields.get(0)))
|
||||
{
|
||||
recordsToUpdate = records;
|
||||
String primaryKeyName = table.getPrimaryKeyField();
|
||||
List<Serializable> primaryKeys = records.stream().map(record -> record.getValue(primaryKeyName)).toList();
|
||||
oldRecords = new QueryAction().execute(new QueryInput(table.getName()).withFilter(new QQueryFilter(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, primaryKeys)))).getRecords();
|
||||
|
||||
///////////////////////////////////////////
|
||||
// get a set of old records primary keys //
|
||||
///////////////////////////////////////////
|
||||
Set<Serializable> matchedPrimaryKeys = oldRecords.stream()
|
||||
.map(r -> r.getValue(table.getPrimaryKeyField()))
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// iterate over file records and if primary keys dont match, add to the non matching records list //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
for(QRecord record : records)
|
||||
{
|
||||
Serializable recordKey = record.getValue(table.getPrimaryKeyField());
|
||||
if(!matchedPrimaryKeys.contains(recordKey))
|
||||
{
|
||||
nonMatchingRecords.add(record);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Set<Serializable> uniqueIds = new HashSet<>();
|
||||
List<QRecord> potentialRecords = new ArrayList<>();
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if not using the primary key, then we will look up all records for each part of the unique key //
|
||||
// and for each found, if all unique parts match we will add to our list of database records //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
for(String uniqueKeyPart : keyFields)
|
||||
{
|
||||
List<Serializable> values = records.stream().map(record -> record.getValue(uniqueKeyPart)).toList();
|
||||
for(QRecord databaseRecord : new QueryAction().execute(new QueryInput(table.getName()).withFilter(new QQueryFilter(new QFilterCriteria(uniqueKeyPart, QCriteriaOperator.IN, values)))).getRecords())
|
||||
{
|
||||
if(!uniqueIds.contains(databaseRecord.getValue(table.getPrimaryKeyField())))
|
||||
{
|
||||
potentialRecords.add(databaseRecord);
|
||||
uniqueIds.add(databaseRecord.getValue(table.getPrimaryKeyField()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// now iterate over all of the potential records checking each unique fields //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
fileRecordLoop:
|
||||
for(QRecord fileRecord : records)
|
||||
{
|
||||
for(QRecord databaseRecord : potentialRecords)
|
||||
{
|
||||
boolean allMatch = true;
|
||||
|
||||
for(String uniqueKeyPart : keyFields)
|
||||
{
|
||||
if(!Objects.equals(fileRecord.getValue(uniqueKeyPart), databaseRecord.getValue(uniqueKeyPart)))
|
||||
{
|
||||
allMatch = false;
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// if we get here with all matching, update the record from the file's primary key, //
|
||||
// add it to the list to update, and continue looping over file records //
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
if(allMatch)
|
||||
{
|
||||
oldRecords.add(databaseRecord);
|
||||
fileRecord.setValue(table.getPrimaryKeyField(), databaseRecord.getValue(table.getPrimaryKeyField()));
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// iterate over the fields in the bulk load profile, if the value for that field is empty and the value //
|
||||
// of 'clear if empty' is set to true, then update the record to update with the old record's value //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
JSONArray array = new JSONArray(runBackendStepInput.getValueString("fieldListJSON"));
|
||||
for(int i = 0; i < array.length(); i++)
|
||||
{
|
||||
JSONObject jsonObject = array.getJSONObject(i);
|
||||
String fieldName = jsonObject.optString("fieldName");
|
||||
boolean clearIfEmpty = jsonObject.optBoolean("clearIfEmpty");
|
||||
|
||||
if(fileRecord.getValue(fieldName) == null)
|
||||
{
|
||||
if(clearIfEmpty)
|
||||
{
|
||||
fileRecord.setValue(fieldName, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
fileRecord.setValue(fieldName, databaseRecord.getValue(fieldName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
recordsToUpdate.add(fileRecord);
|
||||
continue fileRecordLoop;
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
// if we make it here, that means the record was not found, keep for logging warning //
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
nonMatchingRecords.add(fileRecord);
|
||||
}
|
||||
}
|
||||
|
||||
for(QRecord missingRecord : CollectionUtils.nonNullList(nonMatchingRecords))
|
||||
{
|
||||
String message = "Did not have a matching existing record.";
|
||||
processSummaryWarningsAndErrorsRollup.addError(message, null);
|
||||
addToErrorToExampleRowMap(message, missingRecord);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// set up an insert-input, which will be used as input to the pre-customizer as well as for additional validations //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
UpdateInput updateInput = new UpdateInput(table.getName());
|
||||
updateInput.setInputSource(QInputSource.USER);
|
||||
updateInput.setRecords(recordsToUpdate);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// load the pre-insert customizer and set it up, if there is one //
|
||||
// then we'll run it based on its WhenToRun value //
|
||||
// we do this, in case it needs to, for example, adjust values that //
|
||||
// are part of a unique key //
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
boolean didAlreadyRunCustomizer = false;
|
||||
Optional<TableCustomizerInterface> preUpdateCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.PRE_UPDATE_RECORD.getRole());
|
||||
if(preUpdateCustomizer.isPresent())
|
||||
{
|
||||
List<QRecord> recordsAfterCustomizer = preUpdateCustomizer.get().preUpdate(updateInput, records, true, Optional.of(oldRecords));
|
||||
runBackendStepInput.setRecords(recordsAfterCustomizer);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// so we used to have a comment here asking "do we care if the customizer runs both now, and in the validation below?" //
|
||||
// when implementing Bulk Load V2, we were seeing that some customizers were adding errors to records, both now, and //
|
||||
// when they ran below. so, at that time, we added this boolean, to track and avoid the double-run... //
|
||||
// we could also imagine this being a setting on the pre-insert customizer, similar to its whenToRun attribute... //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
didAlreadyRunCustomizer = true;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
// run all validation from the insert action - in Preview mode (boolean param) //
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
updateInput.setRecords(recordsToUpdate);
|
||||
UpdateAction updateAction = new UpdateAction();
|
||||
updateAction.performValidations(updateInput, Optional.of(recordsToUpdate), didAlreadyRunCustomizer);
|
||||
List<QRecord> validationResultRecords = updateInput.getRecords();
|
||||
|
||||
/////////////////////////////////////////////////////////////////
|
||||
// look at validation results to build process summary results //
|
||||
/////////////////////////////////////////////////////////////////
|
||||
List<QRecord> outputRecords = new ArrayList<>();
|
||||
for(QRecord record : validationResultRecords)
|
||||
{
|
||||
List<QErrorMessage> errorsFromAssociations = getErrorsFromAssociations(record);
|
||||
if(CollectionUtils.nullSafeHasContents(errorsFromAssociations))
|
||||
{
|
||||
List<QErrorMessage> recordErrors = Objects.requireNonNullElseGet(record.getErrors(), () -> new ArrayList<>());
|
||||
recordErrors.addAll(errorsFromAssociations);
|
||||
record.setErrors(recordErrors);
|
||||
}
|
||||
|
||||
if(CollectionUtils.nullSafeHasContents(record.getErrors()))
|
||||
{
|
||||
for(QErrorMessage error : record.getErrors())
|
||||
{
|
||||
if(error instanceof AbstractBulkLoadRollableValueError rollableValueError)
|
||||
{
|
||||
processSummaryWarningsAndErrorsRollup.addError(rollableValueError.getMessageToUseAsProcessSummaryRollupKey(), null);
|
||||
addToErrorToExampleRowValueMap(rollableValueError, record);
|
||||
}
|
||||
else
|
||||
{
|
||||
processSummaryWarningsAndErrorsRollup.addError(error.getMessage(), null);
|
||||
addToErrorToExampleRowMap(error.getMessage(), record);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(CollectionUtils.nullSafeHasContents(record.getWarnings()))
|
||||
{
|
||||
String message = record.getWarnings().get(0).getMessage();
|
||||
processSummaryWarningsAndErrorsRollup.addWarning(message, null);
|
||||
outputRecords.add(record);
|
||||
}
|
||||
else
|
||||
{
|
||||
okSummary.incrementCountAndAddPrimaryKey(null);
|
||||
outputRecords.add(record);
|
||||
|
||||
for(Map.Entry<String, List<QRecord>> entry : CollectionUtils.nonNullMap(record.getAssociatedRecords()).entrySet())
|
||||
{
|
||||
String associationName = entry.getKey();
|
||||
ProcessSummaryLine associationToInsertLine = associationsToInsertSummaries.computeIfAbsent(associationName, x -> new ProcessSummaryLine(Status.OK));
|
||||
associationToInsertLine.incrementCount(CollectionUtils.nonNullList(entry.getValue()).size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
runBackendStepOutput.setRecords(outputRecords);
|
||||
this.rowsProcessed += records.size();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void handleBulkLoad(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput, List<QRecord> records, QTableMetaData table) throws QException
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// set up an insert-input, which will be used as input to the pre-customizer as well as for additional validations //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -209,7 +465,7 @@ public class BulkInsertTransformStep extends AbstractTransformStep
|
||||
Optional<TableCustomizerInterface> preInsertCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.PRE_INSERT_RECORD.getRole());
|
||||
if(preInsertCustomizer.isPresent())
|
||||
{
|
||||
AbstractPreInsertCustomizer.WhenToRun whenToRun = preInsertCustomizer.get().whenToRunPreInsert(insertInput, true);
|
||||
WhenToRun whenToRun = preInsertCustomizer.get().whenToRunPreInsert(insertInput, true);
|
||||
if(WhenToRun.BEFORE_ALL_VALIDATIONS.equals(whenToRun) || WhenToRun.BEFORE_UNIQUE_KEY_CHECKS.equals(whenToRun))
|
||||
{
|
||||
List<QRecord> recordsAfterCustomizer = preInsertCustomizer.get().preInsert(insertInput, records, true);
|
||||
@ -485,11 +741,13 @@ public class BulkInsertTransformStep extends AbstractTransformStep
|
||||
recordsProcessedLine.withPluralFutureMessage("records were");
|
||||
recordsProcessedLine.withPluralPastMessage("records were");
|
||||
|
||||
String noWarningsSuffix = processSummaryWarningsAndErrorsRollup.countWarnings() == 0 ? "" : " with no warnings";
|
||||
okSummary.setSingularFutureMessage(tableLabel + " record will be inserted" + noWarningsSuffix + ".");
|
||||
okSummary.setPluralFutureMessage(tableLabel + " records will be inserted" + noWarningsSuffix + ".");
|
||||
okSummary.setSingularPastMessage(tableLabel + " record was inserted" + noWarningsSuffix + ".");
|
||||
okSummary.setPluralPastMessage(tableLabel + " records were inserted" + noWarningsSuffix + ".");
|
||||
boolean isBulkEdit = BooleanUtils.isTrue(runBackendStepOutput.getValueBoolean("isBulkEdit"));
|
||||
String action = isBulkEdit ? "updated" : "inserted";
|
||||
String noWarningsSuffix = processSummaryWarningsAndErrorsRollup.countWarnings() == 0 ? "" : " with no warnings";
|
||||
okSummary.setSingularFutureMessage(tableLabel + " record will be " + action + noWarningsSuffix + ".");
|
||||
okSummary.setPluralFutureMessage(tableLabel + " records will be " + action + noWarningsSuffix + ".");
|
||||
okSummary.setSingularPastMessage(tableLabel + " record was " + action + noWarningsSuffix + ".");
|
||||
okSummary.setPluralPastMessage(tableLabel + " records were " + action + noWarningsSuffix + ".");
|
||||
okSummary.pickMessage(isForResultScreen);
|
||||
okSummary.addSelfToListIfAnyCount(rs);
|
||||
|
||||
@ -502,10 +760,10 @@ public class BulkInsertTransformStep extends AbstractTransformStep
|
||||
String associationLabel = associationTable.getLabel();
|
||||
|
||||
ProcessSummaryLine line = entry.getValue();
|
||||
line.setSingularFutureMessage(associationLabel + " record will be inserted.");
|
||||
line.setPluralFutureMessage(associationLabel + " records will be inserted.");
|
||||
line.setSingularPastMessage(associationLabel + " record was inserted.");
|
||||
line.setPluralPastMessage(associationLabel + " records were inserted.");
|
||||
line.setSingularFutureMessage(associationLabel + " record will be " + action + ".");
|
||||
line.setPluralFutureMessage(associationLabel + " records will be " + action + ".");
|
||||
line.setSingularPastMessage(associationLabel + " record was " + action + ".");
|
||||
line.setPluralPastMessage(associationLabel + " records were " + action + ".");
|
||||
line.pickMessage(isForResultScreen);
|
||||
line.addSelfToListIfAnyCount(rs);
|
||||
}
|
||||
@ -518,8 +776,8 @@ public class BulkInsertTransformStep extends AbstractTransformStep
|
||||
|
||||
ukErrorSummary
|
||||
.withMessageSuffix(" inserted, because of duplicate values in a unique key on the fields (" + uniqueKey.getDescription(table) + "), with values"
|
||||
+ (ukErrorSummary.areThereMoreSampleValues ? " such as: " : ": ")
|
||||
+ StringUtils.joinWithCommasAndAnd(new ArrayList<>(ukErrorSummary.sampleValues)))
|
||||
+ (ukErrorSummary.areThereMoreSampleValues ? " such as: " : ": ")
|
||||
+ StringUtils.joinWithCommasAndAnd(new ArrayList<>(ukErrorSummary.sampleValues)))
|
||||
|
||||
.withSingularFutureMessage(" record will not be")
|
||||
.withPluralFutureMessage(" records will not be")
|
||||
|
@ -54,7 +54,7 @@ public class BulkLoadMappingSuggester
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public BulkLoadProfile suggestBulkLoadMappingProfile(BulkLoadTableStructure tableStructure, List<String> headerRow)
|
||||
public BulkLoadProfile suggestBulkLoadMappingProfile(BulkLoadTableStructure tableStructure, List<String> headerRow, boolean isBulkEdit)
|
||||
{
|
||||
massagedHeadersWithoutNumbersToIndexMap = new LinkedHashMap<>();
|
||||
for(int i = 0; i < headerRow.size(); i++)
|
||||
@ -90,6 +90,7 @@ public class BulkLoadMappingSuggester
|
||||
.withVersion("v1")
|
||||
.withLayout(layout)
|
||||
.withHasHeaderRow(true)
|
||||
.withIsBulkEdit(isBulkEdit)
|
||||
.withFieldList(fieldList);
|
||||
|
||||
return (bulkLoadProfile);
|
||||
|
@ -25,12 +25,19 @@ package com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.map
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceInput;
|
||||
import com.kingsrook.qqq.backend.core.model.bulk.TableKeyFieldsPossibleValueSource;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.model.BulkLoadTableStructure;
|
||||
@ -44,12 +51,16 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
*******************************************************************************/
|
||||
public class BulkLoadTableStructureBuilder
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(BulkLoadTableStructureBuilder.class);
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public static BulkLoadTableStructure buildTableStructure(String tableName)
|
||||
{
|
||||
return (buildTableStructure(tableName, null, null));
|
||||
return (buildTableStructure(tableName, null, null, false));
|
||||
}
|
||||
|
||||
|
||||
@ -57,13 +68,24 @@ public class BulkLoadTableStructureBuilder
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private static BulkLoadTableStructure buildTableStructure(String tableName, Association association, String parentAssociationPath)
|
||||
public static BulkLoadTableStructure buildTableStructure(String tableName, Boolean isBulkEdit)
|
||||
{
|
||||
return (buildTableStructure(tableName, null, null, isBulkEdit));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private static BulkLoadTableStructure buildTableStructure(String tableName, Association association, String parentAssociationPath, Boolean isBulkEdit)
|
||||
{
|
||||
QTableMetaData table = QContext.getQInstance().getTable(tableName);
|
||||
|
||||
BulkLoadTableStructure tableStructure = new BulkLoadTableStructure();
|
||||
tableStructure.setTableName(tableName);
|
||||
tableStructure.setLabel(table.getLabel());
|
||||
tableStructure.setIsBulkEdit(isBulkEdit);
|
||||
|
||||
Set<String> associationJoinFieldNamesToExclude = new HashSet<>();
|
||||
|
||||
@ -119,6 +141,30 @@ public class BulkLoadTableStructureBuilder
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
// for bulk edit, users can use the primary key field //
|
||||
////////////////////////////////////////////////////////
|
||||
if(isBulkEdit)
|
||||
{
|
||||
fields.add(table.getField(table.getPrimaryKeyField()));
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// also make available what key fields are available for this table //
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
try
|
||||
{
|
||||
SearchPossibleValueSourceInput input = new SearchPossibleValueSourceInput()
|
||||
.withPossibleValueSourceName("tableKeyFields")
|
||||
.withPathParamMap(Map.of("processName", tableName + ".bulkEditWithFile"));
|
||||
List<QPossibleValue<String>> search = new TableKeyFieldsPossibleValueSource().search(input);
|
||||
tableStructure.setPossibleKeyFields(new ArrayList<>(search.stream().map(QPossibleValue::getId).toList()));
|
||||
}
|
||||
catch(QException qe)
|
||||
{
|
||||
LOG.warn("Unable to retrieve possible key fields for table [" + tableName + "]", qe);
|
||||
}
|
||||
}
|
||||
|
||||
fields.sort(Comparator.comparing(f -> ObjectUtils.requireNonNullElse(f.getLabel(), f.getName(), "")));
|
||||
|
||||
for(Association childAssociation : CollectionUtils.nonNullList(table.getAssociations()))
|
||||
@ -131,8 +177,8 @@ public class BulkLoadTableStructureBuilder
|
||||
{
|
||||
String nextLevelPath =
|
||||
(StringUtils.hasContent(parentAssociationPath) ? parentAssociationPath + "." : "")
|
||||
+ (association != null ? association.getName() : "");
|
||||
BulkLoadTableStructure associatedStructure = buildTableStructure(childAssociation.getAssociatedTableName(), childAssociation, nextLevelPath);
|
||||
+ (association != null ? association.getName() : "");
|
||||
BulkLoadTableStructure associatedStructure = buildTableStructure(childAssociation.getAssociatedTableName(), childAssociation, nextLevelPath, isBulkEdit);
|
||||
tableStructure.addAssociation(associatedStructure);
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +37,8 @@ public class BulkLoadProfile implements Serializable
|
||||
private Boolean hasHeaderRow;
|
||||
private String layout;
|
||||
private String version;
|
||||
private Boolean isBulkEdit;
|
||||
private String keyFields;
|
||||
|
||||
|
||||
|
||||
@ -132,6 +134,7 @@ public class BulkLoadProfile implements Serializable
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for version
|
||||
*******************************************************************************/
|
||||
@ -162,4 +165,65 @@ public class BulkLoadProfile implements Serializable
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for isBulkEdit
|
||||
*******************************************************************************/
|
||||
public Boolean getIsBulkEdit()
|
||||
{
|
||||
return (this.isBulkEdit);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for isBulkEdit
|
||||
*******************************************************************************/
|
||||
public void setIsBulkEdit(Boolean isBulkEdit)
|
||||
{
|
||||
this.isBulkEdit = isBulkEdit;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for isBulkEdit
|
||||
*******************************************************************************/
|
||||
public BulkLoadProfile withIsBulkEdit(Boolean isBulkEdit)
|
||||
{
|
||||
this.isBulkEdit = isBulkEdit;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for keyFields
|
||||
*******************************************************************************/
|
||||
public String getKeyFields()
|
||||
{
|
||||
return (this.keyFields);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for keyFields
|
||||
*******************************************************************************/
|
||||
public void setKeyFields(String keyFields)
|
||||
{
|
||||
this.keyFields = keyFields;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for keyFields
|
||||
*******************************************************************************/
|
||||
public BulkLoadProfile withKeyFields(String keyFields)
|
||||
{
|
||||
this.keyFields = keyFields;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -35,6 +35,7 @@ public class BulkLoadProfileField
|
||||
private Integer columnIndex;
|
||||
private String headerName;
|
||||
private Serializable defaultValue;
|
||||
private Boolean clearIfEmpty;
|
||||
private Boolean doValueMapping;
|
||||
private Map<String, Serializable> valueMappings;
|
||||
|
||||
@ -194,6 +195,7 @@ public class BulkLoadProfileField
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for headerName
|
||||
*******************************************************************************/
|
||||
@ -224,4 +226,34 @@ public class BulkLoadProfileField
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for clearIfEmpty
|
||||
*******************************************************************************/
|
||||
public Boolean getClearIfEmpty()
|
||||
{
|
||||
return (this.clearIfEmpty);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for clearIfEmpty
|
||||
*******************************************************************************/
|
||||
public void setClearIfEmpty(Boolean clearIfEmpty)
|
||||
{
|
||||
this.clearIfEmpty = clearIfEmpty;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for clearIfEmpty
|
||||
*******************************************************************************/
|
||||
public BulkLoadProfileField withClearIfEmpty(Boolean clearIfEmpty)
|
||||
{
|
||||
this.clearIfEmpty = clearIfEmpty;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -35,9 +35,12 @@ public class BulkLoadTableStructure implements Serializable
|
||||
private boolean isMain;
|
||||
private boolean isMany;
|
||||
|
||||
private String tableName;
|
||||
private String label;
|
||||
private String associationPath; // null/empty for main table, then associationName for a child, associationName.associationName for a grandchild
|
||||
private String tableName;
|
||||
private String label;
|
||||
private String associationPath; // null/empty for main table, then associationName for a child, associationName.associationName for a grandchild
|
||||
private Boolean isBulkEdit;
|
||||
private String keyFields;
|
||||
private ArrayList<String> possibleKeyFields;
|
||||
|
||||
private ArrayList<QFieldMetaData> fields; // mmm, not marked as serializable (at this time) - is okay?
|
||||
private ArrayList<BulkLoadTableStructure> associations;
|
||||
@ -272,4 +275,98 @@ public class BulkLoadTableStructure implements Serializable
|
||||
}
|
||||
this.associations.add(association);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for isBulkEdit
|
||||
*******************************************************************************/
|
||||
public Boolean getIsBulkEdit()
|
||||
{
|
||||
return (this.isBulkEdit);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for isBulkEdit
|
||||
*******************************************************************************/
|
||||
public void setIsBulkEdit(Boolean isBulkEdit)
|
||||
{
|
||||
this.isBulkEdit = isBulkEdit;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for isBulkEdit
|
||||
*******************************************************************************/
|
||||
public BulkLoadTableStructure withIsBulkEdit(Boolean isBulkEdit)
|
||||
{
|
||||
this.isBulkEdit = isBulkEdit;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for keyFields
|
||||
*******************************************************************************/
|
||||
public String getKeyFields()
|
||||
{
|
||||
return (this.keyFields);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for keyFields
|
||||
*******************************************************************************/
|
||||
public void setKeyFields(String keyFields)
|
||||
{
|
||||
this.keyFields = keyFields;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for keyFields
|
||||
*******************************************************************************/
|
||||
public BulkLoadTableStructure withKeyFields(String keyFields)
|
||||
{
|
||||
this.keyFields = keyFields;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for possibleKeyFields
|
||||
*******************************************************************************/
|
||||
public ArrayList<String> getPossibleKeyFields()
|
||||
{
|
||||
return (this.possibleKeyFields);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for possibleKeyFields
|
||||
*******************************************************************************/
|
||||
public void setPossibleKeyFields(ArrayList<String> possibleKeyFields)
|
||||
{
|
||||
this.possibleKeyFields = possibleKeyFields;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for possibleKeyFields
|
||||
*******************************************************************************/
|
||||
public BulkLoadTableStructure withPossibleKeyFields(ArrayList<String> possibleKeyFields)
|
||||
{
|
||||
this.possibleKeyFields = possibleKeyFields;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -45,6 +45,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.savedbulkloadprofiles.SavedBulkLoadProfile;
|
||||
import org.apache.commons.lang.BooleanUtils;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
@ -102,13 +103,29 @@ public class QuerySavedBulkLoadProfileProcess implements BackendStep
|
||||
}
|
||||
else
|
||||
{
|
||||
String tableName = runBackendStepInput.getValueString("tableName");
|
||||
String tableName = runBackendStepInput.getValueString("tableName");
|
||||
boolean isBulkEdit = BooleanUtils.isTrue(runBackendStepInput.getValueBoolean("isBulkEdit"));
|
||||
|
||||
QueryInput input = new QueryInput();
|
||||
input.setTableName(SavedBulkLoadProfile.TABLE_NAME);
|
||||
input.setFilter(new QQueryFilter()
|
||||
|
||||
QQueryFilter filter = new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("tableName", QCriteriaOperator.EQUALS, tableName))
|
||||
.withOrderBy(new QFilterOrderBy("label")));
|
||||
.withOrderBy(new QFilterOrderBy("label"));
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// account for nulls here, so if is bulk edit, only look for true, //
|
||||
// otherwise look for nulls or not equal to true //
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
if(isBulkEdit)
|
||||
{
|
||||
filter.withCriteria(new QFilterCriteria("isBulkEdit", QCriteriaOperator.EQUALS, true));
|
||||
}
|
||||
else
|
||||
{
|
||||
filter.withCriteria(new QFilterCriteria("isBulkEdit", QCriteriaOperator.NOT_EQUALS_OR_IS_NULL, true));
|
||||
}
|
||||
input.setFilter(filter);
|
||||
|
||||
QueryOutput output = new QueryAction().execute(input);
|
||||
runBackendStepOutput.setRecords(output.getRecords());
|
||||
|
@ -50,6 +50,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaD
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.savedbulkloadprofiles.SavedBulkLoadProfile;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import org.apache.commons.lang.BooleanUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -87,9 +88,10 @@ public class StoreSavedBulkLoadProfileProcess implements BackendStep
|
||||
|
||||
try
|
||||
{
|
||||
String userId = QContext.getQSession().getUser().getIdReference();
|
||||
String tableName = runBackendStepInput.getValueString("tableName");
|
||||
String label = runBackendStepInput.getValueString("label");
|
||||
String userId = QContext.getQSession().getUser().getIdReference();
|
||||
String tableName = runBackendStepInput.getValueString("tableName");
|
||||
String label = runBackendStepInput.getValueString("label");
|
||||
Boolean isBulkEdit = BooleanUtils.isTrue(runBackendStepInput.getValueBoolean("isBulkEdit"));
|
||||
|
||||
String mappingJson = processMappingJson(runBackendStepInput.getValueString("mappingJson"));
|
||||
|
||||
@ -98,6 +100,7 @@ public class StoreSavedBulkLoadProfileProcess implements BackendStep
|
||||
.withValue("mappingJson", mappingJson)
|
||||
.withValue("label", label)
|
||||
.withValue("tableName", tableName)
|
||||
.withValue("isBulkEdit", isBulkEdit)
|
||||
.withValue("userId", userId);
|
||||
|
||||
List<QRecord> savedBulkLoadProfileList;
|
||||
|
@ -250,7 +250,7 @@ class MetaDataActionTest extends BaseTest
|
||||
// with several permissions set, we should see some things, and they should have permissions turned on //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
assertEquals(Set.of("person"), result.getTables().keySet());
|
||||
assertEquals(Set.of("increaseBirthdate", "runShapesPersonReport", "person.bulkInsert", "person.bulkEdit", "person.bulkDelete"), result.getProcesses().keySet());
|
||||
assertEquals(Set.of("increaseBirthdate", "runShapesPersonReport", "person.bulkInsert", "person.bulkEdit", "person.bulkEditWithFile", "person.bulkDelete"), result.getProcesses().keySet());
|
||||
assertEquals(Set.of("shapesPersonReport", "personJoinShapeReport", "simplePersonReport"), result.getReports().keySet());
|
||||
assertEquals(Set.of("PersonsByCreateDateBarChart"), result.getWidgets().keySet());
|
||||
|
||||
@ -286,7 +286,7 @@ class MetaDataActionTest extends BaseTest
|
||||
|
||||
assertEquals(Set.of("person", "personFile", "personMemory"), result.getTables().keySet());
|
||||
|
||||
assertEquals(Set.of("increaseBirthdate", "personFile.bulkInsert", "personFile.bulkEdit", "personFile.bulkDelete", "personMemory.bulkInsert", "personMemory.bulkEdit", "personMemory.bulkDelete"), result.getProcesses().keySet());
|
||||
assertEquals(Set.of("increaseBirthdate", "personFile.bulkInsert", "personFile.bulkEdit", "personFile.bulkEditWithFile", "personFile.bulkDelete", "personMemory.bulkInsert", "personMemory.bulkEdit", "personMemory.bulkEditWithFile", "personMemory.bulkDelete"), result.getProcesses().keySet());
|
||||
assertEquals(Set.of(), result.getReports().keySet());
|
||||
assertEquals(Set.of(), result.getWidgets().keySet());
|
||||
|
||||
@ -333,7 +333,7 @@ class MetaDataActionTest extends BaseTest
|
||||
MetaDataOutput result = new MetaDataAction().execute(new MetaDataInput());
|
||||
|
||||
assertEquals(Set.of("person", "personFile", "personMemory"), result.getTables().keySet());
|
||||
assertEquals(Set.of("increaseBirthdate", "personFile.bulkInsert", "personFile.bulkEdit", "personMemory.bulkDelete"), result.getProcesses().keySet());
|
||||
assertEquals(Set.of("increaseBirthdate", "personFile.bulkInsert", "personFile.bulkEdit", "personFile.bulkEditWithFile", "personMemory.bulkDelete"), result.getProcesses().keySet());
|
||||
assertEquals(Set.of(), result.getReports().keySet());
|
||||
assertEquals(Set.of(), result.getWidgets().keySet());
|
||||
|
||||
|
@ -0,0 +1,519 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.processes.implementations.bulk.editwithfile;
|
||||
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import com.kingsrook.qqq.backend.core.BaseTest;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreInsertCustomizer;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.StorageAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryAssert;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLine;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLineInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.Status;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.storage.StorageInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage;
|
||||
import com.kingsrook.qqq.backend.core.model.statusmessages.QWarningMessage;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertFullProcessTest;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.model.BulkLoadProfile;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.model.BulkLoadProfileField;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess;
|
||||
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.TestUtils;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Unit test for full bulk insert process
|
||||
*******************************************************************************/
|
||||
class BulkEditWithFileFullProcessTest extends BaseTest
|
||||
{
|
||||
private static final String defaultEmail = "noone@kingsrook.com";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@BeforeEach
|
||||
@AfterEach
|
||||
void beforeAndAfterEach()
|
||||
{
|
||||
MemoryRecordStore.getInstance().reset();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static String getPersonCsvRow1()
|
||||
{
|
||||
return ("""
|
||||
"1","2021-10-26 14:39:37","2021-10-26 14:39:37","Jehn","Doe","1980-01-01","john@doe.com","Missouri",24
|
||||
""");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static String getPersonCsvRow2()
|
||||
{
|
||||
return ("""
|
||||
"2","2021-10-26 14:39:37","2021-10-26 14:39:37","Jyne","Doe","1981-01-01","john@doe.com","Illinois",
|
||||
""");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static String getPersonCsvHeaderUsingLabels()
|
||||
{
|
||||
return ("""
|
||||
"Id","Create Date","Modify Date","First Name","Last Name","Birth Date","Email","Home State",noOfShoes
|
||||
""");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
public void test() throws Exception
|
||||
{
|
||||
/////////////////////////////////////////////
|
||||
// use the bulk insert test to insert data //
|
||||
/////////////////////////////////////////////
|
||||
new BulkInsertFullProcessTest().test();
|
||||
assertThat(TestUtils.queryTable(TestUtils.TABLE_NAME_PERSON_MEMORY)).isNotEmpty();
|
||||
|
||||
/////////////////////////////////////////////////////////
|
||||
// start the process - expect to go to the upload step //
|
||||
/////////////////////////////////////////////////////////
|
||||
RunProcessInput runProcessInput = new RunProcessInput();
|
||||
runProcessInput.addValue("keyFields", "id");
|
||||
runProcessInput.addValue("isBulkEdit", "true");
|
||||
RunProcessOutput runProcessOutput = startProcess(runProcessInput);
|
||||
String processUUID = runProcessOutput.getProcessUUID();
|
||||
assertThat(runProcessOutput.getProcessState().getNextStepName()).isPresent().get().isEqualTo("upload");
|
||||
|
||||
//////////////////////////
|
||||
// continue post-upload //
|
||||
//////////////////////////
|
||||
runProcessOutput = continueProcessPostUpload(runProcessInput, processUUID, simulateFileUpload(2));
|
||||
assertEquals(List.of("Id", "Create Date", "Modify Date", "First Name", "Last Name", "Birth Date", "Email", "Home State", "noOfShoes"), runProcessOutput.getValue("headerValues"));
|
||||
assertEquals(List.of("A", "B", "C", "D", "E", "F", "G", "H", "I"), runProcessOutput.getValue("headerLetters"));
|
||||
|
||||
//////////////////////////////////////////////////////
|
||||
// assert about the suggested mapping that was done //
|
||||
//////////////////////////////////////////////////////
|
||||
Serializable bulkLoadProfile = runProcessOutput.getValue("bulkLoadProfile");
|
||||
assertThat(bulkLoadProfile).isInstanceOf(BulkLoadProfile.class);
|
||||
assertThat(((BulkLoadProfile) bulkLoadProfile).getFieldList()).hasSizeGreaterThan(5);
|
||||
assertEquals("id", ((BulkLoadProfile) bulkLoadProfile).getFieldList().get(0).getFieldName());
|
||||
assertEquals(0, ((BulkLoadProfile) bulkLoadProfile).getFieldList().get(0).getColumnIndex());
|
||||
assertEquals("firstName", ((BulkLoadProfile) bulkLoadProfile).getFieldList().get(1).getFieldName());
|
||||
assertEquals(3, ((BulkLoadProfile) bulkLoadProfile).getFieldList().get(1).getColumnIndex());
|
||||
assertEquals("lastName", ((BulkLoadProfile) bulkLoadProfile).getFieldList().get(2).getFieldName());
|
||||
assertEquals(4, ((BulkLoadProfile) bulkLoadProfile).getFieldList().get(2).getColumnIndex());
|
||||
assertEquals("birthDate", ((BulkLoadProfile) bulkLoadProfile).getFieldList().get(3).getFieldName());
|
||||
assertEquals(5, ((BulkLoadProfile) bulkLoadProfile).getFieldList().get(3).getColumnIndex());
|
||||
|
||||
assertThat(runProcessOutput.getProcessState().getNextStepName()).isPresent().get().isEqualTo("fileMapping");
|
||||
|
||||
////////////////////////////////
|
||||
// continue post file-mapping //
|
||||
////////////////////////////////
|
||||
runProcessOutput = continueProcessPostFileMapping(runProcessInput);
|
||||
Serializable valueMappingField = runProcessOutput.getValue("valueMappingField");
|
||||
assertThat(valueMappingField).isInstanceOf(QFrontendFieldMetaData.class);
|
||||
assertEquals("homeStateId", ((QFrontendFieldMetaData) valueMappingField).getName());
|
||||
assertEquals(List.of("Missouri", "Illinois"), runProcessOutput.getValue("fileValues"));
|
||||
assertEquals(List.of("homeStateId"), runProcessOutput.getValue("fieldNamesToDoValueMapping"));
|
||||
assertEquals(Map.of(1, "IL"), runProcessOutput.getValue("mappedValueLabels"));
|
||||
assertEquals(0, runProcessOutput.getValue("valueMappingFieldIndex"));
|
||||
assertThat(runProcessOutput.getProcessState().getNextStepName()).isPresent().get().isEqualTo("valueMapping");
|
||||
|
||||
/////////////////////////////////
|
||||
// continue post value-mapping //
|
||||
/////////////////////////////////
|
||||
runProcessOutput = continueProcessPostValueMapping(runProcessInput);
|
||||
assertThat(runProcessOutput.getProcessState().getNextStepName()).isPresent().get().isEqualTo("review");
|
||||
|
||||
/////////////////////////////////
|
||||
// continue post review screen //
|
||||
/////////////////////////////////
|
||||
runProcessOutput = continueProcessPostReviewScreen(runProcessInput);
|
||||
assertThat(runProcessOutput.getRecords()).hasSize(2);
|
||||
assertThat(runProcessOutput.getProcessState().getNextStepName()).isPresent().get().isEqualTo("result");
|
||||
assertThat(runProcessOutput.getValues().get(StreamedETLWithFrontendProcess.FIELD_PROCESS_SUMMARY)).isNotNull().isInstanceOf(List.class);
|
||||
assertThat(runProcessOutput.getException()).isEmpty();
|
||||
|
||||
ProcessSummaryLineInterface okLine = ProcessSummaryAssert.assertThat(runProcessOutput)
|
||||
.hasLineWithMessageContaining("Person Memory records were edited")
|
||||
.hasStatus(Status.OK)
|
||||
.hasCount(2)
|
||||
.getLine();
|
||||
assertEquals(List.of(1, 2), ((ProcessSummaryLine) okLine).getPrimaryKeys());
|
||||
|
||||
////////////////////////////////////
|
||||
// query for the inserted records //
|
||||
////////////////////////////////////
|
||||
List<QRecord> records = TestUtils.queryTable(TestUtils.TABLE_NAME_PERSON_MEMORY);
|
||||
|
||||
assertEquals("Jehn", records.get(0).getValueString("firstName"));
|
||||
assertEquals("Jyne", records.get(1).getValueString("firstName"));
|
||||
|
||||
assertNotNull(records.get(0).getValue("id"));
|
||||
assertNotNull(records.get(1).getValue("id"));
|
||||
assertEquals(1, records.get(0).getValue("id"));
|
||||
assertEquals(2, records.get(1).getValue("id"));
|
||||
|
||||
assertEquals(2, records.get(0).getValueInteger("homeStateId"));
|
||||
assertEquals(1, records.get(1).getValueInteger("homeStateId"));
|
||||
|
||||
assertEquals(defaultEmail, records.get(0).getValueString("email"));
|
||||
assertEquals(defaultEmail, records.get(1).getValueString("email"));
|
||||
|
||||
assertEquals(24, records.get(0).getValueInteger("noOfShoes"));
|
||||
assertNull(records.get(1).getValue("noOfShoes"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testSummaryLinePrimaryKeys() throws Exception
|
||||
{
|
||||
/////////////////////////////////////////////
|
||||
// use the bulk insert test to insert data //
|
||||
/////////////////////////////////////////////
|
||||
new BulkInsertFullProcessTest().testSummaryLinePrimaryKeys();
|
||||
assertThat(TestUtils.queryTable(TestUtils.TABLE_NAME_PERSON_MEMORY)).isNotEmpty();
|
||||
|
||||
QContext.getQInstance().getTable(TestUtils.TABLE_NAME_PERSON_MEMORY)
|
||||
.withCustomizer(TableCustomizers.PRE_UPDATE_RECORD, new QCodeReference(PersonWarnOrErrorCustomizer.class));
|
||||
|
||||
/////////////////////////////////////////////////////////
|
||||
// start the process - expect to go to the upload step //
|
||||
/////////////////////////////////////////////////////////
|
||||
RunProcessInput runProcessInput = new RunProcessInput();
|
||||
RunProcessOutput runProcessOutput = startProcess(runProcessInput);
|
||||
String processUUID = runProcessOutput.getProcessUUID();
|
||||
|
||||
continueProcessPostUpload(runProcessInput, processUUID, simulateFileUploadForWarningCase());
|
||||
continueProcessPostFileMapping(runProcessInput);
|
||||
continueProcessPostValueMapping(runProcessInput);
|
||||
runProcessOutput = continueProcessPostReviewScreen(runProcessInput);
|
||||
|
||||
ProcessSummaryLineInterface okLine = ProcessSummaryAssert.assertThat(runProcessOutput)
|
||||
.hasLineWithMessageContaining("Person Memory records were edited")
|
||||
.hasStatus(Status.OK)
|
||||
.hasCount(4)
|
||||
.getLine();
|
||||
assertEquals(List.of(1, 2, 3, 4), ((ProcessSummaryLine) okLine).getPrimaryKeys());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testSummaryLineErrors() throws Exception
|
||||
{
|
||||
/////////////////////////////////////////////
|
||||
// use the bulk insert test to insert data //
|
||||
/////////////////////////////////////////////
|
||||
new BulkInsertFullProcessTest().testSummaryLineErrors();
|
||||
assertThat(TestUtils.queryTable(TestUtils.TABLE_NAME_PERSON_MEMORY)).isNotEmpty();
|
||||
|
||||
QContext.getQInstance().getTable(TestUtils.TABLE_NAME_PERSON_MEMORY)
|
||||
.withCustomizer(TableCustomizers.PRE_UPDATE_RECORD, new QCodeReference(PersonWarnOrErrorCustomizer.class));
|
||||
|
||||
/////////////////////////////////////////////////////////
|
||||
// start the process - expect to go to the upload step //
|
||||
/////////////////////////////////////////////////////////
|
||||
RunProcessInput runProcessInput = new RunProcessInput();
|
||||
RunProcessOutput runProcessOutput = startProcess(runProcessInput);
|
||||
String processUUID = runProcessOutput.getProcessUUID();
|
||||
|
||||
continueProcessPostUpload(runProcessInput, processUUID, simulateFileUploadForErrorCase());
|
||||
continueProcessPostFileMapping(runProcessInput);
|
||||
continueProcessPostValueMapping(runProcessInput);
|
||||
runProcessOutput = continueProcessPostReviewScreen(runProcessInput);
|
||||
|
||||
ProcessSummaryAssert.assertThat(runProcessOutput).hasLineWithMessageContaining("Person Memory record was edited.").hasStatus(Status.OK).hasCount(1);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testOneRow() throws Exception
|
||||
{
|
||||
/////////////////////////////////////////////
|
||||
// use the bulk insert test to insert data //
|
||||
/////////////////////////////////////////////
|
||||
new BulkInsertFullProcessTest().testSummaryLineErrors();
|
||||
assertThat(TestUtils.queryTable(TestUtils.TABLE_NAME_PERSON_MEMORY)).isNotEmpty();
|
||||
|
||||
RunProcessInput runProcessInput = new RunProcessInput();
|
||||
RunProcessOutput runProcessOutput = startProcess(runProcessInput);
|
||||
String processUUID = runProcessOutput.getProcessUUID();
|
||||
|
||||
continueProcessPostUpload(runProcessInput, processUUID, simulateFileUpload(1));
|
||||
continueProcessPostFileMapping(runProcessInput);
|
||||
continueProcessPostValueMapping(runProcessInput);
|
||||
runProcessOutput = continueProcessPostReviewScreen(runProcessInput);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// all that just so we can make sure this message is right (because it was wrong when we first wrote it, lol) //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
ProcessSummaryAssert.assertThat(runProcessOutput).hasLineWithMessageContaining("Person Memory record was edited.").hasStatus(Status.OK).hasCount(1);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private static RunProcessOutput continueProcessPostReviewScreen(RunProcessInput runProcessInput) throws QException
|
||||
{
|
||||
RunProcessOutput runProcessOutput;
|
||||
runProcessInput.setStartAfterStep("review");
|
||||
addProfileToRunProcessInput(runProcessInput);
|
||||
runProcessOutput = new RunProcessAction().execute(runProcessInput);
|
||||
return runProcessOutput;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private static RunProcessOutput continueProcessPostValueMapping(RunProcessInput runProcessInput) throws QException
|
||||
{
|
||||
runProcessInput.setStartAfterStep("valueMapping");
|
||||
runProcessInput.addValue("mappedValuesJSON", JsonUtils.toJson(Map.of("Illinois", 1, "Missouri", 2)));
|
||||
addProfileToRunProcessInput(runProcessInput);
|
||||
RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput);
|
||||
return (runProcessOutput);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private static RunProcessOutput continueProcessPostFileMapping(RunProcessInput runProcessInput) throws QException
|
||||
{
|
||||
RunProcessOutput runProcessOutput;
|
||||
runProcessInput.setStartAfterStep("fileMapping");
|
||||
addProfileToRunProcessInput(runProcessInput);
|
||||
runProcessOutput = new RunProcessAction().execute(runProcessInput);
|
||||
return runProcessOutput;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private static RunProcessOutput continueProcessPostUpload(RunProcessInput runProcessInput, String processUUID, StorageInput storageInput) throws QException
|
||||
{
|
||||
runProcessInput.setProcessUUID(processUUID);
|
||||
runProcessInput.setStartAfterStep("upload");
|
||||
runProcessInput.addValue("theFile", new ArrayList<>(List.of(storageInput)));
|
||||
RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput);
|
||||
return (runProcessOutput);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private static StorageInput simulateFileUpload(int noOfRows) throws Exception
|
||||
{
|
||||
String storageReference = UUID.randomUUID() + ".csv";
|
||||
StorageInput storageInput = new StorageInput(TestUtils.TABLE_NAME_MEMORY_STORAGE).withReference(storageReference);
|
||||
try(OutputStream outputStream = new StorageAction().createOutputStream(storageInput))
|
||||
{
|
||||
outputStream.write((getPersonCsvHeaderUsingLabels() + getPersonCsvRow1() + (noOfRows == 2 ? getPersonCsvRow2() : "")).getBytes());
|
||||
}
|
||||
return storageInput;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private static StorageInput simulateFileUploadForWarningCase() throws Exception
|
||||
{
|
||||
String storageReference = UUID.randomUUID() + ".csv";
|
||||
StorageInput storageInput = new StorageInput(TestUtils.TABLE_NAME_MEMORY_STORAGE).withReference(storageReference);
|
||||
try(OutputStream outputStream = new StorageAction().createOutputStream(storageInput))
|
||||
{
|
||||
outputStream.write((getPersonCsvHeaderUsingLabels() + """
|
||||
"1","2021-10-26 14:39:37","2021-10-26 14:39:37","John","Doe","1980-01-01","john@doe.com","Missouri",42
|
||||
"2","2021-10-26 14:39:37","2021-10-26 14:39:37","Tornado warning","Doe","1980-01-01","john@doe.com","Missouri",42
|
||||
"3","2021-10-26 14:39:37","2021-10-26 14:39:37","Tornado warning","Doey","1980-01-01","john@doe.com","Missouri",42
|
||||
"4","2021-10-26 14:39:37","2021-10-26 14:39:37","Hurricane warning","Doe","1980-01-01","john@doe.com","Missouri",42
|
||||
""").getBytes());
|
||||
}
|
||||
return storageInput;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private static StorageInput simulateFileUploadForErrorCase() throws Exception
|
||||
{
|
||||
String storageReference = UUID.randomUUID() + ".csv";
|
||||
StorageInput storageInput = new StorageInput(TestUtils.TABLE_NAME_MEMORY_STORAGE).withReference(storageReference);
|
||||
try(OutputStream outputStream = new StorageAction().createOutputStream(storageInput))
|
||||
{
|
||||
outputStream.write((getPersonCsvHeaderUsingLabels() + """
|
||||
"1","2021-10-26 14:39:37","2021-10-26 14:39:37","John","Doe","1980-01-01","john@doe.com","Missouri",42
|
||||
"2","2021-10-26 14:39:37","2021-10-26 14:39:37","not-pre-Error plane","Doe","1980-01-01","john@doe.com","Missouri",42
|
||||
"3","2021-10-26 14:39:37","2021-10-26 14:39:37","Error purifier","Doe","1980-01-01","john@doe.com","Missouri",42
|
||||
""").getBytes());
|
||||
}
|
||||
return storageInput;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private static RunProcessOutput startProcess(RunProcessInput runProcessInput) throws QException
|
||||
{
|
||||
runProcessInput.setProcessName(TestUtils.TABLE_NAME_PERSON_MEMORY + ".bulkEditWithFile");
|
||||
runProcessInput.addValue("tableName", TestUtils.TABLE_NAME_PERSON_MEMORY);
|
||||
runProcessInput.addValue("isBulkEdit", "true");
|
||||
RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput);
|
||||
return runProcessOutput;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private static void addProfileToRunProcessInput(RunProcessInput input)
|
||||
{
|
||||
input.addValue("version", "v1");
|
||||
input.addValue("layout", "FLAT");
|
||||
input.addValue("isBulkEdit", "true");
|
||||
input.addValue("keyFields", "id");
|
||||
input.addValue("hasHeaderRow", "true");
|
||||
input.addValue("fieldListJSON", JsonUtils.toJson(List.of(
|
||||
new BulkLoadProfileField().withFieldName("id").withColumnIndex(0),
|
||||
new BulkLoadProfileField().withFieldName("firstName").withColumnIndex(3),
|
||||
new BulkLoadProfileField().withFieldName("lastName").withColumnIndex(4),
|
||||
new BulkLoadProfileField().withFieldName("email").withDefaultValue(defaultEmail),
|
||||
new BulkLoadProfileField().withFieldName("homeStateId").withColumnIndex(7).withDoValueMapping(true).withValueMappings(Map.of("Illinois", 1)),
|
||||
new BulkLoadProfileField().withFieldName("noOfShoes").withColumnIndex(8)
|
||||
)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public static class PersonWarnOrErrorCustomizer implements TableCustomizerInterface
|
||||
{
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public AbstractPreInsertCustomizer.WhenToRun whenToRunPreInsert(InsertInput insertInput, boolean isPreview)
|
||||
{
|
||||
return AbstractPreInsertCustomizer.WhenToRun.BEFORE_ALL_VALIDATIONS;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public List<QRecord> preInsert(InsertInput insertInput, List<QRecord> records, boolean isPreview) throws QException
|
||||
{
|
||||
for(QRecord record : records)
|
||||
{
|
||||
if(record.getValueString("firstName").toLowerCase().contains("warn"))
|
||||
{
|
||||
record.addWarning(new QWarningMessage(record.getValueString("firstName")));
|
||||
}
|
||||
else if(record.getValueString("firstName").toLowerCase().contains("error"))
|
||||
{
|
||||
if(isPreview && record.getValueString("firstName").toLowerCase().contains("not-pre-error"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
record.addError(new BadInputStatusMessage(record.getValueString("firstName")));
|
||||
}
|
||||
}
|
||||
return records;
|
||||
}
|
||||
}
|
||||
}
|
@ -67,7 +67,7 @@ import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
/*******************************************************************************
|
||||
** Unit test for full bulk insert process
|
||||
*******************************************************************************/
|
||||
class BulkInsertFullProcessTest extends BaseTest
|
||||
public class BulkInsertFullProcessTest extends BaseTest
|
||||
{
|
||||
private static final String defaultEmail = "noone@kingsrook.com";
|
||||
|
||||
@ -125,7 +125,7 @@ class BulkInsertFullProcessTest extends BaseTest
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void test() throws Exception
|
||||
public void test() throws Exception
|
||||
{
|
||||
assertThat(TestUtils.queryTable(TestUtils.TABLE_NAME_PERSON_MEMORY)).isEmpty();
|
||||
|
||||
@ -224,7 +224,7 @@ class BulkInsertFullProcessTest extends BaseTest
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testSummaryLinePrimaryKeys() throws Exception
|
||||
public void testSummaryLinePrimaryKeys() throws Exception
|
||||
{
|
||||
assertThat(TestUtils.queryTable(TestUtils.TABLE_NAME_PERSON_MEMORY)).isEmpty();
|
||||
QContext.getQInstance().getTable(TestUtils.TABLE_NAME_PERSON_MEMORY)
|
||||
@ -267,7 +267,7 @@ class BulkInsertFullProcessTest extends BaseTest
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testSummaryLineErrors() throws Exception
|
||||
public void testSummaryLineErrors() throws Exception
|
||||
{
|
||||
assertThat(TestUtils.queryTable(TestUtils.TABLE_NAME_PERSON_MEMORY)).isEmpty();
|
||||
QContext.getQInstance().getTable(TestUtils.TABLE_NAME_PERSON_MEMORY)
|
||||
@ -304,7 +304,7 @@ class BulkInsertFullProcessTest extends BaseTest
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testOneRow() throws Exception
|
||||
public void testOneRow() throws Exception
|
||||
{
|
||||
///////////////////////////////////////
|
||||
// make sure table is empty to start //
|
||||
@ -514,4 +514,4 @@ class BulkInsertFullProcessTest extends BaseTest
|
||||
return records;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Unit test for BulkLoadMappingSuggester
|
||||
** Unit test for BulkLoadMappingSuggester
|
||||
*******************************************************************************/
|
||||
class BulkLoadMappingSuggesterTest extends BaseTest
|
||||
{
|
||||
@ -52,7 +52,7 @@ class BulkLoadMappingSuggesterTest extends BaseTest
|
||||
BulkLoadTableStructure tableStructure = BulkLoadTableStructureBuilder.buildTableStructure(TestUtils.TABLE_NAME_PERSON_MEMORY);
|
||||
List<String> headerRow = List.of("Id", "First Name", "lastname", "email", "homestate");
|
||||
|
||||
BulkLoadProfile bulkLoadProfile = new BulkLoadMappingSuggester().suggestBulkLoadMappingProfile(tableStructure, headerRow);
|
||||
BulkLoadProfile bulkLoadProfile = new BulkLoadMappingSuggester().suggestBulkLoadMappingProfile(tableStructure, headerRow, false);
|
||||
assertEquals("v1", bulkLoadProfile.getVersion());
|
||||
assertEquals("FLAT", bulkLoadProfile.getLayout());
|
||||
assertNull(getFieldByName(bulkLoadProfile, "id"));
|
||||
@ -73,7 +73,7 @@ class BulkLoadMappingSuggesterTest extends BaseTest
|
||||
BulkLoadTableStructure tableStructure = BulkLoadTableStructureBuilder.buildTableStructure(TestUtils.TABLE_NAME_ORDER);
|
||||
List<String> headerRow = List.of("orderNo", "shipto name", "sku", "quantity");
|
||||
|
||||
BulkLoadProfile bulkLoadProfile = new BulkLoadMappingSuggester().suggestBulkLoadMappingProfile(tableStructure, headerRow);
|
||||
BulkLoadProfile bulkLoadProfile = new BulkLoadMappingSuggester().suggestBulkLoadMappingProfile(tableStructure, headerRow, false);
|
||||
assertEquals("v1", bulkLoadProfile.getVersion());
|
||||
assertEquals("TALL", bulkLoadProfile.getLayout());
|
||||
assertEquals(0, getFieldByName(bulkLoadProfile, "orderNo").getColumnIndex());
|
||||
@ -93,7 +93,7 @@ class BulkLoadMappingSuggesterTest extends BaseTest
|
||||
BulkLoadTableStructure tableStructure = BulkLoadTableStructureBuilder.buildTableStructure(TestUtils.TABLE_NAME_ORDER);
|
||||
List<String> headerRow = List.of("Order No", "Ship To Name", "Order Line: SKU", "Order Line: Quantity");
|
||||
|
||||
BulkLoadProfile bulkLoadProfile = new BulkLoadMappingSuggester().suggestBulkLoadMappingProfile(tableStructure, headerRow);
|
||||
BulkLoadProfile bulkLoadProfile = new BulkLoadMappingSuggester().suggestBulkLoadMappingProfile(tableStructure, headerRow, false);
|
||||
assertEquals("v1", bulkLoadProfile.getVersion());
|
||||
assertEquals("TALL", bulkLoadProfile.getLayout());
|
||||
assertEquals(0, getFieldByName(bulkLoadProfile, "orderNo").getColumnIndex());
|
||||
@ -120,7 +120,7 @@ class BulkLoadMappingSuggesterTest extends BaseTest
|
||||
BulkLoadTableStructure tableStructure = BulkLoadTableStructureBuilder.buildTableStructure(TestUtils.TABLE_NAME_ORDER);
|
||||
List<String> headerRow = List.of("orderNo", "ship to name", "address 1", "address 2");
|
||||
|
||||
BulkLoadProfile bulkLoadProfile = new BulkLoadMappingSuggester().suggestBulkLoadMappingProfile(tableStructure, headerRow);
|
||||
BulkLoadProfile bulkLoadProfile = new BulkLoadMappingSuggester().suggestBulkLoadMappingProfile(tableStructure, headerRow, false);
|
||||
assertEquals(0, getFieldByName(bulkLoadProfile, "orderNo").getColumnIndex());
|
||||
assertEquals(1, getFieldByName(bulkLoadProfile, "shipToName").getColumnIndex());
|
||||
assertEquals(2, getFieldByName(bulkLoadProfile, "address1").getColumnIndex());
|
||||
@ -136,7 +136,7 @@ class BulkLoadMappingSuggesterTest extends BaseTest
|
||||
BulkLoadTableStructure tableStructure = BulkLoadTableStructureBuilder.buildTableStructure(TestUtils.TABLE_NAME_ORDER);
|
||||
List<String> headerRow = List.of("orderNo", "ship to name", "address 1", "address 2");
|
||||
|
||||
BulkLoadProfile bulkLoadProfile = new BulkLoadMappingSuggester().suggestBulkLoadMappingProfile(tableStructure, headerRow);
|
||||
BulkLoadProfile bulkLoadProfile = new BulkLoadMappingSuggester().suggestBulkLoadMappingProfile(tableStructure, headerRow, false);
|
||||
assertEquals(0, getFieldByName(bulkLoadProfile, "orderNo").getColumnIndex());
|
||||
assertEquals(1, getFieldByName(bulkLoadProfile, "shipToName").getColumnIndex());
|
||||
assertEquals(2, getFieldByName(bulkLoadProfile, "address").getColumnIndex());
|
||||
@ -152,7 +152,7 @@ class BulkLoadMappingSuggesterTest extends BaseTest
|
||||
BulkLoadTableStructure tableStructure = BulkLoadTableStructureBuilder.buildTableStructure(TestUtils.TABLE_NAME_ORDER);
|
||||
List<String> headerRow = List.of("orderNo", "ship to name", "address", "address 2");
|
||||
|
||||
BulkLoadProfile bulkLoadProfile = new BulkLoadMappingSuggester().suggestBulkLoadMappingProfile(tableStructure, headerRow);
|
||||
BulkLoadProfile bulkLoadProfile = new BulkLoadMappingSuggester().suggestBulkLoadMappingProfile(tableStructure, headerRow, false);
|
||||
assertEquals(0, getFieldByName(bulkLoadProfile, "orderNo").getColumnIndex());
|
||||
assertEquals(1, getFieldByName(bulkLoadProfile, "shipToName").getColumnIndex());
|
||||
assertEquals(2, getFieldByName(bulkLoadProfile, "address1").getColumnIndex());
|
||||
@ -177,7 +177,7 @@ class BulkLoadMappingSuggesterTest extends BaseTest
|
||||
BulkLoadTableStructure tableStructure = BulkLoadTableStructureBuilder.buildTableStructure(TestUtils.TABLE_NAME_ORDER);
|
||||
List<String> headerRow = List.of("orderNo", "ship to name", "sku", "quantity1", "sku 2", "quantity 2");
|
||||
|
||||
BulkLoadProfile bulkLoadProfile = new BulkLoadMappingSuggester().suggestBulkLoadMappingProfile(tableStructure, headerRow);
|
||||
BulkLoadProfile bulkLoadProfile = new BulkLoadMappingSuggester().suggestBulkLoadMappingProfile(tableStructure, headerRow, false);
|
||||
assertEquals("v1", bulkLoadProfile.getVersion());
|
||||
assertEquals("WIDE", bulkLoadProfile.getLayout());
|
||||
assertEquals(0, getFieldByName(bulkLoadProfile, "orderNo").getColumnIndex());
|
||||
@ -206,4 +206,4 @@ class BulkLoadMappingSuggesterTest extends BaseTest
|
||||
.findFirst().orElse(null));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1940,6 +1940,8 @@ public class QJavalinImplementation
|
||||
input.setSearchTerm(searchTerm);
|
||||
input.setDefaultQueryFilter(defaultFilter);
|
||||
input.setOtherValues(otherValues);
|
||||
input.setPathParamMap(context.pathParamMap());
|
||||
input.setQueryParamMap(context.queryParamMap());
|
||||
|
||||
if(StringUtils.hasContent(ids))
|
||||
{
|
||||
|
@ -1909,6 +1909,13 @@ paths:
|
||||
name: "person.bulkDelete"
|
||||
stepFlow: "LINEAR"
|
||||
tableName: "person"
|
||||
person.bulkEditWithFile:
|
||||
hasPermission: true
|
||||
isHidden: true
|
||||
label: "Person Bulk Edit With File"
|
||||
name: "person.bulkEditWithFile"
|
||||
stepFlow: "LINEAR"
|
||||
tableName: "person"
|
||||
tables:
|
||||
person:
|
||||
capabilities:
|
||||
|
Reference in New Issue
Block a user