diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/AbstractPreUpdateCustomizer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/AbstractPreUpdateCustomizer.java index a0c927c4..3a1a13da 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/AbstractPreUpdateCustomizer.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/AbstractPreUpdateCustomizer.java @@ -52,6 +52,7 @@ public abstract class AbstractPreUpdateCustomizer { protected UpdateInput updateInput; protected List oldRecordList; + protected boolean isPreview = false; private Map oldRecordMap = null; @@ -123,4 +124,35 @@ public abstract class AbstractPreUpdateCustomizer return (oldRecordMap); } + + + /******************************************************************************* + ** Getter for isPreview + *******************************************************************************/ + public boolean getIsPreview() + { + return (this.isPreview); + } + + + + /******************************************************************************* + ** Setter for isPreview + *******************************************************************************/ + public void setIsPreview(boolean isPreview) + { + this.isPreview = isPreview; + } + + + + /******************************************************************************* + ** Fluent setter for isPreview + *******************************************************************************/ + public AbstractPreUpdateCustomizer withIsPreview(boolean isPreview) + { + this.isPreview = isPreview; + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/UpdateAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/UpdateAction.java index f6da6666..37053c96 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/UpdateAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/UpdateAction.java @@ -105,30 +105,7 @@ public class UpdateAction //////////////////////////////////////////////////////////////////////////////// Optional> oldRecordList = fetchOldRecords(updateInput, updateInterface); - ///////////////////////////// - // run standard validators // - ///////////////////////////// - ValueBehaviorApplier.applyFieldBehaviors(updateInput.getInstance(), table, updateInput.getRecords()); - validatePrimaryKeysAreGiven(updateInput); - - if(oldRecordList.isPresent()) - { - validateRecordsExistAndCanBeAccessed(updateInput, oldRecordList.get()); - } - - validateRequiredFields(updateInput); - ValidateRecordSecurityLockHelper.validateSecurityFields(table, updateInput.getRecords(), ValidateRecordSecurityLockHelper.Action.UPDATE); - - /////////////////////////////////////////////////////////////////////////// - // after all validations, run the pre-update customizer, if there is one // - /////////////////////////////////////////////////////////////////////////// - Optional preUpdateCustomizer = QCodeLoader.getTableCustomizer(AbstractPreUpdateCustomizer.class, table, TableCustomizers.PRE_UPDATE_RECORD.getRole()); - if(preUpdateCustomizer.isPresent()) - { - preUpdateCustomizer.get().setUpdateInput(updateInput); - oldRecordList.ifPresent(l -> preUpdateCustomizer.get().setOldRecordList(l)); - updateInput.setRecords(preUpdateCustomizer.get().apply(updateInput.getRecords())); - } + performValidations(updateInput, oldRecordList, false); //////////////////////////////////// // have the backend do the update // @@ -191,6 +168,42 @@ public class UpdateAction + /******************************************************************************* + ** + *******************************************************************************/ + public void performValidations(UpdateInput updateInput, Optional> oldRecordList, boolean isPreview) throws QException + { + QTableMetaData table = updateInput.getTable(); + + ///////////////////////////// + // run standard validators // + ///////////////////////////// + ValueBehaviorApplier.applyFieldBehaviors(updateInput.getInstance(), table, updateInput.getRecords()); + validatePrimaryKeysAreGiven(updateInput); + + if(oldRecordList.isPresent()) + { + validateRecordsExistAndCanBeAccessed(updateInput, oldRecordList.get()); + } + + validateRequiredFields(updateInput); + ValidateRecordSecurityLockHelper.validateSecurityFields(table, updateInput.getRecords(), ValidateRecordSecurityLockHelper.Action.UPDATE); + + /////////////////////////////////////////////////////////////////////////// + // after all validations, run the pre-update customizer, if there is one // + /////////////////////////////////////////////////////////////////////////// + Optional preUpdateCustomizer = QCodeLoader.getTableCustomizer(AbstractPreUpdateCustomizer.class, table, TableCustomizers.PRE_UPDATE_RECORD.getRole()); + if(preUpdateCustomizer.isPresent()) + { + preUpdateCustomizer.get().setUpdateInput(updateInput); + preUpdateCustomizer.get().setIsPreview(isPreview); + oldRecordList.ifPresent(l -> preUpdateCustomizer.get().setOldRecordList(l)); + updateInput.setRecords(preUpdateCustomizer.get().apply(updateInput.getRecords())); + } + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -318,7 +331,7 @@ public class UpdateAction ///////////////////////////////////////////////////////////////////////////////////////////// if(record.getValues().containsKey(requiredField.getName())) { - if(record.getValue(requiredField.getName()) == null || (requiredField.getType().isStringLike() && record.getValueString(requiredField.getName()).trim().equals(""))) + if(record.getValue(requiredField.getName()) == null || record.getValueString(requiredField.getName()).trim().equals("")) { record.addError(new BadInputStatusMessage("Missing value in required field: " + requiredField.getLabel())); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java index 12fc2206..4b1bfa9f 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java @@ -66,13 +66,13 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QMiddlewareTableMeta import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier; import com.kingsrook.qqq.backend.core.processes.implementations.bulk.delete.BulkDeleteTransformStep; +import com.kingsrook.qqq.backend.core.processes.implementations.bulk.edit.BulkEditLoadStep; import com.kingsrook.qqq.backend.core.processes.implementations.bulk.edit.BulkEditTransformStep; import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertExtractStep; import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertTransformStep; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.LoadViaDeleteStep; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.LoadViaInsertStep; -import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.LoadViaUpdateStep; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; @@ -761,7 +761,7 @@ public class QInstanceEnricher QProcessMetaData process = StreamedETLWithFrontendProcess.defineProcessMetaData( ExtractViaQueryStep.class, BulkEditTransformStep.class, - LoadViaUpdateStep.class, + BulkEditLoadStep.class, values ) .withName(processName) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/edit/BulkEditLoadStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/edit/BulkEditLoadStep.java new file mode 100644 index 00000000..d281d3f7 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/edit/BulkEditLoadStep.java @@ -0,0 +1,130 @@ +/* + * 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 . + */ + +package com.kingsrook.qqq.backend.core.processes.implementations.bulk.edit; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import com.kingsrook.qqq.backend.core.exceptions.QException; +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.RunBackendStepInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; +import com.kingsrook.qqq.backend.core.model.actions.processes.Status; +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.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 static com.kingsrook.qqq.backend.core.processes.implementations.bulk.edit.BulkEditTransformStep.buildInfoSummaryLines; +import static com.kingsrook.qqq.backend.core.processes.implementations.bulk.edit.BulkEditTransformStep.getProcessSummaryWarningsAndErrorsRollup; + + +/******************************************************************************* + ** Load step for generic table bulk-edit ETL process + *******************************************************************************/ +public class BulkEditLoadStep extends LoadViaUpdateStep implements ProcessSummaryProviderInterface +{ + public static final String FIELD_ENABLED_FIELDS = "bulkEditEnabledFields"; + + private ProcessSummaryLine okSummary = new ProcessSummaryLine(Status.OK); + private List infoSummaries = new ArrayList<>(); + + private ProcessSummaryWarningsAndErrorsRollup processSummaryWarningsAndErrorsRollup = getProcessSummaryWarningsAndErrorsRollup(); + + private String tableLabel; + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public ArrayList getProcessSummary(RunBackendStepOutput runBackendStepOutput, boolean isForResultScreen) + { + ArrayList rs = new ArrayList<>(); + + String noWarningsSuffix = processSummaryWarningsAndErrorsRollup.countWarnings() == 0 ? "" : " with no warnings"; + + okSummary.setSingularPastMessage(tableLabel + " record was edited" + noWarningsSuffix + "."); + okSummary.setPluralPastMessage(tableLabel + " records were edited" + noWarningsSuffix + "."); + okSummary.pickMessage(isForResultScreen); + okSummary.addSelfToListIfAnyCount(rs); + + processSummaryWarningsAndErrorsRollup.addToList(rs); + rs.addAll(infoSummaries); + return (rs); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void preRun(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException + { + super.preRun(runBackendStepInput, runBackendStepOutput); + + QTableMetaData table = runBackendStepInput.getInstance().getTable(runBackendStepInput.getTableName()); + if(table != null) + { + tableLabel = table.getLabel(); + } + + buildInfoSummaryLines(runBackendStepInput, table, infoSummaries, true); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException + { + QTableMetaData table = runBackendStepInput.getInstance().getTable(runBackendStepInput.getTableName()); + + super.run(runBackendStepInput, runBackendStepOutput); + for(QRecord record : runBackendStepOutput.getRecords()) + { + Serializable recordPrimaryKey = record.getValue(table.getPrimaryKeyField()); + if(CollectionUtils.nullSafeHasContents(record.getErrors())) + { + String message = record.getErrors().get(0).getMessage(); + processSummaryWarningsAndErrorsRollup.addError(message, recordPrimaryKey); + } + else if(CollectionUtils.nullSafeHasContents(record.getWarnings())) + { + String message = record.getWarnings().get(0).getMessage(); + processSummaryWarningsAndErrorsRollup.addWarning(message, recordPrimaryKey); + } + else + { + okSummary.incrementCountAndAddPrimaryKey(recordPrimaryKey); + } + } + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/edit/BulkEditTransformStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/edit/BulkEditTransformStep.java index b3dddc99..ddb8d2c6 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/edit/BulkEditTransformStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/edit/BulkEditTransformStep.java @@ -24,7 +24,11 @@ package com.kingsrook.qqq.backend.core.processes.implementations.bulk.edit; import java.io.Serializable; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Optional; +import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator; import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter; import com.kingsrook.qqq.backend.core.exceptions.QException; @@ -33,11 +37,14 @@ import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLine import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; import com.kingsrook.qqq.backend.core.model.actions.processes.Status; +import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.AbstractTransformStep; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess; +import com.kingsrook.qqq.backend.core.processes.implementations.general.ProcessSummaryWarningsAndErrorsRollup; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils; @@ -52,6 +59,8 @@ public class BulkEditTransformStep extends AbstractTransformStep private ProcessSummaryLine okSummary = new ProcessSummaryLine(Status.OK); private List infoSummaries = new ArrayList<>(); + private ProcessSummaryWarningsAndErrorsRollup processSummaryWarningsAndErrorsRollup = getProcessSummaryWarningsAndErrorsRollup(); + private QTableMetaData table; private String tableLabel; private String[] enabledFields; @@ -62,6 +71,36 @@ public class BulkEditTransformStep extends AbstractTransformStep + /******************************************************************************* + ** used by Load step too + *******************************************************************************/ + static ProcessSummaryWarningsAndErrorsRollup getProcessSummaryWarningsAndErrorsRollup() + { + return new ProcessSummaryWarningsAndErrorsRollup() + .withErrorTemplate(new ProcessSummaryLine(Status.ERROR) + .withSingularFutureMessage("record has an error: ") + .withPluralFutureMessage("records have an error: ") + .withSingularPastMessage("record had an error: ") + .withPluralPastMessage("records had an error: ")) + .withWarningTemplate(new ProcessSummaryLine(Status.WARNING) + .withSingularFutureMessage("record will be edited, but has a warning: ") + .withPluralFutureMessage("records will be edited, but have a warning: ") + .withSingularPastMessage("record was edited, but had a warning: ") + .withPluralPastMessage("records were edited, but had a warning: ")) + .withOtherErrorsSummary(new ProcessSummaryLine(Status.ERROR) + .withSingularFutureMessage("record has an other error.") + .withPluralFutureMessage("records have other errors.") + .withSingularPastMessage("record had an other error.") + .withPluralPastMessage("records had other errors.")) + .withOtherWarningsSummary(new ProcessSummaryLine(Status.WARNING) + .withSingularFutureMessage("record will be edited, but has an other warning.") + .withPluralFutureMessage("records will be edited, but have other warnings.") + .withSingularPastMessage("record was edited, but had other warnings.") + .withPluralPastMessage("records were edited, but had other warnings.")); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -77,14 +116,14 @@ public class BulkEditTransformStep extends AbstractTransformStep tableLabel = table.getLabel(); } - String enabledFieldsString = runBackendStepInput.getValueString(FIELD_ENABLED_FIELDS); - enabledFields = enabledFieldsString.split(","); - isValidateStep = runBackendStepInput.getStepName().equals(StreamedETLWithFrontendProcess.STEP_NAME_VALIDATE); isExecuteStep = runBackendStepInput.getStepName().equals(StreamedETLWithFrontendProcess.STEP_NAME_EXECUTE); haveRecordCount = runBackendStepInput.getValue(StreamedETLWithFrontendProcess.FIELD_RECORD_COUNT) != null; - buildInfoSummaryLines(runBackendStepInput, enabledFields); + String enabledFieldsString = runBackendStepInput.getValueString(FIELD_ENABLED_FIELDS); + enabledFields = enabledFieldsString.split(","); + + buildInfoSummaryLines(runBackendStepInput, table, infoSummaries, isExecuteStep); } @@ -129,22 +168,63 @@ public class BulkEditTransformStep extends AbstractTransformStep outputRecords.add(recordToUpdate); setUpdatedFieldsInRecord(runBackendStepInput, enabledFields, recordToUpdate); } + + okSummary.incrementCount(runBackendStepInput.getRecords().size()); } else { - //////////////////////////////////////////////////////////////////////////////////////////// - // put the value in all the records (note, this is just for display on the review screen, // - // and/or if we wanted to do some validation - this is NOT what will be store, as the // - // Update action only wants fields that are being changed. // - //////////////////////////////////////////////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////////////////// + // Build records-to-update for passing into the validation method of the Update action // + ///////////////////////////////////////////////////////////////////////////////////////// + List recordsForValidation = new ArrayList<>(); + Map pkeyToFullRecordMap = new HashMap<>(); for(QRecord record : runBackendStepInput.getRecords()) { - outputRecords.add(record); + QRecord recordToUpdate = new QRecord(); + recordToUpdate.setValue(table.getPrimaryKeyField(), record.getValue(table.getPrimaryKeyField())); + setUpdatedFieldsInRecord(runBackendStepInput, enabledFields, recordToUpdate); + recordsForValidation.add(recordToUpdate); + + ///////////////////////////////////////////////////////////// + // put the full record (with updated values) in the output // + ///////////////////////////////////////////////////////////// setUpdatedFieldsInRecord(runBackendStepInput, enabledFields, record); + pkeyToFullRecordMap.put(record.getValue(table.getPrimaryKeyField()), record); + } + + /////////////////////////////////////////////////////////////////////// + // run the validation - critically - in preview mode (boolean param) // + /////////////////////////////////////////////////////////////////////// + UpdateInput updateInput = new UpdateInput(); + updateInput.setTableName(table.getName()); + updateInput.setRecords(recordsForValidation); + new UpdateAction().performValidations(updateInput, Optional.of(runBackendStepInput.getRecords()), true); + + ///////////////////////////////////////////////////////////// + // look at the update input to build process summary lines // + ///////////////////////////////////////////////////////////// + for(QRecord record : updateInput.getRecords()) + { + Serializable recordPrimaryKey = record.getValue(table.getPrimaryKeyField()); + if(CollectionUtils.nullSafeHasContents(record.getErrors())) + { + String message = record.getErrors().get(0).getMessage(); + processSummaryWarningsAndErrorsRollup.addError(message, recordPrimaryKey); + } + else if(CollectionUtils.nullSafeHasContents(record.getWarnings())) + { + String message = record.getWarnings().get(0).getMessage(); + processSummaryWarningsAndErrorsRollup.addWarning(message, recordPrimaryKey); + outputRecords.add(pkeyToFullRecordMap.get(recordPrimaryKey)); + } + else + { + okSummary.incrementCountAndAddPrimaryKey(recordPrimaryKey); + outputRecords.add(pkeyToFullRecordMap.get(recordPrimaryKey)); + } } } runBackendStepOutput.setRecords(outputRecords); - okSummary.incrementCount(runBackendStepInput.getRecords().size()); } @@ -152,9 +232,11 @@ public class BulkEditTransformStep extends AbstractTransformStep /******************************************************************************* ** *******************************************************************************/ - private void buildInfoSummaryLines(RunBackendStepInput runBackendStepInput, String[] enabledFields) + static void buildInfoSummaryLines(RunBackendStepInput runBackendStepInput, QTableMetaData table, List infoSummaries, boolean isExecuteStep) { - QValueFormatter qValueFormatter = new QValueFormatter(); + String enabledFieldsString = runBackendStepInput.getValueString(FIELD_ENABLED_FIELDS); + String[] enabledFields = enabledFieldsString.split(","); + for(String fieldName : enabledFields) { QFieldMetaData field = table.getField(fieldName); @@ -168,7 +250,7 @@ public class BulkEditTransformStep extends AbstractTransformStep String verb = isExecuteStep ? "was" : "will be"; if(StringUtils.hasContent(ValueUtils.getValueAsString(value))) { - String formattedValue = qValueFormatter.formatValue(field, value); + String formattedValue = QValueFormatter.formatValue(field, value); if(field.getPossibleValueSourceName() != null) { @@ -211,15 +293,19 @@ public class BulkEditTransformStep extends AbstractTransformStep @Override public ArrayList getProcessSummary(RunBackendStepOutput runBackendStepOutput, boolean isForResultScreen) { - okSummary.setSingularFutureMessage(tableLabel + " record will be edited."); - okSummary.setPluralFutureMessage(tableLabel + " records will be edited."); - okSummary.setSingularPastMessage(tableLabel + " record was edited."); - okSummary.setPluralPastMessage(tableLabel + " records were edited."); - okSummary.pickMessage(isForResultScreen); - ArrayList rs = new ArrayList<>(); - rs.add(okSummary); + + String noWarningsSuffix = processSummaryWarningsAndErrorsRollup.countWarnings() == 0 ? "" : " with no warnings"; + + okSummary.setSingularFutureMessage(tableLabel + " record will be edited" + noWarningsSuffix + "."); + okSummary.setPluralFutureMessage(tableLabel + " records will be edited" + noWarningsSuffix + "."); + okSummary.pickMessage(isForResultScreen); + okSummary.addSelfToListIfAnyCount(rs); + + processSummaryWarningsAndErrorsRollup.addToList(rs); + rs.addAll(infoSummaries); return (rs); } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/general/ProcessSummaryWarningsAndErrorsRollup.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/general/ProcessSummaryWarningsAndErrorsRollup.java new file mode 100644 index 00000000..8bef8eeb --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/general/ProcessSummaryWarningsAndErrorsRollup.java @@ -0,0 +1,309 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. 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 . + */ + +package com.kingsrook.qqq.backend.core.processes.implementations.general; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +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.Status; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ProcessSummaryWarningsAndErrorsRollup +{ + private Map errorSummaries = new HashMap<>(); + private Map warningSummaries = new HashMap<>(); + + private ProcessSummaryLine otherErrorsSummary; + private ProcessSummaryLine otherWarningsSummary; + private ProcessSummaryLine errorTemplate; + private ProcessSummaryLine warningTemplate; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void addToList(ArrayList list) + { + addProcessSummaryLinesFromMap(list, errorSummaries); + if(otherErrorsSummary != null) + { + otherErrorsSummary.addSelfToListIfAnyCount(list); + } + + addProcessSummaryLinesFromMap(list, warningSummaries); + if(otherWarningsSummary != null) + { + otherWarningsSummary.addSelfToListIfAnyCount(list); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void addError(String message, Serializable primaryKey) + { + add(Status.ERROR, errorSummaries, errorTemplate, message, primaryKey); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void addWarning(String message, Serializable primaryKey) + { + add(Status.WARNING, warningSummaries, warningTemplate, message, primaryKey); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public int countWarnings() + { + int sum = 0; + for(ProcessSummaryLine processSummaryLine : warningSummaries.values()) + { + sum += Objects.requireNonNullElse(processSummaryLine.getCount(), 0); + } + if(otherWarningsSummary != null) + { + sum += Objects.requireNonNullElse(otherWarningsSummary.getCount(), 0); + } + return (sum); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public int countErrors() + { + int sum = 0; + for(ProcessSummaryLine processSummaryLine : errorSummaries.values()) + { + sum += Objects.requireNonNullElse(processSummaryLine.getCount(), 0); + } + if(otherErrorsSummary != null) + { + sum += Objects.requireNonNullElse(otherErrorsSummary.getCount(), 0); + } + return (sum); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void add(Status status, Map summaryLineMap, ProcessSummaryLine templateLine, String message, Serializable primaryKey) + { + ProcessSummaryLine processSummaryLine = summaryLineMap.get(message); + if(processSummaryLine == null) + { + if(summaryLineMap.size() < 50) + { + processSummaryLine = new ProcessSummaryLine(status) + .withMessageSuffix(message) + .withSingularFutureMessage(templateLine.getSingularFutureMessage()) + .withPluralFutureMessage(templateLine.getPluralFutureMessage()) + .withSingularPastMessage(templateLine.getSingularPastMessage()) + .withPluralPastMessage(templateLine.getPluralPastMessage()); + summaryLineMap.put(message, processSummaryLine); + } + else + { + if(status.equals(Status.ERROR)) + { + if(otherErrorsSummary == null) + { + otherErrorsSummary = new ProcessSummaryLine(Status.ERROR).withMessageSuffix("records had an other error."); + } + processSummaryLine = otherErrorsSummary; + } + else + { + if(otherWarningsSummary == null) + { + otherWarningsSummary = new ProcessSummaryLine(Status.WARNING).withMessageSuffix("records had an other warning."); + } + processSummaryLine = otherWarningsSummary; + } + } + } + processSummaryLine.incrementCountAndAddPrimaryKey(primaryKey); + } + + + + /******************************************************************************* + ** sort the process summary lines by count desc + *******************************************************************************/ + private static void addProcessSummaryLinesFromMap(ArrayList rs, Map summaryMap) + { + summaryMap.values().stream() + .sorted(Comparator.comparing((ProcessSummaryLine psl) -> Objects.requireNonNullElse(psl.getCount(), 0)).reversed() + .thenComparing((ProcessSummaryLine psl) -> Objects.requireNonNullElse(psl.getMessage(), "")) + .thenComparing((ProcessSummaryLine psl) -> Objects.requireNonNullElse(psl.getMessageSuffix(), "")) + ) + .forEach(psl -> psl.addSelfToListIfAnyCount(rs)); + } + + + + /******************************************************************************* + ** Getter for otherErrorsSummary + *******************************************************************************/ + public ProcessSummaryLine getOtherErrorsSummary() + { + return (this.otherErrorsSummary); + } + + + + /******************************************************************************* + ** Setter for otherErrorsSummary + *******************************************************************************/ + public void setOtherErrorsSummary(ProcessSummaryLine otherErrorsSummary) + { + this.otherErrorsSummary = otherErrorsSummary; + } + + + + /******************************************************************************* + ** Fluent setter for otherErrorsSummary + *******************************************************************************/ + public ProcessSummaryWarningsAndErrorsRollup withOtherErrorsSummary(ProcessSummaryLine otherErrorsSummary) + { + this.otherErrorsSummary = otherErrorsSummary; + return (this); + } + + + + /******************************************************************************* + ** Getter for otherWarningsSummary + *******************************************************************************/ + public ProcessSummaryLine getOtherWarningsSummary() + { + return (this.otherWarningsSummary); + } + + + + /******************************************************************************* + ** Setter for otherWarningsSummary + *******************************************************************************/ + public void setOtherWarningsSummary(ProcessSummaryLine otherWarningsSummary) + { + this.otherWarningsSummary = otherWarningsSummary; + } + + + + /******************************************************************************* + ** Fluent setter for otherWarningsSummary + *******************************************************************************/ + public ProcessSummaryWarningsAndErrorsRollup withOtherWarningsSummary(ProcessSummaryLine otherWarningsSummary) + { + this.otherWarningsSummary = otherWarningsSummary; + return (this); + } + + + + /******************************************************************************* + ** Getter for errorTemplate + *******************************************************************************/ + public ProcessSummaryLine getErrorTemplate() + { + return (this.errorTemplate); + } + + + + /******************************************************************************* + ** Setter for errorTemplate + *******************************************************************************/ + public void setErrorTemplate(ProcessSummaryLine errorTemplate) + { + this.errorTemplate = errorTemplate; + } + + + + /******************************************************************************* + ** Fluent setter for errorTemplate + *******************************************************************************/ + public ProcessSummaryWarningsAndErrorsRollup withErrorTemplate(ProcessSummaryLine errorTemplate) + { + this.errorTemplate = errorTemplate; + return (this); + } + + + + /******************************************************************************* + ** Getter for warningTemplate + *******************************************************************************/ + public ProcessSummaryLine getWarningTemplate() + { + return (this.warningTemplate); + } + + + + /******************************************************************************* + ** Setter for warningTemplate + *******************************************************************************/ + public void setWarningTemplate(ProcessSummaryLine warningTemplate) + { + this.warningTemplate = warningTemplate; + } + + + + /******************************************************************************* + ** Fluent setter for warningTemplate + *******************************************************************************/ + public ProcessSummaryWarningsAndErrorsRollup withWarningTemplate(ProcessSummaryLine warningTemplate) + { + this.warningTemplate = warningTemplate; + return (this); + } + +} diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/edit/BulkEditTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/edit/BulkEditTest.java index dfe3f4b9..35e81866 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/edit/BulkEditTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/edit/BulkEditTest.java @@ -22,9 +22,12 @@ package com.kingsrook.qqq.backend.core.processes.implementations.bulk.edit; +import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreUpdateCustomizer; +import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers; import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; @@ -37,6 +40,10 @@ 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.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +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.etl.streamedwithfrontend.StreamedETLWithFrontendProcess; import com.kingsrook.qqq.backend.core.utils.TestUtils; @@ -143,4 +150,231 @@ class BulkEditTest extends BaseTest assertEquals("james.maes@kingsrook.com", records.get(2).getValue("email")); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testWarningsAndErrors() throws QException + { + ////////////////////////////// + // insert some test records // + ////////////////////////////// + QInstance qInstance = QContext.getQInstance(); + QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY); + List personsToLoad = new ArrayList<>(); + for(int i = 0; i < 100; i++) + { + personsToLoad.add(new QRecord().withValue("id", i).withValue("firstName", "Darin" + i)); + } + TestUtils.insertRecords(table, personsToLoad); + + table.withCustomizer(TableCustomizers.PRE_UPDATE_RECORD, new QCodeReference(PersonPreUpdateReusedMessages.class)); + + ////////////////////////////////// + // set up the run-process input // + ////////////////////////////////// + RunProcessInput runProcessInput = new RunProcessInput(); + runProcessInput.setProcessName(TestUtils.TABLE_NAME_PERSON_MEMORY + ".bulkEdit"); + runProcessInput.addValue(StreamedETLWithFrontendProcess.FIELD_DEFAULT_QUERY_FILTER, + new QQueryFilter().withCriteria(new QFilterCriteria("id", QCriteriaOperator.LESS_THAN_OR_EQUALS, 100))); + + RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput); + String processUUID = runProcessOutput.getProcessUUID(); + + runProcessInput.addValue(BulkEditTransformStep.FIELD_ENABLED_FIELDS, "firstName"); + runProcessInput.addValue("firstName", "Johnny"); + + runProcessInput.setProcessUUID(processUUID); + runProcessInput.setStartAfterStep("edit"); + runProcessOutput = new RunProcessAction().execute(runProcessInput); + assertThat(runProcessOutput.getRecords()).hasSize(0); + assertThat(runProcessOutput.getProcessState().getNextStepName()).isPresent().get().isEqualTo("review"); + + runProcessInput.addValue(StreamedETLWithFrontendProcess.FIELD_DO_FULL_VALIDATION, true); + runProcessInput.setStartAfterStep("review"); + runProcessOutput = new RunProcessAction().execute(runProcessInput); + assertThat(runProcessOutput.getRecords()).hasSize(20); + assertThat(runProcessOutput.getProcessState().getNextStepName()).isPresent().get().isEqualTo("review"); + assertThat(runProcessOutput.getValues().get(StreamedETLWithFrontendProcess.FIELD_VALIDATION_SUMMARY)).isNotNull().isInstanceOf(List.class); + + runProcessInput.setStartAfterStep("review"); + runProcessOutput = new RunProcessAction().execute(runProcessInput); + assertThat(runProcessOutput.getRecords()).hasSize(20); + assertThat(runProcessOutput.getProcessState().getNextStepName()).isPresent().get().isEqualTo("result"); + assertThat(runProcessOutput.getValues().get(StreamedETLWithFrontendProcess.FIELD_PROCESS_SUMMARY)).isNotNull().isInstanceOf(List.class); + assertThat(runProcessOutput.getException()).isEmpty(); + + @SuppressWarnings("unchecked") + List processSummaryLines = (List) runProcessOutput.getValues().get(StreamedETLWithFrontendProcess.FIELD_PROCESS_SUMMARY); + assertThat(processSummaryLines).hasSize(4); + + assertThat(processSummaryLines.get(0)) + .hasFieldOrPropertyWithValue("status", Status.OK) + .hasFieldOrPropertyWithValue("count", 10) + .matches(psl -> psl.getMessage().contains("edited with no warnings"), "expected message"); + + assertThat(processSummaryLines.get(1)) + .hasFieldOrPropertyWithValue("status", Status.ERROR) + .hasFieldOrPropertyWithValue("count", 60) + .matches(psl -> psl.getMessage().contains("Id less than 60 is error"), "expected message"); + + assertThat(processSummaryLines.get(2)) + .hasFieldOrPropertyWithValue("status", Status.WARNING) + .hasFieldOrPropertyWithValue("count", 30) + .matches(psl -> psl.getMessage().contains("Id less than 90 is warning"), "expected message"); + + List infoLines = processSummaryLines.stream().filter(psl -> psl.getStatus().equals(Status.INFO)).collect(Collectors.toList()); + assertThat(infoLines).hasSize(1); + assertThat(infoLines.stream().map(ProcessSummaryLine::getMessage)).anyMatch(m -> m.matches("(?s).*First Name.*Johnny.*")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static class PersonPreUpdateReusedMessages extends AbstractPreUpdateCustomizer + { + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public List apply(List records) throws QException + { + for(QRecord record : records) + { + Integer id = record.getValueInteger("id"); + if(id < 60) + { + record.addError(new BadInputStatusMessage("Id less than 60 is error.")); + } + else if(id < 90) + { + record.addWarning(new QWarningMessage("Id less than 90 is warning.")); + } + } + + return (records); + } + + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testUniqueWarningsAndErrors() throws QException + { + ////////////////////////////// + // insert some test records // + ////////////////////////////// + QInstance qInstance = QContext.getQInstance(); + QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY); + List personsToLoad = new ArrayList<>(); + for(int i = 0; i < 100; i++) + { + personsToLoad.add(new QRecord().withValue("id", i).withValue("firstName", "Darin" + i)); + } + TestUtils.insertRecords(table, personsToLoad); + + table.withCustomizer(TableCustomizers.PRE_UPDATE_RECORD, new QCodeReference(PersonPreUpdateUniqueMessages.class)); + + ////////////////////////////////// + // set up the run-process input // + ////////////////////////////////// + RunProcessInput runProcessInput = new RunProcessInput(); + runProcessInput.setProcessName(TestUtils.TABLE_NAME_PERSON_MEMORY + ".bulkEdit"); + runProcessInput.addValue(StreamedETLWithFrontendProcess.FIELD_DEFAULT_QUERY_FILTER, + new QQueryFilter().withCriteria(new QFilterCriteria("id", QCriteriaOperator.LESS_THAN_OR_EQUALS, 100))); + + RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput); + String processUUID = runProcessOutput.getProcessUUID(); + + runProcessInput.addValue(BulkEditTransformStep.FIELD_ENABLED_FIELDS, "firstName"); + runProcessInput.addValue("firstName", "Johnny"); + + runProcessInput.setProcessUUID(processUUID); + runProcessInput.setStartAfterStep("edit"); + runProcessOutput = new RunProcessAction().execute(runProcessInput); + + runProcessInput.addValue(StreamedETLWithFrontendProcess.FIELD_DO_FULL_VALIDATION, true); + runProcessInput.setStartAfterStep("review"); + runProcessOutput = new RunProcessAction().execute(runProcessInput); + + runProcessInput.setStartAfterStep("review"); + runProcessOutput = new RunProcessAction().execute(runProcessInput); + + @SuppressWarnings("unchecked") + List processSummaryLines = (List) runProcessOutput.getValues().get(StreamedETLWithFrontendProcess.FIELD_PROCESS_SUMMARY); + assertThat(processSummaryLines).hasSize(1 + 50 + 1 + 30 + 1); + + int index = 0; + assertThat(processSummaryLines.get(index++)) + .hasFieldOrPropertyWithValue("status", Status.OK) + .hasFieldOrPropertyWithValue("count", 10) + .matches(psl -> psl.getMessage().contains("edited with no warnings"), "expected message"); + + for(int i = 0; i < 50; i++) + { + assertThat(processSummaryLines.get(index++)) + .hasFieldOrPropertyWithValue("status", Status.ERROR) + .hasFieldOrPropertyWithValue("count", 1) + .matches(psl -> psl.getMessage().contains("less than 60 is error"), "expected message"); + } + + assertThat(processSummaryLines.get(index++)) + .hasFieldOrPropertyWithValue("status", Status.ERROR) + .hasFieldOrPropertyWithValue("count", 10) + .matches(psl -> psl.getMessage().contains("had other errors"), "expected message"); + + for(int i = 0; i < 30; i++) + { + assertThat(processSummaryLines.get(index++)) + .hasFieldOrPropertyWithValue("status", Status.WARNING) + .hasFieldOrPropertyWithValue("count", 1) + .matches(psl -> psl.getMessage().contains("less than 90 is warning"), "expected message"); + } + + assertThat(processSummaryLines.get(index++)) + .hasFieldOrPropertyWithValue("status", Status.INFO) + .matches(psl -> psl.getMessage().matches("(?s).*First Name.*Johnny.*"), "expected message"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static class PersonPreUpdateUniqueMessages extends AbstractPreUpdateCustomizer + { + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public List apply(List records) throws QException + { + for(QRecord record : records) + { + Integer id = record.getValueInteger("id"); + if(id < 60) + { + record.addError(new BadInputStatusMessage("Id [" + id + "] less than 60 is error.")); + } + else if(id < 90) + { + record.addWarning(new QWarningMessage("Id [" + id + "] less than 90 is warning.")); + } + } + + return (records); + } + + } + } \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java index 2eb4f1c0..a8fe21f7 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java @@ -289,7 +289,7 @@ public class TestUtils /******************************************************************************* ** *******************************************************************************/ - @Deprecated + @Deprecated(since = "better to call the one without qInstance param") public static void insertRecords(QInstance qInstance, QTableMetaData table, List records) throws QException { insertRecords(table, records);